一.什么是KVO
KVO,即 Key-Value Observing 是 Objective-C 对观察者设计模式的一种实现。
关于 KVO,即 Key-Value Observing,官方文档Key-Value Observing Programming Guide里的介绍比较简短
Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.

如果person想要知道 Account内部 balance 或者 interestRate的变化,可以使用KVO来实现监听。通常继承自NSObject的对象都可以作为被观察的对象
Person对象调用addObserver:forKeyPath:options:context:,观察Account的某个属性

Person对象需要实现observeValueForKeyPath:ofObject:change:context:方法,

最后在不需要监听时,需要移除监听者该Person
实例必须通过发送消息注销removeObserver:forKeyPath:
二.KVO的底层实现
定义person类
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
在ViewController中定义两个person属性
@interface ViewController ()
@property (strong, nonatomic) Person *person1;
@property (strong, nonatomic) Person *person2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
NSLog(@"person1添加KVO监听之前 - \n%@ %@\n%@ %@",object_getClass(self.person1), [self.person1 class],object_getClass(self.person2),[self.person2 class]);
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];
NSLog(@"person1添加KVO监听之后 -\n %@ %@\n%@ %@",object_getClass(self.person1),[self.person1 class],object_getClass(self.person2),[self.person2 class]);
}
@end
打印结果如下
person1添加KVO监听之前 -
Person Person
Person Person
person1添加KVO监听之后 -
NSKVONotifying_Person Person
Person Person
从上面的打印结果可以看出
-
person1在添加KVO之后,变成了NSKVONotifying_Person类型的对象,person2,由于没有添加KVO,没有任何变化。
-person1添加 KVO之后 object_getClass(self.person1)和[self.person1 class]的打印结果不同,说明class方法被重写了。
01.png
从01图片中也可以看出,_person1成员变量的isa指向了NSKVONotifying_Person类对象
NSLog(@"person1添加KVO监听之前 - \n%@ %@",[object_getClass(self.person1) superclass], [[self.person1 class] superclass]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];
NSLog(@"person1添加KVO监听之后 - \n%@ %@",[object_getClass(self.person1) superclass], [[self.person1 class] superclass]);
打印结果如下
person1添加KVO监听之前 -
NSObject NSObject
person1添加KVO监听之后 -
Person NSObject
通过这次打印可以看出
NSKVONotifying_Person是继承自 Person类
// 重写person的这个三个方法
- (void)setAge:(int)age
{
_age = age;
NSLog(@"setAge:");
}
- (void)willChangeValueForKey:(NSString *)key
{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}
- (void)didChangeValueForKey:(NSString *)key
{
NSLog(@"didChangeValueForKey - begin");
[super didChangeValueForKey:key];
NSLog(@"didChangeValueForKey - end");
}
// 当监听对象的属性值发生改变时,就会调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
}
修改person的age属性,打印日志如下
willChangeValueForKey
setAge:
didChangeValueForKey - begin
监听到<Person: 0x600002dd0750>的age属性值改变了 - {
kind = 1;
new = 21;
old = 1;
} - 18
didChangeValueForKey - end
可以看到当修改person的age值时,会调用willChangeValueForKey,然后调用setAge: 方法,最后调用didChangeValueForKey,在didChangeValueForKey方法中调用observeValueForKeyPath,通知监听者,并把值传出去。

