探讨KVO底层原理、存在的缺陷以及解决方法

275 阅读5分钟

KVO

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

KVO底层实现原理分析与验证

基本使用

打印结果

那么给person1和person2赋值的时候,为什么只是添加了监听者对象的属性变化会调用呢?

我们看到person1与person2的区别就是person2添加了监听者。所以猜测是person2实例对象发生了变化。

打印结果显示person1与person2实例对象的isa指向的类对象是不同的,也就是添加了KVO监听的实例对象的isa指向的类对象是NSKVONotifying_BYPerson,未添加监听者的实例对象的isa指向的还是原来的类对象。

我们都知道实例对象调用方法是给这个对象发送消息,根据实例对象的isa,找到类对象,优先在这个类对象中去寻找对象方法,找不到的话会根据superClass指针找父类的类对象,再去父类的类对象中查询对象方法。

person1的isa

person2的isa

备注: NSKVONotifying_BYPerson这个派生的子类,是runtime动态生成的, NSKVONotifying_BYPerson这个类对象的isa指针指向这个派生类的元类对象

我们来验证下NSKVONotifying_BYPerson类中有哪些对象方法

// 打印类中的方法
- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);
    
    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];
    
    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
        // 获得方法
        Method method = methodList[i];
        // 获得方法名
        NSString *methodName = NSStringFromSelector(method_getName(method));
        // 拼接方法名
        [methodNames appendString:methodName];
        [methodNames appendString:@", "];
    }
    
    // 释放
    free(methodList);
    
    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
}

NSKVONotifying_BYPerson类中有setAge:, class, dealloc, _isKVOA方法

派生的子类中的 setAge: 方法

根据结果来看person2会优先调用NSKVONotifying_BYPerson这个类对象中的setAge:方法

我们来看下NSKVONotifying_BYPerson对象方法的setAge:方法的实现:

**methodForSelector:**方法是返回实例对象对应的对象方法的实现

我们看到person1的"setAge:"方法实现没有发生变化,person2在添加监听者之前和之后是不同的

p (IMP) 地址

会打印出该方法的实现

person1是正常调用的BYPerson类的setAge:方法,而person2的"setAge:"方法的实现是函数**_NSSetIntValueAndNotify**这个函数

_NSSetIntValueAndNotify内部实现

  1. 调用willChangeValueForKey,记录旧的值
  2. 调用原来的setter方法
  3. 调用didChangeValueForKey (该方法内部会调用observer的observeValueForKeyPath:ofObject:change:context方法)

派生的子类中的 class 方法

- (Class)class {return [BYPerson class];}

派生的子类中重写了class方法,返回的是父类的class方法。苹果为什么要这么做呢?

我认为是苹果不想公开NSKVONotifying_BYPerson这个类,屏蔽内部实现,隐藏派生子类的实现,让开发者更注重业务的开发。 即使开发者调用class方法,告诉开发者的也是BYPerson,是父类,也是正确的。

KVOController源码讨论

KVO现状

  • addObserver和removeObserver必须配对出现,仅支持以下用法

      [observe addObserver...];
      // do something
      [observe removeObserver...];
    
  • 很多时候明明代码已经做到了配对出现且按次序调用,由于多线程导致软件运行时环境复杂化 或者 观察者意外地过早释放掉还是发现有相关的crash。

  • 对同一个实例对象的属性添加多次监听,在属性发生变化的时候,observeValueForKeyPath:ofObject:change:context的方法会调用多次

KVOController的优势

  • 提供更易用的的接口

提供对block回调和自定义selector的支持,保持了灵活性和强大

  • 提供更加安全的接口

通过解决addObserver 和 removeObserver必须配对且按次序调用的问题,提供了一套更加安全的接口。

使用单例主动监听,保证在程序声明周期不会释放掉。

  • 消息过滤 通过_FBKVOInfo *existingInfo = [infos member:info], 首先判断是否已经包含对应观察者消息,如果包含则直接返回,不会再重复注册。

KVOController 流程

关键类

	_FBKVOInfo:
	{
		@public
		// 对FBKVOController的弱引用
		__weak FBKVOController *_controller;
		// 被监听者的keyPath
		NSString *_keyPath;
		NSKeyValueObservingOptions _options;
		SEL _action;
		void *_context;
		// 被监听到的实例变量发生变化之后的回调
		FBKVONotificationBlock _block;
		// 本对象的状态
		_FBKVOInfoState _state;
	}

	FBKVOController:
		// 用来记录监听的table
		NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
		// 弱引用一个被监听的对象
		@property (nullable, nonatomic, weak, readonly) id observer;

	_FBKVOSharedController:
	// 记录监听的集合
	NSHashTable<_FBKVOInfo *> *_infos;

第一步、(基本使用)

  // Arrange 1: Create a controller to observe changes to a circle.
  id<FBKVOTestObserving> observer = mockProtocol(@protocol(FBKVOTestObserving));
  FBKVOController *controller = [FBKVOController controllerWithObserver:observer];

  // Arrange 2: Observe the key paths "radius" and "borderWidth" on the circle.
  //            Aggregate the new values in an array.
  NSMutableArray *newValues = [NSMutableArray array];
  FBKVOTestCircle *circle = [FBKVOTestCircle circle];
  // 将被监听者、keyPath、传到FBKVOController内部
  [controller observe:circle keyPaths:@[radius, borderWidth] options:NSKeyValueObservingOptionNew block:^(id observer, FBKVOTestCircle *circle, NSDictionary *change) {
    [newValues addObject:change[NSKeyValueChangeNewKey]];
  }];

第二步、(在FBKVOController的操作)

  1. 根据FBKVOController、keyPath等创建_FBKVOInfo对象,并把被监听者作为key,创建对应NSMutableSet作为value,将_FBKVOInfo对象添加到这个NSMutableSet中。将key与value的对应关系存储到FBKVOController的的objectInfosMap(NSMapTable类型)成员变量中

  2. 等下次添加的时候,根据被监听者(key)判断是否创建了这个NSMutableSet(value),未创建则重新创建;如果判断已经创建了,取出对应的NSMutableSet, 再判断这个NSMutableSet是否包含了该keypath创建_FBKVOInfo对象,如果包含则直接return,未创建则重新创建。过滤掉重复监听同一个被监听者的同一个属性。

  3. 将被监听者和生成的_FBKVOInfo对象传给_FBKVOSharedController这个单例

第三步、(在_FBKVOSharedController中的操作)

  1. 让_FBKVOSharedController这个单例对被监听者进行监听,因为监听者是单例,所以监听者在程序的运行声明周期不会释放掉。

  2. 在监听回调方法observeValueForKeyPath中做了以下判断

    2.1. 取出对应的info,判断info持有的_controller是否释放掉了,如果释放掉了直接return

    2.2. 判断_controller持有的被监听者是否释放掉了,如果释放掉了直接return

  3. 走完上述两步,进行block的回调。保证在回调过程中被监听者、监听者和KVOController都是存在的