iOS技术图谱之KVO
KVO 是 Cocoa 框架提供的一种键-值观察的机制,关于 KVO 的用法可以参考苹果官方文档 Key-Value Observing Programming Guide,本文旨在帮助读者更好的理解 KVO 的原理。
探索 KVO
关于 KVO 的实现细节,苹果似乎不愿意过多暴露。在 Key-Value Observing Implementation Details 中简单描述到:KVO 是基于一种称为 isa-swizzling
的技术实现,当在注册 observer 的时候,会生成一个中间类(继承自原始类),被观察对象的 isa 指针实际指向这个中间类。
中间类
这个中间类到底是什么?借助于 runtime 和 lldb 技术,可以更好的剖析中间类。
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:
、class
、dealloc
、_isKVOA
四个方法,每个方法对应的实现 imp 都是在 Foundation 框架中。
那么当对象变量改变的时候是如何触发 KVO 通知的呢?
在通过 『. 语法』对 property 赋值时(property 会默认生成 setter 和 getter),最终都会执行到 setter 方法。中间类重写 setter 方法后的实现为 _NSSetObjectValueAndNotify
的 imp ,对 _NSSetObjectValueAndNotify
进行断点后可以发现:
主要的方法调用栈如上图所示。
image在 _NSSetObjectValueAndNotify
内部实现中可以发现,会先调用 willChangeValueForKey:
然后调用原始类的 setter ,接着会调用 didChangeValueForKey:
。
在方法调用栈的最后方法 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
执行断点后
imageKVC 赋值底层会调用 _NSSetUsingKeyValueSetter
,在该实现内部会调用 _NSSetValueAndNotifyForKeyInIvar
。
在 _NSSetValueAndNotifyForKeyInIvar
实现中,会先调用 willChangeValueForKey:
,接着对 ivar
进行赋值,然后调用 didChangeValueForKey:
。
在 didChangeValueForKey:
中会调用 NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.15835543126851482145
。
在 NSKeyValueDidChangeWithPerThreadPendingNotifications.llvm.15835543126851482145
中会调用 NSKeyValueDidChange
。
之后的方法调用栈与 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 的实现。
总结
-
调用
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
方法后,会创建一个名为:NSKVONotifying_originalClass
的中间类,父类为originalClass
,并自动创建class
、dealloc
、_isKVOA
三个方法。 -
使用 property (默认生成 setter)或手动添加 setter 后,中间类会重写 setter,以
_NSSetObjectValueAndNotify
的实现作为中间类 setter 的实现。 -
使用 『. 语法』,会直接调用 setter。
-
使用 KVC ,会调用
_NSSetUsingKeyValueSetter
。如果有重写过 setter,_NSSetUsingKeyValueSetter
内部会调用 setter 的实现_NSSetObjectValueAndNotify
, 如果没有,则会调用_NSSetValueAndNotifyForKeyInIvar
。
对于变量赋值的场景可以分为以下几种:
-
有 setter + . 语法
@property (nonatomic, strong) NSString *name; object.name = @"gsc";
会触发 KVO 通知,主要方法调用栈:
-
有 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 通知,主要方法调用栈:
-
无 setter + KVC
{ NSString *_name; } [object setValue:@"gsc" forKey:@"_name"];
会触发 KVO 通知,主要方法调用栈:
-
直接赋值 ivar
{ @public NSString *_name; } object->_name = @"gsc";
设置 watchpoint 可以发现,这种情况直接调用 objc_storeStrong
并不会触发 KVO 通知。
KVO 的内部实现是极其复杂的,并且 KVO 的实现离不开 KVC。 Github 上有人根据 Foundation.framework 汇编反写出了 KVC、KVO 的『实现』 :DIS_KVC_KVO
,感兴趣的可以去研读下。