KVO 实验与原理新发现

KVO的原理已经非常多了,看了很多高赞博客,但是自己动手做了一下实验,还是有非常有趣的新发现

这篇博客的目录大致如下

  • initialize方法的多次调用
  • 派生子类的isa链
  • KVO的防护,有自己的思路,简单分析了开源代码FBKVOController

1. initialize方法的调用

先简单复习一下initialize的调用原理

  • +initialize方法在类收到第一条消息的时候调用,要注意的是,消息包括类方法
  • Category实现initialize会覆盖类的实现
  • 子类没实现会调用父类的,子类实现了
  • 如果父类也实现了,会优先调用父类的initialize

接下来进入实验,会出现一个非常有趣的现象,initialize方法被调用了两次

@implementation TestObject

+ (void)initialize {
    XMLog(@"%@", self);
}

- (void)updateAge:(NSInteger)age {
    _age = age;
}

@end

- (void)commonInit {
    TestObject *object = [[TestObject alloc] init];
    [object addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld context:nil];

    [object updateAge:1];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"observe");
}

这是打印结果

TestBaseObject
TestObject
NSKVONotifying_TestObject
  • 这里证明了父类的initialize方法会被优先调用

  • 当KVO观察的时候,发生了isa swizzling,又调用了一次initialize,只不过这一次是派生子类的initialize,但是还是发生了,也就是说,写在这个方法里的实现可能会因为KVO被重复调用

继续实验,可以看到,如果再次创建实例,观察实例,这个时候initialize方法不会再次调用了

TestObject *object1 = [[TestObject alloc] init];
[object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
[object1 updateAge:2];

2. 派生子类和原类之间的关系

Class klass = object_getClass(object);
Class metaClass = object_getClass(klass);
NSLog(@"KVO class: %@", object.class);
NSLog(@"KVO real class: %@", klass);
NSLog(@"KVO class superClass: %@", class_getSuperclass(klass));
NSLog(@"KVO metaClass: %@", metaClass);

KVO class: TestObject
KVO real class: NSKVONotifying_TestObject
KVO class superClass: TestObject
KVO metaClass: NSKVONotifying_TestObject
  • KVO之后,object.class方法被重写,还是返回原来的class

  • KVOClass isa KVOMetaClass

  • KVOSuperClass isa OriginalClass

  • 原生的Class和它的isa链保持不变

3. 通过KVC修改属性能触发KVO吗?

会触发KVO,KVC时先查对应属性的getter/setter,没有getter/setter的情况下,判断accessInstanceVariablesDirectly的返回值,为true会找对应的ivar,如果都没有找到会抛出异常

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}
  • KVC调用getter流程:getKey,key,isKey,_key,接着找实例变量_key,_isKey,key,isKey

  • KVC调用setter流程:setKey和_setKey,实例变量顺序相同

  • 没找到会走setValue:forUndefinedKey:

总结: 只要KVC是通过setter,就会触发KVO,如果需要触发KVO又要自定义setter命名,需要符合KVC查找规则,才能触发KVC

4. 使用KVO需要注意什么

4.1 哪些情况会Crash?

dealloc时没有移除观察者

  • 多次重复移除一个属性,移除了未注册的观察者

  • iOS18的情况下,如果添加和删除是同样的次数,不会crash

  • 不是同样次数的情况下,删除多会crash

  • 被观察者提前释放,被观察者在dealloc的时候还注册着KVO导致崩溃

  • 添加了观察者,但是没有实现observeValueForKeyPath:ofObject:change:context:方法

  • 添加或移除时keyPath == nil

4.2 KVO优缺点

优点:

  • 观察者模式,支持多对一观察属性,也支持观察者监听不同属性;系统封装,功能和Api齐全

  • 能对非自己创建的对象进行观察,而且不需要改变内部对象,使用SDK的时候很有用

  • 用keyPath观察属性,可以观察嵌套对象

缺点:

  • 硬编码字符串,类型不够安全

  • 回调方式单一,被观察者很多的时候容易混乱;不能像NSNotification一样直接移除观察者

  • 难以debug,不像block/delegate可以通过编译器搜索,硬编码字符串难以查找

  • 回调不一定在主线程

  • 需要开发者移除,容易崩溃

4.3 KVO的防护

4.3.1 思路

既然是治理和防护,肯定是从缺点和容易出bug的地方治理

  • 防重入

  • 防止dealloc多次移除;防止回调方法大量if语句

  • 防止keyPath为空

其实思路很简单,分成4次优化

  1. 实现一个NSObject+XMSafeKVO ```objc
    • (NSMapTable)xm_safeKVOMap { // 懒加载,第一次使用的时候,WeakToStrongMap初始化,使用setAssociatedObject赋值 }
  • (void)xm_safeSetObserver:(id):observer forKeyPath:(NSString *)keyPath { // getAssocaitedObject获取safeKVOMap,让observer和keyPath关联 }

  • (void)xm_safeAddObserver:(id)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { // 1. 判断keyPath != nil // 2. debug环境下使用NSParameter, release环境直接return // 3. 判断是否重复,重复就删除旧的,注册新的 }

  • (void)xm_safeRemoveAllObservers { // 遍历map,调用removeObserver:forKeyPath: // 加上必要的防御式编程 } ``` 通过这样的方式,就可以实现防重入+防keyPath为空+一次性移除所有keyPath的观察

