KVO
文章目录
- KVO
-
- KVO的基本使用
- 手动KVO
-
- [集合 KVO 可以有两种常见使用方式](#集合 KVO 可以有两种常见使用方式)
-
- [1. 通过 Mutable Collection Proxy](#1. 通过 Mutable Collection Proxy)
- [2. 手动发送 KVO 通知](#2. 手动发送 KVO 通知)
- 依赖键
- KVO本质原理
- Q&A
-
- [为什么 person class 可能还是 Person?](#为什么 [person class] 可能还是 Person?)
- [observationInfo 在哪里?](#observationInfo 在哪里?)
- [为什么直接改 `_age` 不会触发自动 KVO?](#为什么直接改
_age不会触发自动 KVO?) - [`NSKeyValueObservingOptionInitial` 为什么不能被 `automaticallyNotifiesObserversForKey:` 阻止?](#
NSKeyValueObservingOptionInitial为什么不能被automaticallyNotifiesObserversForKey:阻止?)
KVO 是 Key-Value Observing (键值监听),用于监听某个对象属性值的改变
NSKeyValueObserving 是 Foundation 为 NSObject 提供的一个 Category,其中定义了 KVO 相关接口
因此,继承自 NSObject 的对象通常都可以使用 KVO
KVO的基本使用
- 注册监听
objc
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
-
observer 观察者对象
-
keyPath 被观察者对象的属性路径
-
options 枚举属性配置
NSKeyValueObservingOptionOld 改变前的旧值
NSKeyValueObservingOptionNew 改变后的新值
NSKeyValueObservingOptionInitial 注册后立刻触发KVO通知
NSKeyValueObservingOptionPrior 值改变前后都要通知
-
context 上下文,注册这条观察关系时自己传进去的一个标记,用来区分消息
- 回调函数
objc
- (void)observeValueForKeyPath:(nullable NSString *)keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
context:(nullable void *)context
-
KeyPath 被观察对象属性的路径
-
object 被观察对象
-
change 这个字典保存了变更消息,具体内容取决于注册时的 NSKeyValueObservingOptions
NSKeyValueChangeKindKey 这次变化属于什么类型
- NSKeyValueChangeSetting 普通的属性值被设置/改变
- NSKeyValueChangeInsertion 集合中插入了元素
- NSKeyValueChangeRemoval 集合中删除了元素
- NSKeyValueChangeReplacement 集合中的元素被替换
NSKeyValueChangeOldKey 旧值(前提是注册时请求了NSKeyValueObservingOptionOld)
NSKeyValueChangeNewKey 新值 (前提是注册时请求了NSKeyValueObservingOptionNew)
NSKeyValueChangeIndexesKey 集合发生插入、删除、替换时,告诉你哪些位置发生了变化
NSKeyValueChangeNotificationIsPriorKey NSNumber包装的YES,用于判断是不是在改变前的通知(前提是注册时请求了NSKeyValueObservingOptionPrior)
-
context 注册观察关系时由调用者传入的自定义指针,KVO 回调时会原样传回。它常用于在一个观察者同时处理多个观察关系时快速区分不同的观察处理逻辑,尤其适合配合继承和
super调用进行判断。如果不需要额外的上下文信息,也可以传入NULL,此时可以通过object和keyPath判断发生变化的对象和属性
- 移除监听
objc
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
参数必须和注册时对应,当观察者不需要再监听就要移除监听
手动KVO
默认情况下,KVO 是自动触发的,如果想要手动 KVO,就需要关闭自动通知
objc
// 默认返回 YES,表示允许 NSObject 自动产生 KVO 通知
// 如果返回 NO,则关闭该属性的自动 KVO,需要手动发送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
也可以实现遵循命名规则为+ (BOOL)automaticallyNotifiesObserversOf<Key>的方法来单一控制属性的 KVO 自动触发
objc
+ (BOOL)automaticallyNotifiesObserversOfAge {
return NO;
}
然后通过以下函数自己控制什么时候发送
objc
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
objc
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey:@"age"];
_age = age;
[self didChangeValueForKey:@"age"];
}
对于集合对象,如果要进行元素级的 KVO 通知,除了需要指定变化的类型,还需要指定发生变化的元素索引:
objc
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
- (void)didChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
对于普通属性,KVO 通常关注属性值发生了怎样的变化;而对于有序集合的元素级变化,为了让观察者准确知道集合发生了插入、删除还是替换 ,以及变化发生在哪些位置,需要通过 willChange:valuesAtIndexes:forKey: 和 didChange:valuesAtIndexes:forKey: 提供变化类型和索引信息
因此,在手动发送集合 KVO 通知时,需要指定:
changeKind:变化类型,例如NSKeyValueChangeInsertion、NSKeyValueChangeRemoval、NSKeyValueChangeReplacementindexes:发生变化的元素索引,由NSIndexSet表示
这些信息最终会反映到 KVO 回调中的 change 字典:
objc
change[NSKeyValueChangeKindKey]
change[NSKeyValueChangeIndexesKey]
其中change[NSKeyValueChangeKindKey]表示发生了什么类型的变化,例如插入、删除或替换
change[NSKeyValueChangeIndexesKey]表示哪些索引位置发生了变化,其值通常是一个 NSIndexSet
例如,手动删除 books 中指定位置的元素:
objc
- (void)removeBookAtIndex:(NSUInteger)index {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:index];
[self willChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"books"];
[self.books removeObjectAtIndex:index];
[self didChange:NSKeyValueChangeRemoval valuesAtIndexes:indexes forKey:@"books"];
}
这里真正修改数据的是[self.books removeObjectAtIndex:index];
它直接调用的是 NSMutableArray 的 removeObjectAtIndex: 方法
集合 KVO 可以有两种常见使用方式
1. 通过 Mutable Collection Proxy
objc
NSMutableArray *children = [person mutableArrayValueForKey:@"children"];
[children insertObject:child atIndex:2];
// insertObject:atIndex: 是 NSMutableArray 风格的接口,
// children 实际上是 Mutable Collection Proxy
这里主要体现的是 KVC 的集合访问机制
Proxy 会将 NSMutableArray 风格的操作转换为相应的 Collection Accessor 操作,同时能够与 KVO 的集合变化通知机制配合
详细参考 【OC】KVC
2. 手动发送 KVO 通知
如果我们自己直接修改内部数组,就可以手动告诉 KVO 发生了什么变化:
objc
- (void)insertChild:(Child *)child atIndex:(NSUInteger)index {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:index];
[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"children"];
[self.children insertObject:child atIndex:index];
[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"children"];
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"children"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
这里的执行过程非常直接:willChange 告诉 KVO:children 即将发生插入---> NSMutableArray直接修改内部数组 ---> didChange告诉 KVO:插入已经完成 ---> 通知观察者
依赖键
依赖键解决的是:当 A 发生变化时,告诉 KVO B 也应该被认为发生了变化
比如我们有一个 Person,其中fullName 并不是一个真正独立存储的值,而是计算出来的:
objc
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, copy, readonly) NSString *fullName;
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
如果 fullName 被监听,当 firstName 发生变化, fullName 的观察者也应该要收到通知,所以我们要让KVO知道这两个属性是有关系的,依赖键就是为了应对这种情况的
那要怎么告诉KVO呢?
objc
+ (NSSet<NSString *> *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}
我们可以在Person 中实现这个keyPathsForValuesAffecting<Key>类方法,它相当于告诉 KVO:fullName 这个键受到 firstName 和 lastName 的影响
当 firstName 发生变化,KVO 检查依赖关系发现 fullName 依赖 firstName,认为 fullName 也发生了变化,就会通知 fullName 的观察者
还有一个更通用的方法
objc
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSSet *affectingKeys =
[NSSet setWithObjects:@"firstName", @"lastName", nil];
keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys];
}
return keyPaths;
}
这里需要先对父类发送keyPathsForValuesAffectingValueForKey消息,以免干扰父类中对此方法的重写
这个通用方法可以根据 key 动态判断,一般会默认先调用通用方法
但是当依赖跨越多个对象,尤其依赖对象来自动态集合时,就需要自己建立观察关系
KVO本质原理
objc
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:NULL];
当建立 KVO 观察关系后,Foundation 会利用 Objective-C Runtime 的动态特性,为被观察实例建立相应的动态子类和通知机制
原来 person 的 isa 指针指向 Person 这个对象,现在 person 的 isa 指针会指向一个动态子类 NSKVONotifying_Person,动态子类的 superclass 指针指向 Person,他不是把 Person 类修改了,而是把 isa 指针指向修改了
那为什么一定要这个动态子类呢?
因为 KVO 要监控 person.age ,本质上就是监控 [person setAge:20],我们要把 setAge: 拦截下来
最方便的方法就是创建一个子类,在子类里面重写 setAge:
概念上相当于(伪代码):
objc
@implementation NSKVONotifying_Person
- (void)setAge:(NSInteger)age {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
@end
这个时候再[person setAge:20];然后 Objective-C Runtime:objc_msgSend(person, @selector(setAge:))
由于 person 的 isa 指针指向NSKVONotifying_Person,所以会优先找到NSKVONotifying_Person类里的setAge:
而这个动态子类的setAge:是被重写过有KVO机制的,执行setAge:会先执行[self willChangeValueForKey:@"age"];
执行结束后也会调用[self didChangeValueForKey:@"age"];
中间会调用[super setAge:age];也就是 Person 真正的 setter
动态子类是针对被观察的实例创建并使用的,修改的是该实例的 isa 指向,而不是直接修改原来的 Person 类。因此,同一个 Person 类的其他未被观察实例仍然使用原来的 Person 类,不会受到这个动态子类的影响
二编:KVO 动态子类会为被观察属性生成新的 Setter 实现,概念上该实现可以通过 Foundation 内部的 _NSSetIntValueAndNotify 等私有辅助函数完成自动通知,也就是说NSKVONotifying_Person类里的 setter 的 IMP(方法实现指针)会被替换为 Foundation 框架中预定义的 C 函数,这个C函数里会在执行willChange后真正调用原来Person 类的setAge:,并且由于不同类型的参数传递方式不同Foundation 针对每种属性类型都准备了对应的内部函数
| 属性类型 | setter 被替换为的内部函数 |
|---|---|
| id(对象类型) | _NSSetObjectValueAndNotify |
| int | _NSSetIntValueAndNotify |
| float | _NSSetFloatValueAndNotify |
| double | _NSSetDoubleValueAndNotify |
| BOOL | _NSSetBoolValueAndNotify |
| CGPoint | _NSSetPointValueAndNotify |
| CGSize | _NSSetSizeValueAndNotify |
| CGRect | _NSSetRectValueAndNotify |
| NSRange | _NSSetRangeValueAndNotify |
这是理解 KVO 工作方式的概念模型,并不是 Foundation 的实际源码
Q&A
为什么 person class 可能还是 Person?
objc
NSLog(@"%@", [person class]);
// 输出 Person
object_getClass(person)
// 输出真实情况 NSKVONotifying_Person
因为 KVO 的动态子类可以对 class 方法做重写处理,让外部通常仍然把它看成原来的 Person
比如内部可能这样实现
objc
- (Class)class {
return [Person class];
}
所以我们一般不会直接从isa指针看类是谁
对象在被KVO监听后,全新生成的NSKVONotifying_Person类里面,除重写了父类Person的setAge:、class之外还会重写dealloc、isKVOA方法
重写 dealloc是KVO 需要在对象销毁的时候处理自己建立的观察相关信息
isKVOA主要是给 Runtime/Foundation 内部判断:这个对象是不是 KVO 动态生成的对象?
可以把 KVO 动态子类 理解成一个"套在 Person 外面的 Runtime 代理层"------它会偷偷接管 setAge:,同时通过重写 class 等方法,让这个动态替身尽量对外表现得仍然像原来的 Person
observationInfo 在哪里?
当addObserver:之后,KVO 不仅需要改变对象的动态类型,还需要保存观察关系信息
这些观察关系信息可以通过:[person observationInfo]获得一个不透明的内部信息指针
为什么直接改 _age 不会触发自动 KVO?
因为_age = 20;没有[person setAge:20];,所以没有经过动态子类重写的setAge:也就没有willChange,didChange因此自动 KVO 不会触发
KVO 并不是通过轮询对象的属性来发现变化,而是利用 Objective-C Runtime 的动态特性,为被观察对象创建动态子类并重写相关 Setter,使属性赋值经过 KVO 的通知逻辑,从而在属性变化前后通知观察者
NSKeyValueObservingOptionInitial 为什么不能被 automaticallyNotifiesObserversForKey: 阻止?
objc
[person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionInitial context:NULL];
Initial 的意思是:注册观察关系成功后,立刻给观察者发送一次当前值的 KVO 通知
即使我们写
objc
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
注册时的 Initial 通知依然会发送,因为这两个东西管的是完全不同的事情
automaticallyNotifiesObserversForKey: 是属性发生修改时,NSObject 是否自动帮你产生 KVO 通知
而 Initial 是建立观察关系之后,立即把当前值告诉观察者