1.KVO实现原理
基本的原理:
当观察某对象 A 时,KVO 机制就会动态创建一个对象A的子类,并为这个新的子类重写了被观察属性 ``keyPath 的 setter 方法。setter 方法随后负责通知观察对象属性的改变状况。
深入剖析:
Apple 使用了 isa 混写(isa-swizzling)来实现 KVO 。当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A 的新类,该类继承自对象A的本类,且 KVO 为 NSKVONotifying_A 重写观察属性的 setter 方法,setter 方法会负责在调用原setter方法之前和之后,通知所有观察对象属性值的更改情况。
(备注: isa 混写(isa-swizzling)isa:is a kind of ; swizzling:混合,搅合;)
NSKVONotifying_A 类剖析:在这个过程,被观察对象的 isa 指针从指向原来的 A 类,被 KVO 机制修改为指向系统新创建的子类 NSKVONotifying_A 类,来实现当前类属性值改变的监听;
所以当我们从应用层面上看来,完全没有意识到有新的类出现,这是系统“隐瞒”了对 KVO 的底层实现过程,让我们误以为还是原来的类。但是此时如果我们创建一个新的名为“NSKVONotifying_A”的类,就会发现系统运行到注册 KVO 的那段代码时程序就崩溃,因为系统在注册监听的时候动态创建了名为 NSKVONotifying_A 的中间类,并指向这个中间类了。
(isa 指针的作用:每个对象都有 isa 指针,指向该对象的类,它告诉 Runtime 系统这个对象的类是什么。所以对象注册为观察者时,isa 指针指向新子类,那么这个被观察的对象就神奇地变成新子类的对象(或实例)了。 )
因而在该对象上对 setter 的调用就会调用已重写的 setter,从而激活键值通知机制。
我猜,这也是 KVO 回调机制,为什么都俗称 KVO 技术为黑魔法的原因之一吧:内部神秘、外观简洁。
子类 setter 方法剖析:KVO 的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的前后分别调用 2 个方法:
被观察属性发生改变之前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变更;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变更;
之后, observeValueForKey:ofObject:change:context: 也会被调用。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)name{
_NSSetObjectValueAndNotify();
}
void _NSSetObjectValueAndNotify {
[self willChangeValueForKey:@"name"];//KVO 在调用存取方法之前总调用
[super setName:name];//调用父类的存取方法
[self didChangeValueForKey:@"name"];//KVO 在调用存取方法之后总调用
}
- (void)didChangeValueForKey:(NSString *)key{
[observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}`
面试答案
KVO基于runtime机制实现- 使用了
isa混写(isa-swizzling),当一个person对象的属性age发生改变时,系统会自动生成一个类,继承自Person,在这个类的setAge方法里面,调用[super setAge:age],[self willChangeValueForKey:@“age"],[self didChangeValueForKey:@“age”],而这些方法内部会主动调用监听者内部的observeValueForKeyPath这个方法
2.如何手动关闭KVO?
被观察的对象复写如下方法 返回NO即可关闭KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}`
**如果关闭后还想触发 KVO 的话 **
修改需要手动调用在变量setter的前后主动调用 willChangeValueForKey:和didChangeValueForKey:
3.通过 KVC 修改属性会触发 KVO 吗?直接修改成员变量呢?
会触发,KVC 内部在修改成员变量的同时是会主动调用 didChangeValueForKey,willChangeValueForKey 去触发 KVO 的
直接修改成员变量不会触发 KVO,因为直接修改成员变量内部并没有处理,只是单纯的赋值
4.哪些情况下使用 KVO 会崩溃,怎么防护崩溃?
添加和移出不是成对出现且存在多线程添加KVO的情况
经常遇到的crash是移出:内存dealloc的时候,或者对象销毁前没有正确移出Observer
如何防护?
- 注意移出对象匹配
- 内存野指针问题,一定要在对象销毁前移出观察者
- 可以使用第三方库
BlockKit添加KVO,BlockKit内部会自动移除Observer避免crash.
5.KVO的优缺点
优点:
- 能够提供一种简单的方法实现两个对象间的同步。例如:
model和view之间同步; - 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
- 能够提供观察的属性的最新值以及先前值;
- 用
key paths来观察属性,因此也可以观察嵌套对象; - 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
- 我们观察的属性必须使用
strings来定义。因此在编译器不会出现警告以及检查; - 对属性重构将导致我们的观察代码不再可用;
- 复杂的
“IF”语句要求对象正在观察多个值,这是因为所有的观察代码通过一个方法来指向; - 当释放观察者时需要移除观察者。
6.KVC原理
一、setValue:forKey:的原理
通过 KVC 赋值时, KVC 内部会通过传进去的 key 去寻找对应的 setter 方法. 按照顺序查找 setKey:、_setKey , 如果找到了方法, 就传递参数, 调用方法.
如果没有找到方法, 就会来到 accessInstanceVariablesDirectly 方法查看它的返回值, 如果返回 NO, 就调用 setValue:forUndefineKey: 并抛出异常 NSUnknownKeyException 崩溃. 如果返回 YES, 就会按照 _key、_isKey、key、isKey 的顺序查找成员变量, 找到成员变量后直接赋值, 如果没有找到, 会也会调用 setValue:forUndefineKey: 并抛出异常 NSUnknownKeyException 崩溃.
accessInstanceVariablesDirectly 方法默认返回 YES. 可以用下面这幅图来表示:
二、valueForKey:的原理
valueForKey 的原理跟 setValue:forKey: 的原理很像, 就不再赘述了, 直接上图:
7.KVO 的使用场景:
- 实现上下拉刷新控件
content offset webView混合排版content size- 监听模型属性实时更新
UI