问题是:回调方法大量if语句的情况没有解决

  1. 面向对象编程 上面的思路有点类似面向过程,使用方法(函数)解决问题,如果要面向对象,可以封装一个XMKVOInfo对象
@interface _XMKVOInfo : NSObject 

@property (nonatomic, copy) id block;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
@property (nonatomic, assign) void *context;

@end

注册的时候把keyPath换成_XMKVOInfo就可以了,添加几个新的方法,进行addObserver,addObserver的时候根据参数创建_XMKVOInfo

私有类:防止入侵业务代码

到这里问题基本上都解决了,使用封装的形式可以在回调的时候根据keyPath调用block

  1. 单一性原则 当业务不使用的时候,XMSafeKVO这个Category是不会入侵业务的,只要不导入头文件,可以做到业务无感知

但是往NSObject里添加Category,破坏了NSObject的单一性原则

  1. 整体迁移 就像网络请求应该被单独拆分成一个Manager一样,SafeKVO也可以是一个单独的Manager,通过这样的形式来管理关联对象

4.3.2 FBKVOController

简单看了一下FBKVOController的源码

FBKVOController:可以理解为KVOControllerManager,相当于_FBKVOSharedController和_FBKVOInfo的封装

分析最重要的几个方法

FBKVOController实现

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];

  // observe object with info
  [self _observe:object info:info];
}

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    // observation info already exists; do not observe it again

    // unlock and return
    pthread_mutex_unlock(&_lock);
    return;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

  [[_FBKVOSharedController sharedController] observe:object info:info];
}
  • 这里可以看到FBKVOController本质上就是保存了objct到它的info的映射,然后把接下来的事情交给_FBKVOSharedController

_FBKVOSharedController

- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
  if (nil == info) {
    return;
  }

  // register info
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

  // add observer
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

  if (info->_state == _FBKVOInfoStateInitial) {
    info->_state = _FBKVOInfoStateObserving;
  } else if (info->_state == _FBKVOInfoStateNotObserving) {
    // this could happen when `NSKeyValueObservingOptionInitial` is one of the NSKeyValueObservingOptions,
    // and the observer is unregistered within the callback block.
    // at this time the object has been registered as an observer (in Foundation KVO),
    // so we can safely unobserve it.
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}
  • 把info添加进HashTable,通过kvo观察object,设置info的状态

  • 这里要理解一下_state的作用

    • 当被注册为NSKeyValueObservingOptionInitial的时候,addObserver会直接调用block,有的业务可能就会在回调里unobserve

    • 这个时候下面的代码可能还没有执行,而state会直接变成_FBKVOInfoStateNotObserving

    • 在这里移除KVO

    • info已经在unobserve中被删除

_FBKVOSharedController:真正处理KVO逻辑的class

- (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos
{
  if (0 == infos.count) {
    return;
  }

  // unregister info
  pthread_mutex_lock(&_mutex);
  for (_FBKVOInfo *info in infos) {
    [_infos removeObject:info];
  }
  pthread_mutex_unlock(&_mutex);

  // remove observer
  for (_FBKVOInfo *info in infos) {
    if (info->_state == _FBKVOInfoStateObserving) {
      [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
    info->_state = _FBKVOInfoStateNotObserving;
  }
}
  • 删除info,移除KVO,设置info的状态

  • _state标识了info的状态,只有_FBKVOInfoStateObserving状态才会被移除

  • 为什么不直接判断_state == _FBKVOInfoStateInitial也移除?而是要分开在两个地方?

    • 这里涉及到时序的问题,这一块代码和observe的后续代码,不一定哪一块先执行

    • 这样判断无论谁先执行,都能正确移除观察者

回调方法处理

上面聊过,_FBKVOSharedController主要负责KVO的注册移除,当然也包括回调

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
  NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

  _FBKVOInfo *info;

  {
    // lookup context in registered infos, taking out a strong reference only if it exists
    pthread_mutex_lock(&_mutex);
    info = [_infos member:(__bridge id)context];
    pthread_mutex_unlock(&_mutex);
  }

  if (nil != info) {

    // take strong reference to controller
    FBKVOController *controller = info->_controller;
    if (nil != controller) {

      // take strong reference to observer
      id observer = controller.observer;
      if (nil != observer) {

        // dispatch custom block or action, fall back to default action
        if (info->_block) {
          NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
          // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
          if (keyPath) {
            NSMutableDictionary<NSString *, id> *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
            [mChange addEntriesFromDictionary:change];
            changeWithKeyPath = [mChange copy];
          }
          info->_block(observer, object, changeWithKeyPath);
        } else if (info->_action) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
          [observer performSelector:info->_action withObject:change withObject:object];
#pragma clang diagnostic pop
        } else {
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}
  • context实际上就是info,在observe的时候作为参数传递的

  • 这里可以看到,回调实际上就是做了一堆防御式编程相关的判断,然后根据参数调用block/action,如果是直接注册的,就调用observer的实现

  • 所以这里要注意的是,即使使用了SDK,如果不使用block和action进行回调,还是需要实现KVO的回调方法,不然就会崩溃

自动解绑

- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

5. 其它

  • 关闭KVO方法,写在被观察者类里
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }else{
        return [super automaticallyNotifiesObserversForKey:key];
    }
}