美文网首页iOS技术图谱
iOS技术图谱之KVO

iOS技术图谱之KVO

作者: iOS大蝠 | 来源:发表于2019-11-20 16:07 被阅读0次

    KVO 是 Cocoa 框架提供的一种键-值观察的机制,关于 KVO 的用法可以参考苹果官方文档 Key-Value Observing Programming Guide,本文旨在帮助读者更好的理解 KVO 的原理。

    探索 KVO

    关于 KVO 的实现细节,苹果似乎不愿意过多暴露。在 Key-Value Observing Implementation Details 中简单描述到:KVO 是基于一种称为 isa-swizzling 的技术实现,当在注册 observer 的时候,会生成一个中间类(继承自原始类),被观察对象的 isa 指针实际指向这个中间类。

    中间类

    这个中间类到底是什么?借助于 runtimelldb 技术,可以更好的剖析中间类。

    property

    @interface GSCObject : NSObject
    
    @property (nonatomic, strong) NSString *name;
    
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            GSCObject *object = [[GSCObject alloc] init];
            GSCObserver *observer = [[GSCObserver alloc] init];
            [object addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
            NSLog(@"%p", object_getClass(object));
            object.name = @"gsc";
    
        }
        return 0;
    }
    
    

    object.name = @"gsc"; 行加入断点,执行以下命令后的结果:

    0x101d0cd30
    (lldb) po object->isa
    NSKVONotifying_GSCObject
    
    (lldb) p (class_data_bits_t *) 0x101d0cd50 //(在类指针上加 32 的 offset 打印 class_data_bits_t 指针)
    (class_data_bits_t *) $1 = 0x0000000101d0cd50
    (lldb) p $1->data()
    (class_rw_t *) $2 = 0x0000000101d0ce50
    (lldb) p $2.methods
    (method_array_t) $3 = {
      list_array_tt<method_t, method_list_t> = {
         = {
          list = 0x0000000101e126a1
          arrayAndFlag = 4326500001
        }
      }
    }
      Fix-it applied, fixed expression was: 
        $2->methods
    (lldb) p $3.beginCategoryMethodLists()[0][0]
    (method_list_t) $4 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "setName:"
          types = 0x0000000100001f82 "v24@0:8@16"
          imp = 0x00007fff35fd7b6b (Foundation`_NSSetObjectValueAndNotify)
        }
      }
    }
    (lldb) p $3.beginCategoryMethodLists()[1][0]
    (method_list_t) $5 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "class"
          types = 0x00000001003b9fb2 "#16@0:8"
          imp = 0x00007fff35fd77cf (Foundation`NSKVOClass)
        }
      }
    }
    (lldb) p $3.beginCategoryMethodLists()[2][0]
    (method_list_t) $6 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "dealloc"
          types = 0x00000001003b9f5c "v16@0:8"
          imp = 0x00007fff3609d8fb (Foundation`NSKVODeallocate)
        }
      }
    }
    (lldb) p $3.beginCategoryMethodLists()[3][0]
    (method_list_t) $7 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "_isKVOA"
          types = 0x00007fff3636b11b "c16@0:8"
          imp = 0x00007fff361637e0 (Foundation`NSKVOIsAutonotifying)
        }
      }
    }
    (lldb) p (objc_class *)0x101d0cd38  // (在类指针上加 8 的 offset 打印 superclass 指针)
    (objc_class *) $8 = 0x0000000101d0cd38
    (lldb) p *$8
    (objc_class) $9 = {
      isa = GSCObject
    }
    
    

    中间类的名称是:NSKVONotifying_GSCObject,父类是:GSCObject。并且自动创建了 setName:classdealloc_isKVOA 四个方法,每个方法对应的实现 imp 都是在 Foundation 框架中。

    那么当对象变量改变的时候是如何触发 KVO 通知的呢?

    在通过 『. 语法』对 property 赋值时(property 会默认生成 setter 和 getter),最终都会执行到 setter 方法。中间类重写 setter 方法后的实现为 _NSSetObjectValueAndNotify 的 imp ,对 _NSSetObjectValueAndNotify 进行断点后可以发现:

    image

    主要的方法调用栈如上图所示。

    image

    _NSSetObjectValueAndNotify 内部实现中可以发现,会先调用 willChangeValueForKey: 然后调用原始类的 setter ,接着会调用 didChangeValueForKey:

    image

    在方法调用栈的最后方法 NSKVONotify 中可以看到 KVO 的通知方法 observeValueForKeyPath:ofObject:change:context:

    ivar

    @interface GSCObject : NSObject
    {
        NSString *_name;
    }
    
    @end
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            GSCObject *object = [[GSCObject alloc] init];
            GSCObserver *observer = [[GSCObserver alloc] init];
            [object addObserver:observer forKeyPath:@"_name" options:NSKeyValueObservingOptionNew context:nil];
            NSLog(@"%p", object_getClass(object));
            [object setValue:@"gsc" forKey:@"_name"];
    
        }
        return 0;
    }
    
    

    对于 ivar 来说,原始类并不会自动生成 setter 方法,执行命令后的结果为:

    (lldb) p $2.beginCategoryMethodLists()[0][0]
    (method_list_t) $3 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "class"
          types = 0x00000001003b8fb2 "#16@0:8"
          imp = 0x00007fff35fd77cf (Foundation`NSKVOClass)
        }
      }
    }
    (lldb) p $2.beginCategoryMethodLists()[1][0]
    (method_list_t) $4 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "dealloc"
          types = 0x00000001003b8f5c "v16@0:8"
          imp = 0x00007fff3609d8fb (Foundation`NSKVODeallocate)
        }
      }
    }
    (lldb) p $2.beginCategoryMethodLists()[2][0]
    (method_list_t) $5 = {
      entsize_list_tt<method_t, method_list_t, 3> = {
        entsizeAndFlags = 26
        count = 1
        first = {
          name = "_isKVOA"
          types = 0x00007fff3636b11b "c16@0:8"
          imp = 0x00007fff361637e0 (Foundation`NSKVOIsAutonotifying)
        }
      }
    }
    
    

    中间类并没有创建 setter,那么是如何触发 KVO 通知的呢?

    _name 设置 watchpoint:

    (lldb) watchpoint set variable object->_name
    
    

    执行断点后

    image

    KVC 赋值底层会调用 _NSSetUsingKeyValueSetter,在该实现内部会调用 _NSSetValueAndNotifyForKeyInIvar

    image

    _NSSetValueAndNotifyForKeyInIvar 实现中,会先调用 willChangeValueForKey: ,接着对 ivar 进行赋值,然后调用 didChangeValueForKey:

    image

    didChangeValueForKey: 中会调用 NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.15835543126851482145

    image

    NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.15835543126851482145 中会调用 NSKeyValueDidChange

    image

    之后的方法调用栈与 property 一致。

    ivar + setter

    @interface GSCObject : NSObject
    {
        NSString *_name;
    }
    
    @end
    
    
    @implementation GSCObject
    
    - (void)set_name:(NSString *)name
    {
        _name = name;
    }
    
    @end
    
    

    当我们手动添加 setter 之后会发现,中间类也会重写 setter 。

    image

    与无 setter 的情况对比可以发现:在 _NSSetUsingKeyValueSetter 内会调用 _NSSetObjectValueAndNotify,也就是中间类 setter 的实现。

    总结

    1. 调用- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context; 方法后,会创建一个名为:NSKVONotifying_originalClass 的中间类,父类为 originalClass,并自动创建 classdealloc_isKVOA 三个方法。

    2. 使用 property (默认生成 setter)或手动添加 setter 后,中间类会重写 setter,以 _NSSetObjectValueAndNotify 的实现作为中间类 setter 的实现。

    3. 使用 『. 语法』,会直接调用 setter。

    4. 使用 KVC ,会调用 _NSSetUsingKeyValueSetter。如果有重写过 setter,_NSSetUsingKeyValueSetter 内部会调用 setter 的实现 _NSSetObjectValueAndNotify, 如果没有,则会调用 _NSSetValueAndNotifyForKeyInIvar

    对于变量赋值的场景可以分为以下几种:

    1. 有 setter + . 语法

      @property (nonatomic, strong) NSString *name;
      
      object.name = @"gsc";
      
      

      会触发 KVO 通知,主要方法调用栈:

    image
    1. 有 setter + KVC(自动生成 setter 和手动添加 setter 两种)

      @property (nonatomic, strong) NSString *name;
      
      [object setValue:@"gsc" forKey:@"_name"];
      
      
      {
          NSString *_name;
      }
      
      - (void)set_name:(NSString *)name
      {
          _name = name;
      }
      
      [object setValue:@"gsc" forKey:@"_name"];
      
      

      两种情况都会触发 KVO 通知,主要方法调用栈:

    image
    1. 无 setter + KVC

      {
          NSString *_name;
      }
      
      [object setValue:@"gsc" forKey:@"_name"];
      
      

      会触发 KVO 通知,主要方法调用栈:

    image
    1. 直接赋值 ivar

      {
          @public
          NSString *_name;
      }
      
      object->_name = @"gsc";
      
      
    image

    设置 watchpoint 可以发现,这种情况直接调用 objc_storeStrong 并不会触发 KVO 通知。

    KVO 的内部实现是极其复杂的,并且 KVO 的实现离不开 KVC。 Github 上有人根据 Foundation.framework 汇编反写出了 KVC、KVO 的『实现』 :DIS_KVC_KVO,感兴趣的可以去研读下。

    相关文章

      网友评论

        本文标题:iOS技术图谱之KVO

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