理解KVO

作者: Zombie_Coder | 来源:发表于2018-01-08 15:08 被阅读10次

    相信KVO对大家来说都不陌生,即使你没有使用过,基本也都曾了解过,听说过。
    这篇文章作者就带大家来深入研究一下KVO的实现原理,以及使用,目的是让大家真正的理解KVO。

    一. 了解方法

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;  
    注册监听者
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; 
    实现监听方法
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context; 
    注销监听
    

    看到官方给我们听过的几个方法有没有觉得和通知、代理很类似?
    其实就是一样的用法特别简单,共分为三个步骤:

    • 注册
    • 监听
    • 移除

    二. 注册监听者

    我们来看注册监听方法

    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;  
    
    我们来看几个参数:
    • observer: 监听对象(就是谁来监听KVO事件)
    • keyPath: 监听属性的名字
    • context: 添加监听方法的最后一个参数,是一个可选的参数,可以传任何数据,这个参数最后会被传到监听者的响应方法中,可以用来区分不同通知,也可以用来传值。如果你要用context来区分不同的监听知,一个推荐的做法是声明一个静态变量,其保持它自己的地址,这个变量没有什么意义,但是却能起到区分的作用,如下:
    static void *ObservePersonContext = &ObservePersonContext;
    
    • NSKeyValueObservingOptions: 监听选项是个枚举,我们来看一下都有那些类型可选
    typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    
        NSKeyValueObservingOptionNew = 0x01,
        NSKeyValueObservingOptionOld = 0x02,
        NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
        NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
    
    };
    
    • NSKeyValueObservingOptionNew:提供更改前的值
    • NSKeyValueObservingOptionOld:提供更改后的值
    • NSKeyValueObservingOptionInitial:观察最初的值(在注册观察服务时会调用一次触发方法)
    • NSKeyValueObservingOptionPrior:分别在值修改前后触发方法(即一次修改有两次触发)
    下面我们来看一段代码
    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) NSInteger age;
    
    @end
    
    #import "ViewController.h"
    #import "Person.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) Person *richPerson;
    
    @end
    
    @implementation ViewController
    
    static void *ObservePersonContext = &ObservePersonContext;
    
    - (void)viewDidLoad {
        
        [super viewDidLoad];
        
        // 1. 首先我们先实例化一个Person对象
        Person *richPerson = [[Person alloc] init];
        richPerson.name = @"光头强";
        richPerson.age = 38;
        self.richPerson = richPerson;
        [self.richPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionPrior context:ObservePersonContext];
        
        self.richPerson.age = 50;
    }
    

    以上就是一个简单的监听注册,这里大家一定要切记context的值,当监听多个对象,以及分类也使用了KVO等情况,强烈建议大家使用context来做区分。以免造成后期维护成本过高的影响。

    三. 监听

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    {
        if (context == ObservePersonContext) {
            NSLog(@"%@",keyPath);
            [self p_DoSometing];
        }
    }
    

    我们来了解一下监听方法中的几个参数

    • keyPath: 我们监听变化的属性名字。
    • object: 我们监听的对象。
    • change: 一个存储我们需求的字典类型容器,根据注册时我们选择Option来传递给我们想要的数据,有五个常量作为它的键值:
    * FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
      一般情况下返回的都是1也就是第一个NSKeyValueChangeSetting,但是如果你监听的属性是一个集合对象的话,当这个集合中的元素被插入,
      删除,替换时,就会分别返回NSKeyValueChangeInsertion,NSKeyValueChangeRemoval和NSKeyValueChangeReplacement。
    
    * FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
      被监听属性改变后新值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
      该值返回的是一个数组,包含插入,替换后的新值(删除操作不会返回新值)。
    
    * FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
      被监听属性改变前旧值的key,当监听属性为一个集合对象,且NSKeyValueChangeKindKey不为NSKeyValueChangeSetting时,
      该值返回的是一个数组,包含删除,替换前的旧值(插入操作不会返回旧值)
    
    * FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
      如果NSKeyValueChangeKindKey的值为NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 或者 NSKeyValueChangeReplacement,
      这个键的值是一个NSIndexSet对象,包含了增加,移除或者替换对象的index。
    
    * FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey
      如果注册监听者是options中指明了NSKeyValueObservingOptionPrior,change字典中就会带有这个key,值为NSNumber类型的YES.
    
    • context: 同步骤二注册!

    四. 注销监听

    - (void)dealloc
    {
        [self removeObserver:self.richPerson forKeyPath:@"age"];
    }
    

    所谓注销就和通知一样,在我们编写逻辑的View或Controller销毁的时候,注销掉我们的监听。
    当你向一个不是监听者的对象发送remove消息的时候(也可能是,你发送remove消息时,接受消息的对象已经被remove了一次,或者在注册为监听者前就调用了remove),xcode会抛出一个NSRangeException异常,所以,如果为了保险起见,可以把remove操作放在try/catch中。

    注意:

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

    OK,如果你认真看完了以上内容,恭喜你,你已经完全知道该如何来使用KVO了!
    但是这部是本篇内容的主旨。但要深刻理解,我们继续往下看!

    五. KVO的实现原理

    先上两张截图方便大家参考


    1.png
    2.png

    细心的你早已发现NSObject里的isa指针改变了
    根据两张图我们可以看出,指向了一个看着眼熟似曾相识的类(NSKVONotifying_Person)。
    没错!这个就是系统帮我们创建的Person的子类

    愿意一探究竟的小伙伴可以自己在以上例子的基础上,创建一个名字为NSKVONotifying_Person的类,编译不会有任何问题,因为OC是运行时语言,在运行程序到注册监听的时候,系统会通过runtime的方法创建一个名字为NSKVONotifying_Person的类,但是这时程序会导致崩溃。
    因为我已经手动创建了这个类。

    当然感兴趣的小伙伴也可以使用runtime的方法在注册监听之前,动态创建NSKVONotifying_Person类,也会产生同样的问题!

    总结

    KVO 很强大,没错。知道它内部实现,或许能帮助更好地使用它,或在它出错时更方便调试。但官方实现的 KVO 提供的 API 实在不怎么样。

    比如,你只能通过重写 -observeValueForKeyPath:ofObject:change:context: 方法来获得通知。想要提供自定义的 selector ,不行;想要传一个 block ,门都没有。而且你还要处理父类的情况 - 父类同样监听同一个对象的同一个属性。但有时候,你不知道父类是不是对这个消息有兴趣。虽然 context 这个参数就是干这个的,也可以解决这个问题 - 在 -addObserver:forKeyPath:options:context: 传进去一个父类不知道的 context。但总觉得框在这个 API 的设计下,代码写的很别扭。至少至少,也应该支持 block 吧。

    所以在实际开发中 KVO 使用的情景并不多,更多时候还是用 Delegate 或 NotificationCenter。
    后续作者会在进阶篇讲解自定义KVO的实现,敬请关注!

    相关文章

      网友评论

          本文标题:理解KVO

          本文链接:https://www.haomeiwen.com/subject/wtxbnxtx.html