KVO
KVO:Key-Value Observing,一种通过给目标对象添加观察者来监听字段值改变的机制。
原理
它的底层原理是通过 Runtime 的 isa 混写来实现的。
假设我们现在需要使用一个名为 KVOObserve 的类,来监听 Goods 类 price 属性值的改变,下面是实现代码:
// Goods.h
@interface Goods : NSObject
@property (nonatomic, assign) int price;
@end
// KVOObserve.m
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"object: %@, keyPath:%@, change: %@", object, keyPath, change);
}
// main
KVOObserve *observer = [KVOObserve new];
Goods *goods = [Goods new];
[goods addObserver:observer forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
goods.price = 10;
当执行完 goods.price = 10; 之后,KVOObserve 的回调方法就会打印相关信息:
object: <Goods: 0x100530310>, keyPath:price, change: {
kind = 1;
new = 10;
old = 0;
}
在给 goods 添加观察者之后,Runtime 会动态生成一个名为 NSKVONotifying_Goods 的类,goods 的 isa 会指向该类。NSKVONotifying_Goods 的 superclass 指向 Goods 类。
通过以下代码可以验证上述结论:
NSLog(@"使用 KVO 之前:%@", object_getClass(goods));
[goods addObserver:observer forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
NSLog(@"使用 KVO 之后:%@", object_getClass(goods));
// 打印
使用 KVO 之前:Goods
使用 KVO 之后:NSKVONotifying_Goods
通过打印可以看到,添加观察者之后,goods 的 isa 指向了 NSKVONotifying_Goods。
NSKVONotifying_Goods
goods 的 isa 指向了 NSKVONotifying_Goods,那 NSKVONotifying_Goods 都包含那些方法呢?这个可以通过下面的代码获取:
Goods *goods = [Goods new];
// 获取 goods 添加观察者之前的方法列表,也就是 Goods 的方法列表
unsigned int count;
Method *methodList = class_copyMethodList(object_getClass(goods), &count);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"method: %@", methodName);
}
free(methodList);
[goods addObserver:observer forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
// 获取 goods 添加观察者之后的方法列表,也就是 NSKVONotifying_Goods 的方法列表
Method *methodList2 = class_copyMethodList(object_getClass(goods), &count);
for (int i = 0; i < count; i++) {
Method method = methodList2[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"methodName: %@", methodName);
}
free(methodList2);
下面是打印结果:
使用 KVO 之前
method: price
method: setPrice:
使用 KVO 之后
methodName: setPrice:
methodName: class
methodName: dealloc
methodName: _isKVOA
Goods 与 NSKVONotifying_Goods 类对象内存结构图:
通过打印可以得知:Goods 的方法列表中有 get、set 方法;NSKVONotifying_Goods 的方法列表中有 setPrice:、class、dealloc、_isKVOA 四个方法。
得到 NSKVONotifying_Goods 的方法列表后,再来看一下它里面的 setPrice: 的具体实现。
_NSSetXXXValueAndNotify
获取 setPrice: 的方法实现:
[goods addObserver:observer forKeyPath:@"price" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
NSLog(@"%p", [goods methodForSelector:@selector(setPrice:)]);
得到 setPrice: 方法的内存地址后,通过 LLDB 调试得出方法实现:
看上图得知 setPrice: 方法内部会调用一个名为 _NSSetIntValueAndNotify 的方法。
而 _NSSetIntValueAndNotify 内部则会调用下面的三个方法:
- (void)willChangeValueForKey:(NSString *)key
- [super setPrice: xx]
- (void)didChangeValueForKey:(NSString *)key
- 内部调用观察者的回调方法 通过上述方法的调用,完成 KVO 值监听的机制。
KVO 监听流程:
手动触发 KVO
调用 willChangeValueForKey 和 didChangeValueForKey 就能手动触发 KVO:
[goods willChangeValueForKey:@"price"];
[goods didChangeValueForKey:@"price"];
关闭自动触发
当我们想自己控制某些条件下才去触发 KVO 时,可以通过重写 + (BOOL)automaticallyNotifiesObserversOfxxx 方法来实现。
比如,在某些业务场景下,仅当 price 的价格与上次的值不同我们才去触发 KVO:
// KVO 不自动触发
+ (BOOL)automaticallyNotifiesObserversOfPrice {
return NO;
}
- (void)setPrice:(int)price {
if (_price == price) {
return;
}
[self willChangeValueForKey:@"price"];
_price = price;
[self didChangeValueForKey:@"price"];
}
// 只会触发一次
goods.price = 10;
goods.price = 10;
使用场景
- 监听 model 属性值的改变,来更新 UI。
使用时的注意事项
- 回调函数的 keyPath 不能为空,为空会崩溃。
- 添加观察者和移除观察者要一一对应,多次移除会造成崩溃。
- 因为 KVO 是同步行为,如果在子线程修改了属性值,那回调函数也是在相应的子线程执行的。因为它是同步的,所以不推荐在多线程情况下使用。
KVO、代理与通知
这三种机制都是用来处理对象之间的交互的。代理对象之间的关系是一对一的,而 KVO 和通知的关系是多对多的。
代理的优势是逻辑清晰好调试,如果遵守了协议而没有实现代理方法,会有编译⚠️。缺点则是代码量多。一般的使用场景是控件之间的交互。
KVO 的优势就是代码简洁明了,只需添加观察者,在合适的时候移除观察者,即可实现监听。缺点是使用较复杂,代码需注意规范。
通知 的优势也是代码简洁明。缺点则是调试不太好调试🤣。
KVC
Key-value coding (KVC),键值编码。通过官方给出的一套 API 来存值和取值。
取值
// 根据 key 取值
- valueForKey:
// 根据 keyPath 取值
- valueForKeyPath:
// 当 valueForKey 传入的 key 找不到的时候调用
- valueForUndefinedKey:
API 使用实例:
// Car.h
@interface Car : NSObject
@property (nonatomic, copy) NSString *name;
@end
// Goods.h
@interface Goods : NSObject
@property (nonatomic, assign) int price;
@property (nonatomic, strong) Car *car;
@end
[goods valueForKey:@"price"];
[goods valueForKeyPath:@"car.name"];
[goods valueForKey:@"abc"]; //因为 Goods 没有 abc 属性,所以这一句会崩溃,并调用 valueForUndefinedKey 方法
valueForKey: 的底层方法调用流程图:
赋值
// 根据 key 设值
- setValue:forKey:
// 根据 keyPath 设值
- setValue:forKeyPath:
// 当 setValue:forKey: 传入的 key 找不到的时候调用
- setValue:forUndefinedKey:
// 当 setValue:forKey: 传入的 value 为 nil 时调用
- setNilValueForKey:
API 使用示例:
[goods setValue:@10 forKey:@"price"];
[goods setValue:@"吉利" forKeyPath:@"car.name"];
// value 传入 nil,崩溃,调用 setNilValueForKey:
[goods setValue:nil forKey:@"price"];
// abc 没有该属性,崩溃,调用 setValue:forUndefinedKey:
[goods setValue:@20 forKey:@"abc"];
setValue:forKey: 的底层方法调用流程图:
KVC 与 KVO
- 使用 KVC 赋值是否会触发 KVO?
会触发,因为 KVC 的本质就是调用 set 方法去修改属性值。而 KVO 的本质就是重写了被监听对象的 set 方法。