Facebook出品,优雅的KVO框架源码分析+使用小技巧

1,925 阅读5分钟

原文链接,代码高亮支持要好一点

#FBKVOController源码学习 背景:Apple原生的KVO相信做过iOS开发的童鞋基本上都用过,有一些坑大家说不定都踩过(偷笑)

1.添加和移除观察者的代码要配对写,重复添加观察者可能会导致回调多次。

2.添加,移除观察者,以及回调处理的代码过于分散

3.使用原声KVO时要是忘了移除观察者,程序可能会crash。

直到一天发现了Facebook出的KVOController这个第三方库,它对KVO进行了封装,用起来更为简单,且线程安全,也提供了灰常方便的block回调,用起来贼爽,这里强烈推荐给大家。

这个库用了好长时间了,顺带读读人家的代码吧,读完发现代码质量挺高的,600多行代码,倘若仔细阅读一番,相信你会有收货的。

下面,我们来看看这个小巧精美的库的实现原理。 #源码分析

##NSObject+FBKVOControlle 目录简单,就这两大类文件

FBKVOController.h/.m

NSObject+FBKVOController.h/.m

其中NSObject+FBKVOController通过AssociateObject的方式给每个NSObject提供了一个FBKVOController对象,这样一来,使用的时候更为方便,直接self.KVOController就能获取到一个FBKVOController对象。

   [self.KVOController observe:_aBook keyPath:@"name" options:NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
        
        NSString *name = change[NSKeyValueChangeNewKey];
        NSLog(@"书名发生变化---name=%@",name);
    }];
    

简单的几行代码就完成了对对象属性的监听,以及对监听到的值进行一些处理,so easy.

##FBKVOController FBKVOController是整个框架中对外提供API的类,来看看这个类的初始化部分


@implementation FBKVOController
{
    //作为KVO的管理者,自然要知晓与KVO有关的信息,KVO有关的信息都保存在_objectInfosMap中
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
//    使 KVOController 达到线程安全,用于在操作 _objectInfosMap 时使用。
  pthread_mutex_t _lock;
}

#pragma mark Lifecycle -

+ (instancetype)controllerWithObserver:(nullable id)observer
{
  return [[self alloc] initWithObserver:observer];
}

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
//      1.
    _observer = observer;
//      2.
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
//      3.
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

1.初始化的时候对传入的observer进行弱引用,避免Retain Cycle

2.NSMapTable平时很少用到,类似于NSMutableDictionary,不同的是NSMapTable可以对Key或是value采用不同的内存管理方案.(NSMutableDictionary中对key和value都采用强引用了),这里对key值可以选择若引用或是强引用。

更多的介绍看这里

3.初始化锁,后续对_objectInfosMap操作时用到。

初始化完成后,紧接着就是调用API完成对某对象的keypath进行观察

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{

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

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

1.构建一个内部的_FBKVOInfo数据结构,_FBKVOInfo保存了kvo有关的信息,这个类主要是用于保存kvo相关信息。

2.调用私有方法-_observe:info:

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{

//1.
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

//2.
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    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);
  //3.
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

1.根据被观察的对象获取infos。

2.判断要添加的info是否添加过了,这里避免对一个keypath多次添加,避免程序crash,毕竟每次调用addObserverForKeyPath,就得有相应的removeObserverForKey

3.添加info后,把要观察的信息交由_FBKVOSharedController来处理。

到此FBKVOController光荣完成了任务,剩下的就看_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) {

    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
}

_FBKVOSharedController的主要职责便是调用苹果原生的KOV -observe:forKeyPath:options:context:方法,开始对属性进行监听。

同时会在原生KVO的回调方法中,将回调的数据进行分发。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, id> *)change
                       context:(nullable void *)context
{

  _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) {
        //block
          NSDictionary<NSString *, 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];
        }
      }
    }
  }
}

_FBKVOSharedController 会根据注册过的KVO信息 _KVOInfo ,选择不同的方式分发,block 或者选择 子,优先选择block的方式。

要是之前既没有传入block或者_action,则调用观察者 KVO 回调方法。

#KVO的移除操作 一般情况下,我们不需要手动去移除观察者,框架已经帮我们处理好了,每当一个FBKVOController对象被释放的时候,会主动移除观察者。

移除和添加KVO时进行的一系列操作比较相近,代码阅读起来也不费事,就不一一述说了。

说说一些框架使用的小技巧和如何避免潜在的坑。 ##使用框架的小技巧

    [self.KVOController observe:_aBook
                        keyPath:FBKVOKeyPath(_aBook.name)
                        options:NSKeyValueObservingOptionNew
                          block:^(id  _Nullable observer,
                                  id  _Nonnull object,
                                  NSDictionary<NSString *,id> * _Nonnull change)
    {
        NSString *name = change[NSKeyValueChangeNewKey];
        NSLog(@"book:%@",name);
    }];

传入keypath参数的时候可以用上FBKVOKeyPath宏,这样在编译的时候倘若发现要添加的keypath不存在时会报错。

##注意,有个小坑

[self.KVOController observe:self keyPath:@"date" options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
    // to do
}];

监听自身某个属性的时候会出现循环引用,需要在合适的时候(例如在某些操作完成后,不需要再对该属性进行监听了)手动去调用移除监听api。

这里千万别忘记啊!!

整个框架小巧精美,解决了使用原生KVO的一些痛点,代码也不难维护,喜欢用KVO的童鞋用起来吧。