1、KVO简介
KVO全称是Key-Value Observing,俗称“键值监听”,可用于监听某个对象属性值的改变。
添加监听:
self.person = [[Person alloc] init];
self.person.age = 10;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:nil];
监听回调方法:
// 当监听属性值发生改变时调用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到%@的%@属性值改变 - %@", object, keyPath, change);
}
移除监听:
- (void)dealloc {
[self.person removeObserver:self forKeyPath:@"age"];
}
2、KVO本质
给Person的实例对象添加KVO后,RunTime会在运行过程中创建一个继承于Person的子类NSKVONOtifying_Person,并将Person实例对象的isa指针指向这个子类。子类内部会将被观察属性的set方法实现替换为_NSSetIntValueAndNotify方法,如图所示:
伪代码:
// 伪代码
- (void)setAge:(int)age {
// 调用Foundation框架里的方法
_NSSetIntValueAndNotify();
}
_NSSetIntValueAndNotify内部会调用willChangeValueForKey:、父类setAge:、didChangeValueForKey:方法:
// 伪代码
void _NSSetIntValueAndNotify() {
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}
didChangeValueForKey内部会调用observeValueForKeyPath:方法:
// 伪代码
- (void)didChangeValueForKey:(NSString *)key {
// 通知监听器,某某属性值发生了改变
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
总结:
当属性的set方法出发后,执行子类内部的_NSSetIntValueAndNotify方法
_NSSetIntValueAndNotify内部调用:
willChangeValueForKey:—->super setAge:—->didChangeValueForKey:
didChangeValueForKey:内部调用:observeValueForKeyPath:
从而完成整个KVO监听流程。
3、验证
3.1、验证NSKVONotifying_Person类的存在
创建Person的两个实例对象,给其中一对象的age属性添加KVO,打印两个实例对象的isa指针
self.person1 = [[Person alloc] init];
self.person1.age = 11;
self.person2 = [[Person alloc] init];
self.person2.age = 12;
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"person1.isa = %@, person2.isa = %@",object_getClass(self.person1),object_getClass(self.person2));
打印结果:
~:添加KVO前 - person1.isa = Person, person2.isa = Person
~:添加KVO后 - person1.isa = NSKVONotifying_Person, person2.isa = Person
从打印结果可知,添加KVO后person1.isa指针指向了NSKVONotifying_Person
3.2、验证_NSSetIntValueAndNotify方法存在
通过methodForSelector方法,获取setAge:方法的IMP(实现)
NSLog(@"添加KVO前 - person1.setAge = %p, person2.setAge = %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
NSLog(@"添加KVO后 - person1.setAge = %p, person2.setAge = %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
打印结果:
添加KVO前 - person1.setAge = 0x108041e10, person2.setAge = 0x108041e10
添加KVO后 - person1.setAge = 0x10835779f, person2.setAge = 0x108041e10
从打印结果可知,添加KVO后的setAge:方法的IMP地址发生了改变,再通过LLDB命令打印IMP:
(lldb) p (IMP)0x10efcae10
(IMP) $0 = 0x000000010efcae10 (Study`-[Person setAge:] at Person.h:14)
(lldb) p (IMP)0x10835779f
(IMP) $1 = 0x000000010835779f (Foundation`_NSSetIntValueAndNotify)
得出0x10835779f对应的方法实现是Foundation框架里的_NSSetIntValueAndNotify方法。
3.3、验证_NSSetXXValueAndNotify方法内部调用顺序
在Person类中添加打印,并重新设置age的值:
- (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");
}
打印结果:
~:willChangeValueForKey
~:setAge
~:didChangeValueForKey - Begin
~:监听到<Person: 0x600000d5c270>的age属性值改变 - {
kind = 1;
new = 20;
old = 11;
}
~:didChangeValueForKey - End
得出_NSSetIntValueAndNotify内部执行顺序是:
willChangeValueForKey:—->setAge:—->didChangeValueForKey:—->observeValueForKeyPath:
*这里之所以调用_NSSetIntValueAndNotify方法是因为被添加KVO的属性是int类型,Foundation框架内部还有_NSSetDoubleValueAndNotify、_NSSetRangeValueAndNotify等方法,会根据被添加观察的属性类型决定调用哪个方法。
3.4、NSKVONotifying_*子类内部方法
打印两个对象person1(添加了KVO)和person2的类对象方法列表,进行对比
[self printClassMethodList:object_getClass(self.person1)];
[self printClassMethodList:object_getClass(self.person2)];
打印方法列表方法:
- (void)printClassMethodList:(Class)cls {
unsigned int count;
Method *methodList = class_copyMethodList(cls, &count);
NSMutableString *methodNames = [[NSMutableString alloc] init];
for (int i = 0; i < count; i++) {
Method method = methodList[i];
[methodNames appendString:NSStringFromSelector(method_getName(method))];
[methodNames appendString:@", "];
}
free(methodList);
NSLog(@"%@的方法有%@", NSStringFromClass(cls), methodNames);
}
打印结果:
~:NSKVONotifying_Person的方法有setAge:, class, dealloc, _isKVOA
~:Person的方法有setAge:, age
由打印结果得出,子类中不仅有set方法,还有class、dealloc、_isKVOA方法
子类重新class方法是为了不想让开发者知道子类的存在,所以返回的仍然是Person的类对象。内部可能实现方式:
- (Class)class {
// 返回自己的父类
return class_getSuperclass(object_getClass(self));
}
4、手动触发KVO
由于observeValueForKeyPath:是在didChangeValueForKey:方法内部调用的,所以需要调用didChangeValueForKey:方法来手动触发,且在调用didChangeValueForKey:前需要调用willChangeValueForKey:方法:
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];