KVO原理分析
一、KVO底层实现原理
示例代码:
@interface LGViewController ()
@property (nonatomic, strong) LGPerson *person;
@end
@implementation LGViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.name);
self.person.name = @"KC";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
@end
KVO
的实现过程实际上是利用了 OC
的 runtime
机制,当一个实例对象(比如上面的 self.person
)添加观察者时,底层根据该实例对象所属的类动态添加了一个类(动态添加的类名就是在原来类的类名前加上NSKVONotifying_前缀
),这个类是继承自原来的类的。上面实例的底层实现过程如下:
self.person
添加观察者时,底层就利用 runtime
动态生成一个叫 NSKVONotifying_LGPerson
的类,这个类继承自 LGPerson
类,并重写了以下实例方法: 重写 class
方法,不重写的话调用这个方法返回的是 NSKVONotifying_LGPerson
这个类,重写后返回的是原本的LGPerson
类。苹果这么做的目的是为了隐藏 KVO
的实现细节。 重写 dealloc
方法,在这个方法里面做一些收尾的工作。 重写 _isKVOA
方法,这是一个私有方法,我们不必关心。 重写被监听属性的 setter
方法,上面案例只监听了 name
属性,所以只需重写 setName:
方法。重写 setter
是实现 KVO
的关键,在 setter
方法里面实际是调用的 Foundation
框架下的 _NSSet***ValueAndNotify
方法(***表示不是一个固定的,这个和监听的属性的类型有关,比如是属性是int类型的话这里就是 __NSSetIntValueAndNotify
,所包含的类型会在后面列出来)。
然后将 self.person
这个实例对象的 isa
改为指向 NSKVONotifying_LGPerson
(原本是指向 LGPerson
类的)。
当我们设置被监听属性的值时 self.person.name = @"KC"
,是调用的 setName:
方法,前面说了 setName:
方法被重写了,所以实际上调用的是 _NSSetObjectValueAndNotify
这个方法。这个方法实现苹果是没有开源的,无法得知其具体实现,不过可以猜出其实现流程大致如下: 首先调用 [self willChangeValueForKey:@"name"];
这个方法。 然后调用原先的 setter
方法的实现(比如 _name = name;
); 再调用 [self didChangeValueForKey:@"name"];
这个方法。 最后在 didChangeValueForKey:
这个方法中调用观察者的 observeValueForKeyPath: ofObject: change: context:
方法来通知观察者属性值发生了变化。
二、 KVO底层实现的验证
2.1 我们怎么知道添加观察者时动态添加了一个类?
官方文档:
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.
当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。 isa指针的值不一定反映实例的实际类
首先,我们来验证一下这个中间类,我们可以看到在设置 KVO
之后,对象的类已经指向了 NSKVONotifying_LGPerson
- 如果原类为
A
,那么生成的派生类名为NSKVONotifying_A
- 如果我们创建一个新的名为
“NSKVONotifying_A”
的类,就会发现系统的KVO
(键值观察)并没有起作用,因为系统在注册监听的时候动态创建名为NSKVONotifying_A
的中间类,而这个类已经被我们创建,所以负责键值观察的这个类并不会工作。
KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class
KVO
给名为NSKVONotifying_Person
的类分配空间失败,自动键值观察对于该类不会工作
2.2 如何知道重写了哪些方法?
这里我们需要用到 runtime
的一些 API
来获取一个类对象里面存储的方法列表信息,下面我们先封装一个方法来获取这些信息,然后把监听前和监听后的方法列表打印出来。
#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@",NSStringFromSelector(sel));
}
free(methodList);
}
没有设置KVO之前
self.person = [[LGPerson alloc] init];
[self printClassAllMethod:object_getClass(self.person)];
打印结果如下:
2020-10-27 20:14:43.272856+0800 003---自定义KVO[74222:1238967] copyWithZone:
2020-10-27 20:14:43.272988+0800 003---自定义KVO[74222:1238967] .cxx_destruct
2020-10-27 20:14:43.273091+0800 003---自定义KVO[74222:1238967] name
2020-10-27 20:14:43.273197+0800 003---自定义KVO[74222:1238967] setName:
设置KVO之后
[self.person lg_addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:object_getClass(self.person)];
打印结果如下:
2020-10-27 20:14:49.319536+0800 003---自定义KVO[74222:1238967] setName:
所以可以看出 KVO
的原理是生成一个中间类,重写监听属性的 setter
方法
三、KVO总结
KVO
的核心是动态生成一个继承自原类的类,然后将实例对象的 isa
指向这个类。然后重写了监听属性的 setter
方法,在原有 setter
方法的前面调用 willChangeValueForKey
方法,在原有 setter
方法的后面调用 didChangeValueForKey
。
所以我们要判断某个操作是否会触发 KVO
关键在于它是否调用了监听属性的 setter
方法。比如上面的例子,self.person.name = @"KC";
这种方式就是调用 setter
方法,所以它会触发 KVO
。但是下面这几种方式是不会触发 KVO
的:
- 采用给成员变量赋值的方式,
self.person->_name = @"KC";
(前提是需要将成员变量_name
给暴露出去才能在外面访问),这种方式是不会触发KVO
的,因为它没有调用setter
方法。 - 对于集合类型,集合里面数据的更新是不会触发
KVO
的。比如[self.person.dateArray addObject:@"1"]
这样的操作,它同样没有调用setDateArray:
方法,所以不会触发KVO
。 - 如果所监听的属性是一个自定义的
OC
对象,比如有个LGDog
类里面有个age
属性,LGPerson
类里面有个LGDog
类型的属性dog
,如果我们监听dog
这个属性,当dog
的age
发生变化时并不会触发KVO
,因为它不会调用setDog:
方法。