KVO

作者: 张_何 | 来源:发表于2019-12-11 17:53 被阅读0次
    • KVO 全程 Key-Value Observing,俗称键值监听,可以用于监听某个对象属性值的改变,官方文档如下:

    意思是KVO是基于isa-swizzling实现的,在注册观察者的时候,会修改观察对象的isa指向。不要使用isa指针来判断类的关系,而应该使用class方法。

    • 当监听对象的属性变化时会调用监听者的observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 方法
    • 当监听者销毁时需要被监听对象调用removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath方法将监听者移除
    • 并不是所有的属性都可以监听,因为 KVO的本质是重写了 setter 方法,所以只有有 setter 方法的属性才能使用 KVO 监听,例如 readonly 属性就就不能用使用 KVO 监听

    KVO 的本质

    • 当对象调用addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context 给某个属性添加观察者时,系统会利用 runtime 动态的创建一个全新的子类(NSKVONotifying_XXX),该类继承之前的类.在添加监听者以后该实例对象变为了NSKVONotifying_XXX类型的实例对象,其 isa 指针指向NSKVONotifying_XXX的类对象
    • NSKVONotifying_XXX会重写之前类的 setter 方法,当属性通过 setter 方法赋值时,重写后的 setter 方法内部会触发 KVO
    • 重写后的 setter 方法内部会调用Foundation框架内部的 _NSSet***AndNotifying函数,在_NSSet***AndNotifying函数内部会依次调用willChangeValueForKey:supersettter 方法、didChangeValueForKey:.在didChangeValueForKey:方法内部会调用observeobserveValueForKeyPath: ofObject:change: context:从而触发 KVO
    NSKVONotifying_XXX窥探
    setter 方法窥探
        NSLog(@"监听前:%p", [self.p methodForSelector:@selector(setAge:)]);//这个方法返回一个方法实现的地址, 通过终端 p (IMP)方法地址 可以知道调用方法的信息
        [self.p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:NULL];
        NSLog(@"监听后:%p", [self.p methodForSelector:@selector(setAge:)]);
    

    我们在对象 p 的 age 属性添加监听对象前后分别打印 setAge:方法实现的地址指针,然后通过断点,在终端中打印该方法的具体实现

    监听前:0x10762e240
    监听后:0x7fff2564db08
    (lldb) p (IMP)0x10762e240
    (IMP) $0 = 0x000000010762e240 (ObjcCode`-[KVOPerson setAge:] at KVOKVCVC.m:16)
    (lldb) p (IMP)0x7fff2564db08
    (IMP) $1 = 0x00007fff2564db08 (Foundation`_NSSetLongLongValueAndNotify)
    

    通过打印内容看到监听后 setAge 方法内部调用的是Foundation内部的_NSSetLongLongValueAndNotify函数

    内部结构窥探

    首先实现一个方法来获取一个类内部的所有方法

    -(void)printMethodNamesOfClass:(Class)cls {
        unsigned int count;
        Method *methodList = class_copyMethodList(cls, &count);
        NSMutableString *methodNames = [NSMutableString new];
        for (int i = 0; i < count; i++) {
            Method method = methodList[i];
            NSString *methodName = NSStringFromSelector(method_getName(method));
            [methodNames appendString:methodName];
            [methodNames appendString:@", "];
        }
        free(methodList);
        NSLog(@"%@",methodNames);
    

    调用该方法[self printMethodNamesOfClass:object_getClass(self.p)];获得结果setAge:, class, dealloc, _isKVOA,,通过结果我们得出NSKVONotifying_XXX类内部不仅重写的setAge方法,还重写的class dealloc _isKVOA这三个方法.
    重写 class 方法可能是不想让外界知道重新生成了子类
    重写dealloc方法,做一些实例销毁时的清理工作
    添加了_isKVOA方法,来说明自己是kvo类


    • 当我们调用- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
      方法时传递的 context 值有什么用?
      首先我们知道在代码中重复移除KVO 是会 crash 的,那么考虑下面的这个场景:就是如果子类和父类同时对某个"KeyPath"添加了观察时,这个时候如果我们移除的时候如果没有 context,那么就会造成 remove 两次,这样就会导致程序 crash,如果我们在添加KVO 的时候传上 不同context,在移除的时候也传上 对应的context,这样就会有针对性的移除,就不会存在子类移除后,父类又移除一次,其文档也建议我们在使用的时候传入 context.官方注释如下:
      Register or deregister as an observer of the value at a key path relative to the receiver. The options determine what is included in observer notifications and when they're sent, as described above, and the context is passed in observer notifications as described above. You should use -removeObserver:forKeyPath:context: instead of -removeObserver:forKeyPath: whenever possible because it allows you to more precisely specify your intent. When the same observer is registered for the same key path multiple times, but with different context pointers each time, -removeObserver:forKeyPath: has to guess at the context pointer when deciding what exactly to remove, and it can guess wrong.
      意思是:注册或注销一个相对于接收方的键路径上的值的观察者。这些选项决定了观察者通知中包含什么内容,以及它们何时被发送(如上所述),上下文在观察者通知中传递(如上所述)。你应该尽可能使用-removeObserver:forKeyPath:context:而不是-removeObserver:forKeyPath:因为它允许你更精确地指定你的意图。当同一个观察者多次为同一个键路径注册,但每次都使用不同的上下文指针时,-removeObserver:forKeyPath:不得不猜测上下文指针来决定到底要删除什么,它可能猜错了。

    • 什么情况下会使用 KVO监听失败?
      代码中已经有了一个NSKVONotifying_XXX类的时候,会导致系统动态创建NSKVONotifying_XXX类失败,从而导致 KVO监听失败.

    • 怎么手动触发 KVO?
      首先调用willChangeValueForKey:方法,然后给属性设值,然后再调用didChangeValueForKey:.这里我们要注意,虽然observeValueForKeyPath:ofObject:change:context:是在didChangeValueForKey:方法中调用的,但是如果没有先调用willChangeValueForKey:方法的话,didChangeValueForKey:内部是不会调用该方法的.

    • 直接修改成员变量的值会触发 KVO吗?
      不会,因为没有调用 setter 方法.如果是通过 KVC 赋值的话,会触发 KVO.

    • 使用 KVO 时需要注意哪些事项
      1、使用完需要移除,注意重复移除会崩溃
      2、当子类中使用 KVO时,在调用回到方法是判断,如果不是自身监听内容的改变造成的回调,需要调用 super 方法,将该事件传递给父类,因为有可能是父类监听的,如果不使用 super 往上传,会导致事件的的中断.

    相关文章

      网友评论

          本文标题:KVO

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