KVO的使用和底层实现原理
1.什么是KVO
KVO是Key-Value-Observing的缩写,通过KVO这种机制对象可以通过它得到其他对象的某个属性的变更通知。这种机制在MVC模式下显得更为重要,KVO可以让视图对象经过控制器观察模型对象的变更从而做出更新等操作。
2.KVO的使用
/**
* 1. self.person:要监听的对象
* 2. 参数说明:
* @param addObserver 观察者,负责处理监听事件的对象
* @param forKeyPath 要监听的属性
* @param options 观察的选项(观察新、旧值,也可以都观察)
* @param context 上下文,用于传递数据,可以利用上下文区分不同的监听
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
/**
* 当监控的某个属性的值改变了就会调用
*
* @param keyPath 监听的属性名
* @param object 属性所属的对象
* @param change 属性的修改情况(属性原来的值`oldValue`、属性最新的值`newValue`)
* @param context 传递的上下文数据,与监听的时候传递的一致,可以利用上下文区分不同的监听
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
NSLog(@"%@对象的%@属性改变了:%@", object, keyPath, change);
}
/**
* 移除KVO监听
*/
-(void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
}
3.KVO的使用场景
KVO主要用于用户界面交互,当多个View共同使用了同一个实体,当这个实体中的某个属性改变时,如果需要更新多个界面,KVO的作用就发挥出来了。
4.KVO的底层实现原理
-
KVO是基于
runtime
机制实现的 -
当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的
setter
方法。派生类在被重写的setter
方法内实现真正的通知机制 -
如果原类为Person,那么生成的派生类名为
NSKVONotifying_Person
-
每个类对象中都有一个
isa
指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa
指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter
方法 -
键值观察通知依赖于NSObject 的两个方法:
willChangeValueForKey:
和didChangevlueForKey:
;在一个被观察属性发生改变之前,willChangeValueForKey:
一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:
会被调用,继而observeValueForKey:ofObject:change:context:
也会被调用。 -
补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类
5.KVO的缺陷
-
API 设计缺陷。NSNotification 的 API 允许我们传入
selector( -addObserver: selector: name: object:
),这样收到通知的时候就可以调用到 selector ;相反使用 KVO 的话,我们必须在观察者类当中重写-observeValueForKeyPath:ofObject:change:context:
方法,我们不得不在这个方法里加很多判断逻辑写一坨代码来监听不同的属性。 -
在
-addObserver:forKeyPath:options:context:
里传入 NSString 对象作为keyPath
,编译器是无法检测错误的,如果属性名被更改了,这个观察就没意义了。这个问题可以通过NSStringFromSelector
稍稍改善。另外,传给context
的参数大部分情况下都是 nil. -
如果父类和子类都监听了同一个对象的同一个属性,很容易出现父类中
remove
了一次,子类又remove
了一次的情况,应用程序第二次remove
就会抛出异常然后 crash. 解决方案见 KVO Considered Harmful . -
KVO 可能会导致死循环。死循环发生的场景是:你在
-observeValueForKeyPath
方法里边设置了属性,而这个属性刚好被自己监听。 -
某些情况下使用 KVO 监听属性会失效。比如说被监听属性的 setter 被 hook 了;还有一种情况是,对 weak 属性监听,如果 weak 实例变量指向的对象被释放了,weak 修饰的实例变量会自动设为 nil,KVO 也就失效了。