iOS技术图谱

iOS技术图谱之KVO

2019-11-20  本文已影响0人  iOS大蝠

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,感兴趣的可以去研读下。

上一篇下一篇

猜你喜欢

热点阅读