概念
KVO (Key-Value Observing),看名字为键值观察,它是观察者设计模式
的一种实现。简单来说 就是提供一种机制,对目标对象的某个属性添加观察,当该属性发生变化的时候,会通过KVO提供的接口自动通知观察者。【不需要对被观察对象添加任何额外代码,就可使用KVO机制】
使用
注册观察者
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
- observer : 观察者
- keyPath : 键路径参数,描述将要观察的属性
- options :标识KVO希望变化如何传递给观察者,可以使用|进行多选(有四个选项)
- context:上下文内存区,通常指为NULL,设置了上下文可以用上下文在 通知回调中做区分 相对来说安全
被观察者改变,收到通知,调用以下方法:
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change
context:(void *)context {}
- keyPath: 被观察者的属性
- object : 被观察者对象
- change:变化前后的值都存储在 change 字典中
- context:注册观察者时,context 传过来的值
使用了上下文的判断区分
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if (context == PersonAccountBalanceContext) {
// Do something with the balance…
} else if (context == PersonAccountInterestRateContext) {
// Do something with the interest rate…
} else {
//任何无法识别的上下文都必须属于super
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
移除观察者
///使用了上下文用这个移除
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
///没有使用用这个移除
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
触发自动kvo方法
/// set方法
[account setName:@"Savings"];
///kvo 赋值
[account setValue:@"Savings" forKey:@"name"];
[document setValue:@"Savings" forKeyPath:@"account.name"];
/// 集合类型的需要 mutableArrayValueForKey 来获取集合 并增删改查
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
手动KVO
KVO中,当被观察的属性改变时,观察者相应被触发。在某些情况下,您可能希望控制通知过程,例如,最大程度地减少出于应用程序特定原因而不必要的触发通知,或者将多个更改分组为一个通知。手动更改通知提供了执行此操作的方法
- automaticallyNotifiesObserversForKey:(NSString *)theKey
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
- 要实现手动通知需要 将
willChangeValueForKey:
放在更改值之前
和didChangeValueForKey:
之后
。
- 普通操作单个属性:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
_balance = theBalance;
[self didChangeValueForKey:@"balance"];
}
- 如果单个操作更改多个属性,则必须嵌套更改通知,如下:
- (void)setBalance:(double)theBalance {
[self willChangeValueForKey:@"balance"];
[self willChangeValueForKey:@"itemChanged"];
_balance = theBalance;
_itemChanged = _itemChanged+1;
[self didChangeValueForKey:@"itemChanged"];
[self didChangeValueForKey:@"balance"];
}
- 对于有序的一对多关系,不仅必须指定已更改的键,而且还必须指定更改的类型和所涉及对象的索引。的类型变化的是
NSKeyValueChange
,指定NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
。受影响对象的索引作为NSIndexSet
对象传递 ;如下:
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[self willChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
// Remove the transaction objects at the specified indexes.
[self didChange:NSKeyValueChangeRemoval
valuesAtIndexes:indexes forKey:@"transactions"];
}
底层原理探究
官方文档
- 自动键值观察是使用一种称为isa-swizzling的技术实现的。
- isa指针,顾名思义,指向维护分派表的对象的类。这个分派表本质上包含了指向类实现的方法和其他数据的指针。
- 当观察者为对象的属性注册时,被观察对象的isa指针将被修改,指向中间类而不是真正的类。因此,isa指针的值不一定反映实例的实际类。
- 您不应该依赖isa指针来确定类成员关系。相反,您应该使用类方法来确定对象实例的类。
实践出真理
-
当观察者为对象的属性注册时,被观察对象的isa指针将被修改
1、 通过 runtime api object_getClassName()方法 根据isa获取类
2、 分别在注册前和注册后断点打印 发现的确 是isa被修改 被修改的isa的指向为 NSKVONotifying_A 类
-
证明这个中间类到底和本类是什么关系
通过如下封装方法 遍历类及子类
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
}
由此可以证明 中间类 为 本类的 子类 也就是继承关系
-
中间类中有什么?
可通过下面封装方法
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
由此可以看出 KVO派生出的中间类 除了重写了要观察的属性的setter方法 还重写了class dealloc 因为 NSObject 都有这些方法。至于_isKVOA方法个人猜测是 派生类的一个 标识。
-
那他是继承下来的方法 还是 新建立的 标识?
- 在注册之前 向self.a 发送 _isKVOA的消息 发现返回 nil
- 在注册之前 向self.a 发送 不存在 的消息发现 直接报错 (由此可以看出 父类是有实现的 虽然没有见过)
- 注册之后 发现_isKVOA 不在返回nil了 (由此可以看出_isKVOA也是继承下来的方法)
-
dealloc中移除观察者后,isa指向是谁,以及中间类是否会销毁?
由此可以看出 在移除观察者之前 self.a 对象 的isa依旧指向的 中间类 在移除之后变回 A本类
-
那么中间类从创建后,到dealloc方法中移除观察者之后,是否还存在?
通过子类的打印结果可以看出,中间类一旦生成,没有移除,没有销毁,还在内存中 -- 主要是考虑重用的想法,即中间类注册到内存中,为了考虑后续的重用问题,所以中间类一直存在
总结
基本原理
- 当观察某对象 A 时,KVO 机制动态创建一个对象A当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法(类似手动KVO)。setter 方法随后负责通知观察对象属性的改变状况。
底层实现
- Apple 使用了
isa-swizzling
技术 来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类, 该类继承自对象A的本类,此时A的实例对象isa的指向会指向 NSKVONotifying_A 新类。 且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。当移除 KVO观察者之后 才会将 A实例对象的isa指向 指回来 指为 A。 - 中间类创建后就会存在内存中,不会被销毁
- 除了重写 被观察的属性setter ,还有 class ,delloc ,_isKVOA.