前言
KVO作为iOS一个设计模式,监听对象属性变化。通过属性变化来做出一些处理。那么KVO底层原理是什么?相信大家前期都不怎么关注过,知其然知其所以然,所以我也研究讲一下KVO底层实现原理。
思考
- KVO 底层实现是什么?
- 如何手动触发 KVO?
- 修改成员变量的值会触发 KVO 吗?
- KVC 赋值会触发 KVO 吗?
把这几个问题都整明白,KVO掌握的也就差不多了。
KVO的底层实现
首先创建一个对象Person,在Person.h声明一个属性
-
创建两个对象,一个对象KVO监听,一个对象不监听,查看一下他们的监听前后地址
Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; NSLog(@"KVO监听前:p1:%@ p2:%@", p1, p2); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld| NSKeyValueObservingOptionNew context:nil]; NSLog(@"KVO监听后:p1:%@ p2:%@",p1, p2);
打印的结果:
KVO监听前:p1:<Person: 0x600003810380> p2:<Person: 0x6000038103a0>
KVO监听后:p1:<Person: 0x600003810380> p2:<Person: 0x6000038103a0>
打印的结果说明监听前后地址没啥变化。
-
那就查看一下监听前后对象的类对象有没有变化,使用object_getClass来获取类对象,只有引入#import <objc/runtime.h>才能使用object_getClass方法
Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; id class1 = object_getClass(p1); id class2 = object_getClass(p2); NSLog(@"KVO监听前:p1:%@ p2:%@",class1, class2); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; class1 = object_getClass(p1); class2 = object_getClass(p2); NSLog(@"KVO监听后:p1:%@ p2:%@",class1, class2);
打印结果是:
KVO监听前:p1:Person p2:Person KVO监听后:p1:NSKVONotifying_Person p2:Person
发现监听后的p1是的类对象变成NSKVONotifying_Person,怎么监听后Person变成NSKVONotifying_Person。NSKVONotifying_Person是什么东东?
-
进一步窥探 KVO 添加前后的变化,打印 setName 方法实现IMP指针有没有发生改变,我们知道同一个方法的实现,IMP地址是不变的.
Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; //setName的IMP IMP imp1 = [p1 methodForSelector:@selector(setName:)]; IMP imp2 = [p2 methodForSelector:@selector(setName:)]; NSLog(@"KVO监听前:imp1:%p imp2:%p",imp1, imp2); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; //setName的IMP imp1 = [p1 methodForSelector:@selector(setName:)]; imp2 = [p2 methodForSelector:@selector(setName:)]; NSLog(@"KVO监听后:imp1:%p imp2:%p",imp1, imp2);
打印结果:
KVO监听前:imp1:0x10fa01640 imp2:0x10fa01640 KVO监听后:imp1:0x10fd5a63a imp2:0x10fa01640
从打印结果来看连 setName方法都不一样了, 为了一探究竟 ,对上边的 NSKVONotifying_Person 和 添加 KVO 之后的 imp 指针进行进一步研究.首先 在 lldb 上输入 imp1和 imp2
(lldb) po imp1 (Foundation`_NSSetObjectValueAndNotify) (lldb) po imp2 (KVO底层原理`-[Person setName:] at Person.h:13)
发生了 imp1 方法实现在 Foundation 框架里的 _NSSetObjectValueAndNotify 函数中 ,而 imp2 则调用了 Person setName 方法.
添加了 KVO 之后 p1 修改 name 值之后 不再调用 Person 的 setName方法 ,而 p2没有添加 kvo 监听 依然正常调用 setName:方法 ,由此可以得出 p1 添加完 KVO 监听后 系统修改了默认方法实现,那么既然没有调用 setName: 方法 为什么 p1.name 的值也发生了改变? -
NSKVONotifying_Person和 Person 之间的关系,打印一下各自的父类
Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; //获取p1 p2类型对象 id class1 = object_getClass(p1); id class2 = object_getClass(p2); id superClass1 = class_getSuperclass(class1); id superClass2 = class_getSuperclass(class2); NSLog(@"KVO监听前:superclass1:%@ superclass2:%@",superClass1, superClass2); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; class1 = object_getClass(p1); class2 = object_getClass(p2); superClass1 = class_getSuperclass(class1); superClass2 = class_getSuperclass(class2); NSLog(@"KVO监听后:superclass1:%@ superclass2:%@",superClass1, superClass2);
打印结果:
KVO监听前:superclass1:NSObject superclass2:NSObject KVO监听后:superclass1:Person superclass2:NSObject
通过打印 NSKVONotifying_Person 的 superclass 和 Person 的 superclass 可以得出, NSKVONotifying_Person是一个 Person 子类,那么为什么苹果会动态创建这么一个 子类呢? NSKVONotifying_Person 这个子类 跟 Person 内部有哪些不同呢 ?
-
输出下 Person 和 NSKVONotifying_Person 内部的方法列表 和 属性列表 ,看看NSKVONotifying_Person 子类都添加了哪些方法和属性.
Person *p1 = [[Person alloc] init]; Person *p2 = [[Person alloc] init]; //获取p1 p2类型对象 id class1 = object_getClass(p1); id class2 = object_getClass(p2); [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:nil]; class1 = object_getClass(p1); class2 = object_getClass(p2); NSString *methodList1 = [self printPersonMethods:class1]; NSString *methodList2 = [self printPersonMethods:class2]; NSLog(@"KVO监听后:methodlist1:%@ \n methodlist2:%@",methodList1, methodList2); - (NSString *) printPersonMethods:(id)obj { unsigned int count = 0; Method *methods = class_copyMethodList([obj class],&count); NSMutableString *methodList = [NSMutableString string]; [methodList appendString:@"[\n"]; for (int i = 0; i<count; i++) { Method method = methods[I]; SEL sel = method_getName(method); [methodList appendFormat:@"%@",NSStringFromSelector(sel)]; [methodList appendString:@"\n"]; } [methodList appendFormat:@"]"]; free(methods); return methodList; }
打印结果:
KVO监听后:methodlist1:[
setName:
class
dealloc
_isKVOA
]
methodlist2:[
.cxx_destruct
name
setName:
]
- 发现监听后的methodlist1方法内部也有setName:
- 重写了class 和 dealloc 方法,多了个_isKVOA方法。
- KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类。
- 重写了 setName 方法 ,那么 setName 内部一定是做了注册通知的事情,当值改变才会触发 observeValueForKeyPath 监听方法.
继续探究 NSKVONotifying_Person 子类 重写 setName 都做了什么?
- 前面打印了监听后p1的setName的IMP方法变成(Foundation`_NSSetObjectValueAndNotify)
- setName方法内部应该是调用了 Foundation 的_NSSetObjectValueAndNotify 函数
- 在 _NSSetObjectValueAndNotify实现触发监听
- 首先会调用 willChangeValueForKey
- 其次给 name 属性赋值
- 然后调用 didChangeValueForKey方法,该内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:)
- 最后调用 observer 的 observeValueForKeyPath 去告诉监听器属性值发生了改变 .
由于源码无法查看,大概设想一下NSKVONotifying_Person的实现代码:
- (void)setName:(NSString *)name {
_NSSetObjectValueAndNotify()
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
//observer观察者
[observer observeValueForKeyPath:name];
}
void _NSSetObjectValueAndNotify() {
//触发监听
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];]
}
所以我们依然可以通过重写Person 的 willChangeValueForKey 和 didChangeValueForKey 验证我们的猜想.打断点调试一下
//每次属性改变,第二步
- (void)setName:(NSString *)name {
_name = [name copy];
}
//每次属性改变,先走这一步
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
}
//每次属性改变,第三步
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
}
//这个方法不是在Person,是在哪个类监听的p1
//每次属性值改变,第四步会走observeValueForKeyPath
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"new -- %@",change[NSKeyValueChangeNewKey]);
}
- 监听时候Runtime动态的创建Person的子类NSKVONotifying_Person
- 重写了setName方法,在该方法里面调用_NSSetObjectValueAndNotify()方法
- 重写class方法返回的还是Person类
- 重写了dealloc 和 class 方法 是为了做一些KVO 释放内存和隐藏外界对于NSKVONotifying_Person 子类的存在
- 这就是我们打印[p1 class] [p2 class]都显示Person,让我们误以为 Person 没有发生变化 。
问题解答
-
如何手动的触发KVO?
关闭该属性的自动触发
实现willChangeValueForKey didChangeValueForKey成对出现才能手动触发KVO
一个被观察属性发生改变之前,willChangeValueForKey: 一定会被调用,这就会记录旧的值
-
当执行didChangeValueForKey这个方法就会触发监听,这个会记录新的值
- (void)setName:(NSString *)name { if ([_name isEqualToString:name]) return; //手动触发 关闭自动监听之后,如果不写下面三行代码是不会触发监听的, //要是打开自动监听的话,willChangeValueForKey didChangeValueForKey可以不用不写的,系统会自动加上 [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; } + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { //对name属性关闭自动监听 if ([key isEqualToString:@"name"]) { return NO; } else { //对其他属性没影响 return [super automaticallyNotifiesObserversForKey:key]; } }
-
修改成员变量的值会触发 KVO 吗?
- 修改成员变量不会触发,因为不会触发setter方法,因此不会触发KVO相关的代码
-
KVC 赋值会出发 KVO 吗?
- 会触发KVO,KVC 对属性赋值时候 是会在这个类里边去查找setName、_name、_isName、name、isName等方法的
- 最终会调用属性的setter方法,所以KVO还是会被触发的.
- KVO在调用存取方法之前总是调用 willChangeValueForKey ,之后总是调用 didChangeValueForkey: 怎么做到的呢?答案是通过 isa 混写(isa-swizzling)
使用KVO注意问题
- 如果同一个属性在不同的类(比如父类)均设置了KVO,那么可通过定义不同的context(类名)来区分,并且相应移除。
- 对同一个KVO属性多次移除,会导致程序crash。
- 使用类别(category)向一个已存在的类添加一个属性,可以用KVO观察其变化。
- 我们可以分别在父类以及本类中定义各自的context字符串,比如在本类中定义context为@"ThisIsMyKVOContextNotSuper";然后在dealloc中remove observer时指定移除的自身添加的observer。这样iOS就能知道移除的是自己的kvo,而不是父类中的kvo,避免二次remove造成crash。