1、KVO简介

KVO全称是Key-Value Observing,俗称“键值监听”,可用于监听某个对象属性值的改变。
添加监听:

  1. self.person = [[Person alloc] init];
  2. self.person.age = 10;
  3. NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  4. [self.person addObserver:self forKeyPath:@"age" options:options context:nil];

监听回调方法:

  1. // 当监听属性值发生改变时调用
  2. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
  3. NSLog(@"监听到%@的%@属性值改变 - %@", object, keyPath, change);
  4. }

移除监听:

  1. - (void)dealloc {
  2. [self.person removeObserver:self forKeyPath:@"age"];
  3. }

2、KVO本质

给Person的实例对象添加KVO后,RunTime会在运行过程中创建一个继承于Person的子类NSKVONOtifying_Person,并将Person实例对象的isa指针指向这个子类。子类内部会将被观察属性的set方法实现替换为_NSSetIntValueAndNotify方法,如图所示:
image.png
伪代码:

  1. // 伪代码
  2. - (void)setAge:(int)age {
  3. // 调用Foundation框架里的方法
  4. _NSSetIntValueAndNotify();
  5. }

_NSSetIntValueAndNotify内部会调用willChangeValueForKey:、父类setAge:、didChangeValueForKey:方法:

  1. // 伪代码
  2. void _NSSetIntValueAndNotify() {
  3. [self willChangeValueForKey:@"age"];
  4. [super setAge:age];
  5. [self didChangeValueForKey:@"age"];
  6. }

didChangeValueForKey内部会调用observeValueForKeyPath:方法:

  1. // 伪代码
  2. - (void)didChangeValueForKey:(NSString *)key {
  3. // 通知监听器,某某属性值发生了改变
  4. [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
  5. }

总结:
当属性的set方法出发后,执行子类内部的_NSSetIntValueAndNotify方法
_NSSetIntValueAndNotify内部调用:
willChangeValueForKey:—->super setAge:—->didChangeValueForKey:
didChangeValueForKey:内部调用:observeValueForKeyPath:
从而完成整个KVO监听流程。

3、验证

3.1、验证NSKVONotifying_Person类的存在

创建Person的两个实例对象,给其中一对象的age属性添加KVO,打印两个实例对象的isa指针

  1. self.person1 = [[Person alloc] init];
  2. self.person1.age = 11;
  3. self.person2 = [[Person alloc] init];
  4. self.person2.age = 12;
  5. NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  6. [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
  7. NSLog(@"person1.isa = %@, person2.isa = %@",object_getClass(self.person1),object_getClass(self.person2));

打印结果:

  1. ~:添加KVO - person1.isa = Person, person2.isa = Person
  2. ~:添加KVO - person1.isa = NSKVONotifying_Person, person2.isa = Person

从打印结果可知,添加KVO后person1.isa指针指向了NSKVONotifying_Person

3.2、验证_NSSetIntValueAndNotify方法存在

通过methodForSelector方法,获取setAge:方法的IMP(实现)

  1. NSLog(@"添加KVO前 - person1.setAge = %p, person2.setAge = %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);
  2. NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
  3. [self.person1 addObserver:self forKeyPath:@"age" options:options context:nil];
  4. NSLog(@"添加KVO后 - person1.setAge = %p, person2.setAge = %p",[self.person1 methodForSelector:@selector(setAge:)],[self.person2 methodForSelector:@selector(setAge:)]);

打印结果:

  1. 添加KVO - person1.setAge = 0x108041e10, person2.setAge = 0x108041e10
  2. 添加KVO - person1.setAge = 0x10835779f, person2.setAge = 0x108041e10

从打印结果可知,添加KVO后的setAge:方法的IMP地址发生了改变,再通过LLDB命令打印IMP:

  1. (lldb) p (IMP)0x10efcae10
  2. (IMP) $0 = 0x000000010efcae10 (Study`-[Person setAge:] at Person.h:14)
  3. (lldb) p (IMP)0x10835779f
  4. (IMP) $1 = 0x000000010835779f (Foundation`_NSSetIntValueAndNotify)

得出0x10835779f对应的方法实现是Foundation框架里的_NSSetIntValueAndNotify方法。

3.3、验证_NSSetXXValueAndNotify方法内部调用顺序

在Person类中添加打印,并重新设置age的值:

  1. - (void)setAge:(int)age {
  2. _age = age;
  3. NSLog(@"setAge");
  4. }
  5. - (void)willChangeValueForKey:(NSString *)key {
  6. [super willChangeValueForKey:key];
  7. NSLog(@"willChangeValueForKey");
  8. }
  9. - (void)didChangeValueForKey:(NSString *)key {
  10. NSLog(@"didChangeValueForKey - Begin");
  11. [super didChangeValueForKey:key];
  12. NSLog(@"didChangeValueForKey - End");
  13. }

打印结果:

  1. ~:willChangeValueForKey
  2. ~:setAge
  3. ~:didChangeValueForKey - Begin
  4. ~:监听到<Person: 0x600000d5c270>的age属性值改变 - {
  5. kind = 1;
  6. new = 20;
  7. old = 11;
  8. }
  9. ~:didChangeValueForKey - End

得出_NSSetIntValueAndNotify内部执行顺序是:
willChangeValueForKey:—->setAge:—->didChangeValueForKey:—->observeValueForKeyPath:

*这里之所以调用_NSSetIntValueAndNotify方法是因为被添加KVO的属性是int类型,Foundation框架内部还有_NSSetDoubleValueAndNotify、_NSSetRangeValueAndNotify等方法,会根据被添加观察的属性类型决定调用哪个方法。

3.4、NSKVONotifying_*子类内部方法

打印两个对象person1(添加了KVO)和person2的类对象方法列表,进行对比

  1. [self printClassMethodList:object_getClass(self.person1)];
  2. [self printClassMethodList:object_getClass(self.person2)];

打印方法列表方法:

  1. - (void)printClassMethodList:(Class)cls {
  2. unsigned int count;
  3. Method *methodList = class_copyMethodList(cls, &count);
  4. NSMutableString *methodNames = [[NSMutableString alloc] init];
  5. for (int i = 0; i < count; i++) {
  6. Method method = methodList[i];
  7. [methodNames appendString:NSStringFromSelector(method_getName(method))];
  8. [methodNames appendString:@", "];
  9. }
  10. free(methodList);
  11. NSLog(@"%@的方法有%@", NSStringFromClass(cls), methodNames);
  12. }

打印结果:

  1. ~:NSKVONotifying_Person的方法有setAge:, class, dealloc, _isKVOA
  2. ~:Person的方法有setAge:, age

由打印结果得出,子类中不仅有set方法,还有class、dealloc、_isKVOA方法
image.png
子类重新class方法是为了不想让开发者知道子类的存在,所以返回的仍然是Person的类对象。内部可能实现方式:

  1. - (Class)class {
  2. // 返回自己的父类
  3. return class_getSuperclass(object_getClass(self));
  4. }

4、手动触发KVO

由于observeValueForKeyPath:是在didChangeValueForKey:方法内部调用的,所以需要调用didChangeValueForKey:方法来手动触发,且在调用didChangeValueForKey:前需要调用willChangeValueForKey:方法:

  1. [self.person1 willChangeValueForKey:@"age"];
  2. [self.person1 didChangeValueForKey:@"age"];