概念
KVO —— key value observer
俗称“键值监听”,可以用于监听某个对象属性值的改变。Apple
使用了isa
混写(isa-swizzling
)来实现KVO
监听对象属性变化
的一种手段,可以用在开源框架,让代码解耦。例如:上拉、下拉刷新控件
底层实现
KVC
和 KVO
都属于 键值编程
而且底层实现机制都是isa-swizzing
就是类型混合指针机制
- 当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
- 在派生类B中重写类A的
setter
方法,B类在被重写的setter方法
中实现通知机制。 - 类B会重写
class方法
,将自己伪装成类A,重写dealloc方法
释放资源。 - 系统将所有指向类A对象的isa指针指向类B的对象。
注意:
isa
指针的值并不一定反映实例的实际类,不能依靠isa指针来确定对象是否是一个类的成员,应该使用class
方法来确定对象实例的类。KVO
依赖于强大的Runtime
机制我们并不需要向被观察者
添加额外的代码,就能在属性变化的时候得到通知。查看
_NSSet*AndNotify
的存在_NSSet*ValueAndNotify
的内部实现调用willChangeValueForKey:
- 调用原来的setter实现
- 调用didChangeValueForKey:
- didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法
获取派生类的方法列表
#import <objc/runtime.h>
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[i];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
打印出结果如下:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,
优点:
- 能够提供一种简单的方法实现两个对象间的同步。例如:
model
和view
之间同步; - 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
- 能够提供观察的属性的最新值以及先前值;
- 用
key paths
来观察属性,因此也可以观察嵌套对象; - 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察
缺点:
- 观察的属性必须使用
strings
来定义。因此在编译器不会出现警告以及检查; - 对属性重构将导致我们的观察代码不再可用;
- 复杂的
if
语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向; - 当释放观察者时不需要移除观察者。
代码演练
- 添加观察
// 添加键值观察
/**
1. 调用对象:要监听的对象
2. 参数
1> 观察者,负责处理监听事件的对象
2> 观察的属性
3> 观察的选项
4> 上下文
*/
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
- 监听方法
// NSObject 分类方法,意味着所有的 NSObject 都可以实现这个方法!
// 跟协议的方法很像,分类方法又可以称为“隐式代理”!不提倡用,但是要知道概念!
// 所有的 kvo 监听到事件,都会调用此方法
/**
1. 观察的属性
2. 观察的对象
3. change 属性变化字典(新/旧)
4. 上下文,与监听的时候传递的一致
可以利用上下文区分不同的监听!
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"睡会 %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
NSLog(@"%@ %@ %@ %@", keyPath, object, change, context);
}
常见面试题
iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
- 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
willChangeValueForKey:
- 父类原来的
setter
didChangeValueForKey:
didChangeValueForKey
内部会触发监听器(Oberser
)的监听方法(observeValueForKeyPath:ofObject:change:context:
)
如何手动触发KVO?
- 手动调用
willChangeValueForKey:
和didChangeValueForKey:
直接修改成员变量会触发KVO么?
-
NSNotification、KVO、Delegate 是同步的还是异步的?
在哪个线程中触发,就在哪个线程中响应,都是同步的,会阻塞当前线程,直到处理完成。
- 要注意避免阻塞主线程,如果存在耗时操作,建议在方法中先异步操作,再回到主线程做更新UI操作。
注意事项
- 监听方法执行会在属性变化所在的线程上执行!
- 如果多个线程同时修改一个属性,可能会出现资源抢夺的问题
- 如果监听的属性多,KVO 的监听方法会非常难写
- 对象销毁之前,一定要取消监听