从图02堆栈中可以看到在 didChangeValueForKey会调用监听者的observeValueForKeyPath。
NSLog(@"person1添加KVO监听之前 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];
NSLog(@"person1添加KVO监听之后 - %p %p",
[self.person1 methodForSelector:@selector(setAge:)],
[self.person2 methodForSelector:@selector(setAge:)]);
打印结果如下:
person1添加KVO监听之前 - 0x10bdd24b0 0x10bdd24b0
person1添加KVO监听之后 - 0x10c130216 0x10bdd24b0
person1的setAge:方法已经改了,此时已经是他的子类NSKVONotifying_Person的方法,因为person1的isa指向了NSKVONotifying_Person,而NSKVONotifying_Person重写了setAge:方法,所以此时setAge:的地址变了。

如图03,当我们自己实现NSKVONotifying_Person类时,控制台打印如下:
KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
KVO已经失效
KVO 与Runtime
KVO 的实现也依赖于 Objective-C 的 Runtime,官方文档《Key-Value Observing Programming Guide》中在《Key-Value Observing Implementation Details》部分简单提到它的实现:
Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
KVO的实现
当你观察一个对象(称该对象为被观察对象)时,一个新的类(NSKVONotifying_MJPerson)会动态被创建。这个类继承自被观察对象所对应类(MJPerson),并重写该被观察属性的 setter 方法;将对象的isa指向新生产的类(NSKVONotifying_MJPerson),这样在修改属性值的时候,就会调用到新类的setter方法,在新类的setter方法中通知监听者。
// 获取一个类的方法列表
- (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);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[MJPerson alloc] init];
self.person1.age = 1;
self.person2 = [[MJPerson alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"18"];
[self printMethodNamesOfClass:object_getClass(self.person1)];
[self printMethodNamesOfClass:object_getClass(self.person2)];
}
打印如下:
NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,
通过打印我们可以看到NSKVONotifying_MJPerson重写了 setAge:, class, dealloc, _isKVOA这几个方法。
子类重写了4个方法
- setAge
当修改age时,通过isa找到NSKVONotifying_MJPerson中的setAge方法,在新的类中会重写对应的set方法,在setter方法中调用了 _NSSetIntValueAndNotify (),在这个方法中会按顺序调用willChangeValueForKey,[super setAge],didChangeValueForKey
- _NSSetIntValueAndNotify ()
- (void)willChangeValueForKey:(NSString *)key
- [super setAge]
(void)didChangeValueForKey:(NSString *)key
image.png
- class
- (Class)class
{
return [Person class];
}
通过上面的打印可以看到person的class返回值是Person,并不是NSKVONotifying_MJPerson。重写class方法是为了我们调用它的时候返回跟重写继承类之前同样的内容, 为了 isKindOfClass能够返回真确的结果
- dealloc
销毁新生成的NSKVONotifying_类。
- _isKVOA
用来标示该类是一个 KVO 机制声称的类
通过终端命令可以查看Foundation中的信息:
nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
00000000001c1077 t ____NSSetBoolValueAndNotify_block_invoke
0000000000054564 t ____NSSetCharValueAndNotify_block_invoke
00000000000c54be t ____NSSetDoubleValueAndNotify_block_invoke
000000000010031c t ____NSSetFloatValueAndNotify_block_invoke
0000000000134c0a t ____NSSetIntValueAndNotify_block_invoke
0000000000051e8c t ____NSSetLongLongValueAndNotify_block_invoke
00000000001c10e2 t ____NSSetLongValueAndNotify_block_invoke
00000000000901aa t ____NSSetObjectValueAndNotify_block_invoke
00000000001233b0 t ____NSSetPointValueAndNotify_block_invoke
00000000001c11a3 t ____NSSetRangeValueAndNotify_block_invoke
0000000000090d64 t ____NSSetRectValueAndNotify_block_invoke
00000000001c1146 t ____NSSetShortValueAndNotify_block_invoke
00000000001209a3 t ____NSSetSizeValueAndNotify_block_invoke
三.KVO的缺陷
在mikeash.com: just this guy, you know?和KVO Considered Harmful这两篇文章中描述了kvo的一些缺陷。
- 所有的 observe 处理都放在一个方法里
监听者所有的回调都在observeValueForKeyPath:ofObject:change:context:中。如果监听者多了,需要做各种判断。
- KVO是字符串类型的
KVO 中的 keyPath 必须是NSString,使得编译器没办法在编译阶段将错误的 keyPath 给找出来;譬如很容易将「contentSize」写成「contentsize」;硬编码很容易出错。必须使用NSStringFromSelector(@selector(contentSize))
- KVO要求您自己处理父类
如果父类也在监听时,需要在observeValueForKeyPath中调用父类的observeValueForKeyPath
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == _tableView && [keyPath isEqualToString:@"contentSize"]) {
[self configureView];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- 注销时KVO可能崩溃
多次相同的 remove observer 会导致 crash
四.如何安全的移除KVO
- @try catch
@try {
[self.person1 removeObserver:self
forKeyPath:NSStringFromSelector(@selector(age))
context:nil];
}
@catch (NSException *exception) {
NSLog(@"%@",exception);
}
Cannot remove an observer <ViewController 0x7fa514c06890> for the key path "age" from <Person 0x60000005c490> because it is not registered as an observer.
当多次移除时,会cath到这个错误,不会造成crash
- 利用runtime获取真实类型,判断是否需要移除
Class kvoCls = object_getClass(self.person1);
// 如果包含NSKVONotifying_,则说明对象被监听了,需要移除
if ([NSStringFromClass(kvoCls) containsString:@"NSKVONotifying_"]) {
NSLog(@"移除KVO");
[self.person1 removeObserver:self forKeyPath:@"age"];
}else{
NSLog(@"已经移除");
}
如果真实类型包含NSKVONotifying_字符串,说明被监听了,可以移除
五.手动触发KVO
调用下面两个方法就可以触发
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
本文参考
《Key-Value Observing Programming Guide》
https://www.mikeash.com/pyblog/key-value-observing-done-right.html
https://khanlou.com/2013/12/kvo-considered-harmful/
https://halfrost.com/how_to_use_runtime/
https://nshipster.com/key-value-observing/
KVO底层实现
网友评论