阅读 209

KVO原理探索

  • KVO简介

  • 基本用法

    1. addObserver:forKeyPath:options:context:
      注册观察者对象以接收相对于接收此消息的对象的密钥路径的KVO通知。
      - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
      参数说明:
      1. observer:注册KVO通知的对象。观察者必须实现键值观察方法observeValueForKeyPath:ofObject:change:context:
      2. keyPath:要观察的属性相对于接收此消息的对象的关键路径。该值不能为nil。
      3. options:主要决定了observeValueForKeyPath:ofObject:change:context:`回调方法中change返回的数据
        • NSKeyValueObservingOptionNew 返回新值
        • NSKeyValueObservingOptionOld 返回旧值
        • NSKeyValueObservingOptionInitial在观察者注册方法的时候和value改变的时候都会通知发送给观察者(其他两个类型在注册的时候不会通知给观察者,只有value改变的时候才会通知)
      4. context: addObserver:forKeyPath:options:context:方法中的上下文context指针包含任意数据,这些数据将在相应的更改通知中传递回观察者。可以通过指定contextNULL,从而依靠keyPath即键路径字符串传来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因也观察到相同的键路径而导致问题。所以可以为每个观察到的keyPath创建一个不同的context,从而完全不需要进行字符串比较,从而可以更有效地进行通知解析 主要用处在于,可能在一个viewController需要监听多个对象中的一个属性,而在observeValueForKeyPath:ofObject:change:context:回调方法中KeyPath返回的都是监听的key值此时就没办法区分到底是哪个类中的简直改变,此时就可以使用context来区分
        用法:
          //定义context
         static void *PersonNickContext = &PersonNickContext;
         static void *PersonNameContext = &PersonNameContext;
         //注册观察者
         [self.person addObserver:self forKeyPath:@"nick" options:NSKeyValueObservingOptionNew context:PersonNickContext];
         [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
        
         //KVO回调
         - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
             if (context == PersonNickContext) {
                 NSLog(@"%@",change);
             }else if (context == PersonNameContext){
                 NSLog(@"%@",change);
             }
         }
        复制代码
    2. observeValueForKeyPath:ofObject:change:context:
      监听回调当监听的键值对应的value改变的时候会执行,如果options设置成NSKeyValueObservingOptionInitial的时候当注册观察者的时候也会发送一个通知此时该方法也会执行
      参数说明:
      1. keyPath:改变的value对应点额键的路径
      2. object: 键值所在的对象
      3. change: 相对于对象的键路径keyPath上的属性值所做的更改
      4. context:改变的value对应的context值该值在`addObserver:forKeyPath:options:context:方法中设置
    3. removeObserver:forKeyPath:
      移除KVO通知(如果添加观察者的时候没有设置context则条用此方法来移除通知)如果移除了一个没有观察的键值则会崩溃
      注意注册观察者之后观察的类销毁的时候不移除通知的危害:如果被观察的类是个单例类(单例类在内存中是常驻的就算实例化他的类被销毁了对应的单例类还在内存中),观察的类销毁后观察的类就变成一个‘野类’,那么再有value变化的时候就会找不到观察的类从而爆出类似野指针的崩溃,(如果被观察的类不是单例类则不会出现这种情况,大致原因应该是当观察类被释放后,被观察的类相应的也会被释放,所以被观察者和观察者之间所建立的联系相应的也断了就是就是自动移除了观察者,所以value变化的时候就不会在崩溃)
    4. removeObserver:forKeyPath:context:
      移除KVO通知(如果添加观察者的时候设置了context需要调用这个方法来移除通知) 如果移除了一个没有观察的键值则会崩溃
    5. willChangeValueForKey:
      被观察的键值value将要改变,在手动实现键值观察的时候使用
    6. didChangeValueForKey:
      被观察的键值value已经改变,在手动实现键值观察的时候使用该方法和willChangeValueForKey: 方法是成对出现的
    7. automaticallyNotifiesObserversForKey:
      是否自动实现键值观察,模式是YES,如果需要手动实现键值观察则需要搭配willChangeValueForKey:didChangeValueForKey:方法一起使用具体使用方法如下
    8. keyPathsForValuesAffectingValueForKey:
      为属性的值返回一组键路径,这些属性的值会影响指定键的值。
      如果需要监听一个需要多个属性组合的一个值(例如下载进度,属性中含有文件总大小和当前大小,所以下载进度就相当于当前大小除以文件总大小)这个时候就可以使用该方法返回一个组合值具体用法如下:
    9. 观察数组注意点
      如果声明了一个可变数组,监听这个可变数组变化,往这个可变数组中添加一个元素一般都是直接add就好了,这样添加是没有问题但是并不会触发observeValueForKeyPath方法,应为KVO是和KVC对应出现的KVC中明确指出数组需要调用mutableArrayValueForKey返回一个数组后再调用add方法这样才能将元素添加到数组中,所以此时才会执行observeValueForKeyPath回调方法
  • KVO原理探索

    • 官方文档探索
      应为没有源码可以探索,所以只能通过官方文档来推测出KVO的底层实现原理 image.png 通过官方文档的介绍大概可以知道KVO的实现是通过isa-swizzling技术实现的,
      1. 大致意思就是isa指针指向维护分发表的对象的类。该分发表实质上包含指向该类实现的方法的指针以及其他数据。
      2. 当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类。
      3. 您永远不应依靠isa指针来确定类成员身份。相反,您应该使用class方法来确定对象实例的类。
      总结一下官方文档大致意思就是当你注册了观察者的时候此时会生成一个中间类,然后当前类的isa不再指向当前类而会指向这个中间类,最后还告诫我们如果想要确定对象的实例类最好用class方法来获取而不是isa(这里也可以被当做一个小面试题)
    • 代码调试探索
      • 分别添加属性和成员变量观察
        image.pngimage.pngimage.png image.png 点击屏幕后发现只打印了nick的监听但是nickName也的确变化了 image.png 说明KVO会监听属性但是不会监听成员变量,属性和成员变量最大的区别就在于属性没有setter方法,得出结论KVO监听的是setter方法
      • 中间类探索
        • 在添加观察者之前和之后分别打断点,然后打印isa查看打印情况,获取isa方法object_getClassNameimage.png发现在没有注册观察者之前isa还是指向当前类的image.png注册观察者之后会指向中间类NSKVONotifying_LGPersonimage.png这里就验证了文档中的话添加观察者会生成中间类然后改变isa的指向,指向中间类
        • 探索中间类和当前类的关系 在添加观察者之前和之后分类打印当前类以及子类打印方法如下:
          #pragma mark - 遍历类以及子类
           - (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);
           }
          复制代码
          结果如下:image.png说明生成的中间类是当前类的子类
        • 探索中间类中的方法 打印一个类中的所有方法代码:
          - (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);
          }
          复制代码
          image.png发现一共有四个方法setNickNameclassdealloc_isKVOA,这些方法到底是重写的方法还是继承的呢,这里有个探索的小技巧,既然是子类,不妨再建一个子类,打印子类中的所有方法,image.png发现并没有打印方法,此时再重写一个setNickName方法在打印image.png发现此时就打印了setNickName方法,则说明继承的方法并不会被打印,重写的方法才会被打印所以说明setNickNameclassdealloc_isKVOA都是重写的方法(通过源码也可以找到根源,加载类的时候打印ro中的方法列表只会出现在类中实现的方法,父类的方法并不会继承下来,方法查找的时候也印证了这一思想,方法查找会先在当前类中查找没有再去父类中查找)
        • 探索移除观察这之后isa的指向以及中间类是否还存在
          移除观察者之后打断点调试image.png 发现移除观察者之后isa又冲中间类指回了当前类
          继续打印当前类的所有子类image.png发现中间类依然存在
  • KVO原理探索总结

    1. 添加观察之后会创建一个中间类(如果是第一次添加观察者则创建,其他直接用已经创建好的中间类)
    2. 添加完成之后会改变isa的指向,指向中间类
    3. 中间类中存在set方法、classdealloc_isKVOA,这些方法都是重写的方法
    4. 移除观察者之后中间类不会被销毁掉,但是isa指针的指向会指回当前类
文章分类
iOS
文章标签