KVO使用以及原理分析

基础使用

使用KVO需要三个步骤:

  1. 在观察者中,调用被观察者的addObserver:forKeyPath:options:context:进行注册
  2. 在观察者中实现observeValueForKeyPath:ofObject:change:context:方法
  3. 在观察者中使用removeObserver:forKeyPath:移除KVO,一般可以在dealloc方法中移除,否则会导致Crash

1 注册观察者

比方说ViewController是观察者,person是其属性。ViewController需要监听person中的age属性

NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
[self.person addObserver:self forKeyPath:@"numArray" options:options context:nil];

options是一个NS_OPTIONS枚举,可以决定以下内容

  • 通知发出的时机
  • observeValueForKeyPath:ofObject:change:context:方法中,字典参数change字典中包含哪些值

参数options有四个值

  1. NSKeyValueObservingOptionNew,change字典中应该包含改变后的新值

  2. NSKeyValueObservingOptionOld,change字典中应该包含改变前的旧值

  3. NSKeyValueObservingOptionInitialaddObserver:forKeyPath:options:context:消息被发出去后,甚至不用等待这个消息返回,监听者对象会马上收到一个通知。这种通知只会发送一次,你可以利用这种“一次性“的通知来确定要监听属性的初始值。当同时制定这3个选项时,这种通知的change字典中只会包含新值,而不会包含旧值。虽然这时候的新值实际上是改变前的'旧值',但是这个值对于监听者来说是新的。

  4. NSKeyValueObservingOptionPrior:当指定了这个选项时,在被监听的属性被改变前,监听者对象就会收到一个通知(一般的通知发出时机都是在属性改变后,虽然change字典中包含了新值和旧值,但是通知还是在属性改变后才发出),这个通知会包含一个NSKeyValueChangeNotificationIsPriorKeykey,其对应的值为一个NSNumber类型的YES。当同时指定该值、new和old的话,change字典会包含旧值而不会包含新值。

2 接收通知

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;

关于change参数,它是一个字典,有五个常量作为它的键:

  1. NSKeyValueChangeKindKey:指明了变更的类型,值为NSKeyValueChange枚举中的某一个,类型为NSNumber。
    • 一般情况下返回的都是第一个NSKeyValueChangeSetting
    • 如果监听的属性是一个集合对象的话,当这个集合中的元素被插入,删除,替换时,就会分别返回NSKeyValueChangeInsertionNSKeyValueChangeRemovalNSKeyValueChangeReplacement
enum {
 NSKeyValueChangeSetting = 1,
 NSKeyValueChangeInsertion = 2,
 NSKeyValueChangeRemoval = 3,
 NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
  1. NSKeyValueChangeNewKey:被监听属性改变后新值的key。当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。

  2. NSKeyValueChangeOldKey:被监听属性改变前旧值的key。当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)

  3. NSKeyValueChangeIndexesKey:如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index

  4. NSKeyValueChangeNotificationIsPriorKey:如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.

3. 移除监听

当一个监听者完成了它的监听任务之后,就需要注销(移除)监听者,调用以下2个方法来移除监听。通常会在-dealloc方法或者observeValueForKeyPath:ofObject:change:context:方法中移除。

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context
或者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

有几点需要注意的:

当你向一个不是监听者的对象发送remove消息的时候(也可能是,你发送remove消息时,接受消息的对象已经被remove了一次,或者在注册为监听者前就调用了remove),xcode会抛出一个NSRangeException异常,所以,保险的做法是,把remove操作放在try/catch中。

一个监听者在其被销毁时,并不会自己注销监听,而给一个已经销毁的监听者发送通知,会造成野指针错误。所以至少保证,在监听者被释放前,将其监听注销。保证有一个add方法,就有一个remove方法。

4 触发KVO

触发KVO的方式包括:

  • 点语法
  • set方法
  • kvc
  • 如果监听的是集合属性,以数组为例,得使用被观察者mutableArrayValueForKey,进行操作
[[self.person mutableArrayValueForKey:@"numArray"] addObject:@(randNum)];

KVO原理

当某个对象第一次被观察时,系统就会在运行期动态地创建该类的一个子类类对象(类名就是在该类的前面加上NSKVONotifying_ 前缀),被观察对象的isa指针就指向这个类对象。这个子类类对象,会有如下的变化:

  • 被观察属性的set方法被修改
  • 修改class实例方法,让其返回原始的类对象
  • dealloc_isKVOA
    KVO之后类的关系.png

自己实现KVO的步骤

  1. NSObject的分类,提供addObserver的接口
  2. 判断isa是否指向KVONotify_class,否则生成该类
    • 使用objc_allocateClassPair生成子类
    • 给子类添加KVONotify_class.class方法,返回原始的类
    • 使用object_setClass(self, kvoClass),让被观测对象isa指向KVONotify_class
  3. 给KVONotify_class添加setter方法实现,观察属性的setter都会走这个实现
    • 从setter selector解析出属性,调用objc_msgSend方法,发送getter消息,获取oldValue
    • 使用msgSendSuper,调用原始class上的setter方法
    • 取出被观察对象的关联对象,是一个数组,元素是一个自定义类,包含观测者,观测属性,block等
  4. 向被观察对象添加关联对象,是一个数组,元素是一个自定义类,包含观测者,观测属性,block等

参考

https://www.jianshu.com/p/badf5cac0130
Demo地址:https://github.com/xiaoLong1010/DeepObjectiveC/tree/master/03-KVO

你可能感兴趣的:(KVO使用以及原理分析)