KVOController开源库bug分析

FBKVOController是一个对KVO机制简单封装的开源库,最大特点就是持有了observer与observe object及其keyPath的关系,利用delloc自动移除observer,同时提供了便捷的block方式去处理观察对象keyPath值的变化,省去了NSKeyValueObserving协议中原生API的麻烦。

看起来很棒!但,它并非完美

问题

举个例子,当需要观察自身keyPath值变化时

使用KVO原生API在init中添加Observer,并在dealloc中移除Observer,代码如下:

- (instancetype)init {
    self = [super init];
    if (self) {
        [self addObserver:self 
               forKeyPath:@"foo.bar" 
                  options:NSKeyValueObservingOptionNew 
                  context:nil];
    }
    return self;
}
- (void)dealloc {
    [self removeObserver:self forKeyPath:@"foo.bar"];
}

以上一个非常简单的KVO例子,但使用FBKVOController来实现就会有问题,比如:

  • 使用FBKVOController (Lazy-loaded的FBKVOController默认使用strong observe作为内部mapTable的key,此处ObserverObserve Object均为self,循环引用造成内存泄露):
- (instancetype)init {
    self = [super init];
    if (self) {
        //...
        [self.KVOController observe:self keyPath:@"foo.bar" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
            //...
        }];
    }

    return self;
}

  • 使用KVOControllerNonRetaining (Lazy-loaded的KVOControllerNonRetaining默认使用weak observe作为内部mapTable的key,在delloc时,KVOControllerNonRetaining会因为无法正确移除Observe Object而造成crash):
- (instancetype)init {
    self = [super init];
    if (self) {
        //...
        [self.KVOControllerNonRetaining observe:self keyPath:@"foo.bar" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
            //...
        }];
    }

    return self;
}

要搞清楚无法正确移除Observe Object而造成crash的原因,我们先来看看KVOControllerNonRetaining dealloc过程:

  1. self自身dealloc函数被调用, 所有持有self的weak引用对象被置为nil, 由于此处self作为KVOControllerNonRetaining内部的NSMapTable的key存在,这里直接从mapTable中remove
  2. KVOControllerNonRetaining dealloc函数被调用
  3. KVOControllerNonRetaining dealloc函数中调用unobserveAll函数用于移除所有观察者
  4. 在mapTable中无法找到self作为key的FBVKOInfo(因为第一步就被提前移除了), self没有正确的执行unobserve过程
  5. crash

解决方案

在self同时作为观察者和被观察对象时,使用NSKeyValueObserving协议的原生API代替KVOController

其他的解决思路

官方库截至1.1.0版本仍然未修复此问题,但issues中早已有一些讨论,详见