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"];
