概念

  • KVO —— key value observer 俗称“键值监听”,可以用于监听某个对象属性值的改变。
  • Apple 使用了isa 混写(isa-swizzling )来实现 KVO
  • 监听对象属性变化的一种手段,可以用在开源框架,让代码解耦。例如:上拉、下拉刷新控件

底层实现

KVCKVO 都属于 键值编程 而且底层实现机制都是isa-swizzing就是类型混合指针机制

image.png

  • 当类A的对象第一次被观察的时候,系统会在运行期动态创建类A的派生类。我们称为B。
  • 在派生类B中重写类A的setter方法,B类在被重写的setter方法中实现通知机制。
  • 类B会重写 class方法,将自己伪装成类A,重写dealloc方法释放资源。
  • 系统将所有指向类A对象的isa指针指向类B的对象。

注意:

  • isa 指针的值并不一定反映实例的实际类,不能依靠isa指针来确定对象是否是一个类的成员,应该使用class方法来确定对象实例的类。
  • KVO依赖于强大的Runtime机制我们并不需要向被观察者添加额外的代码,就能在属性变化的时候得到通知。

    查看_NSSet*AndNotify的存在

    image.png

    _NSSet*ValueAndNotify的内部实现

    image.png

  • 调用willChangeValueForKey:

  • 调用原来的setter实现
  • 调用didChangeValueForKey:
    • didChangeValueForKey:内部会调用observer的observeValueForKeyPath:ofObject:change:context:方法

获取派生类的方法列表

  1. #import <objc/runtime.h>
  2. - (void)printMethodNamesOfClass:(Class)cls
  3. {
  4. unsigned int count;
  5. // 获得方法数组
  6. Method *methodList = class_copyMethodList(cls, &count);
  7. // 存储方法名
  8. NSMutableString *methodNames = [NSMutableString string];
  9. // 遍历所有的方法
  10. for (int i = 0; i < count; i++) {
  11. // 获得方法
  12. Method method = methodList[i];
  13. // 获得方法名
  14. NSString *methodName = NSStringFromSelector(method_getName(method));
  15. // 拼接方法名
  16. [methodNames appendString:methodName];
  17. [methodNames appendString:@", "];
  18. }
  19. // 释放
  20. free(methodList);
  21. // 打印方法名
  22. NSLog(@"%@ %@", cls, methodNames);
  23. }
  24. [self printMethodNamesOfClass:object_getClass(self.person1)];
  25. [self printMethodNamesOfClass:object_getClass(self.person2)];

打印出结果如下:

NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
MJPerson setAge:, age,

优点:

  1. 能够提供一种简单的方法实现两个对象间的同步。例如:modelview之间同步;
  2. 能够对非我们创建的对象,即内部对象的状态改变作出响应,而且不需要改变内部对象(SKD对象)的实现;
  3. 能够提供观察的属性的最新值以及先前值;
  4. key paths来观察属性,因此也可以观察嵌套对象;
  5. 完成了对观察对象的抽象,因为不需要额外的代码来允许观察值能够被观察

缺点:

  1. 观察的属性必须使用strings来定义。因此在编译器不会出现警告以及检查;
  2. 对属性重构将导致我们的观察代码不再可用;
  3. 复杂的 if语句要求对象正在观察多个值。这是因为所有的观察代码通过一个方法来指向;
  4. 当释放观察者时不需要移除观察者。

代码演练

  • 添加观察
// 添加键值观察
/**
 1. 调用对象:要监听的对象
 2. 参数
 1> 观察者,负责处理监听事件的对象
 2> 观察的属性
 3> 观察的选项
 4> 上下文
 */
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];
  • 监听方法
// NSObject 分类方法,意味着所有的 NSObject 都可以实现这个方法!
// 跟协议的方法很像,分类方法又可以称为“隐式代理”!不提倡用,但是要知道概念!
// 所有的 kvo 监听到事件,都会调用此方法
/**
 1. 观察的属性
 2. 观察的对象
 3. change 属性变化字典(新/旧)
 4. 上下文,与监听的时候传递的一致

 可以利用上下文区分不同的监听!
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    NSLog(@"睡会 %@", [NSThread currentThread]);

    [NSThread sleepForTimeInterval:1.0];

    NSLog(@"%@ %@ %@ %@", keyPath, object, change, context);
}

常见面试题

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

  • 利用RuntimeAPI动态生成一个子类,并且让instance对象的isa指向这个全新的子类
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数
    • willChangeValueForKey:
    • 父类原来的setter
    • didChangeValueForKey:
  • didChangeValueForKey内部会触发监听器(Oberser)的监听方法( observeValueForKeyPath:ofObject:change:context:

如何手动触发KVO?

  • 手动调用willChangeValueForKey:didChangeValueForKey:

直接修改成员变量会触发KVO么?

  • 不会触发KVO

    NSNotification、KVO、Delegate 是同步的还是异步的?

  • 在哪个线程中触发,就在哪个线程中响应,都是同步的,会阻塞当前线程,直到处理完成。

  • 要注意避免阻塞主线程,如果存在耗时操作,建议在方法中先异步操作,再回到主线程做更新UI操作。

注意事项

  • 监听方法执行会在属性变化所在的线程上执行!
  • 如果多个线程同时修改一个属性,可能会出现资源抢夺的问题
  • 如果监听的属性多,KVO 的监听方法会非常难写
  • 对象销毁之前,一定要取消监听