FBKVOController 源码解析

开发过iOS的app已经不计其数了,在不同的项目中采用的架构也各不相同,有传统的MVC,简化的VIPER,以及一些简单的MVVM

这其中,我最不推荐的就是VIPER,谁写谁知道,,绝对是增加了项目的复杂性。MVVM由于自己总是受限于传统的Object-Oriented的思路,总是想不出真正的Functional Programming的代码,因此,绝大多数情况,写着写着都回归到了MVC

其实,相较于网上大家总喜欢提到的Massive View Controller问题,我更想说的是这种传统架构中对于信息流的不友好。

在一个典型的iOS的问题中,我们的代码执行流程,通常都是从View Controller的生命周期开始,如果是一个完全基于顺序执行的应用,那整个app的信息流是单向可跟踪的。但是往往事情并不会那么简单,我们会包含至少如下这些潜在打乱信息流的坏蛋

  • Delegate回调
  • NSNotification
  • UIView控件的Target-Action
  • KVO

在这里,你可能会以为我想谈谈ReactiveCocoaRxSwift,那你错啦,那个开源项目我暂时还没有能力去深究,所以我想从KVO事件入手,读一读Facebook出品的FBKVOController

FBKVOController

简单来说,FBKVOController是对KVO机制的一层封装,同时提供了线程安全的特性和并对如下这个臭名昭著的函数进行了封装,提供了干净的block的回调,避免了处理这个函数的逻辑散落的到处都是。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context

源码分析

整个项目的结构非常简单,包含如下四个文件:

  • FBKVOController.h/.m
  • NSObject+FBKVOController.h/.m

其中,NSObject+FBKVOController只是通过AssociateObject给NSObject提供了一个retain及一个非retain型的KVOController。

这两种不同类型的KVOController有啥区别,我们稍后再提,我们将重点投向FBKVOController这个文件。

打开这个FBKVOController.m文件,哎呀,600多行文件,有点蛋疼。没事,配合头文件粗略扫一眼以后,可以发现其中很多方法都是convenience method

简单剥离一下数据结构以后,我们可以发现,主要的数据结构有如下三个。

  • FBKVOInfo
  • FBKVOSharedController
  • FBKVOController

FBKVOController

既然我们前面通过NSObject+FBKVOController知道了每个对象都会有其对应的FBKVOController,那我们就先来看看这个类吧。

//1.
@implementation FBKVOController
{
  NSMapTable *_objectInfosMap;
  OSSpinLock _lock;
}

//2.
- (instancetype)initWithObserver:(id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    // 2.
    _observer = observer;

    // 3.
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];

    // 4.
    _lock = OS_SPINLOCK_INIT;
  }
  return self;
}
  1. 首先我们看到,这个对象持有一个OSSpinLock及一个NSMapTable。其中OSSpinLock即为自旋锁,当多个线程竞争相同的critical section时,起到保护作用。NSMapTable可能大家接触不是很多,我们在后文会详细介绍,这里大家可以先理解为一个高级的NSDictionary。

  2. 在构造函数中,首先将传入的observer进行weak持有,这主要为了避免Retain Cycle

  3. 这一段的内容可能大家不太熟悉,NSPointerFunctionsOptions简单来说就是定义NSMapTable中的key和value采用何种内存管理策略,包括strong强引用,weak弱引用以及copy(要支持NSCopying协议)

  4. 初始化自旋锁

接下来,使我们通过FBKVOController来对一个对象的某个或者某些keypath进行观察。

- (void)observe:(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;
  }

  // 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数据结构
  2. 调用[self _observe:object info:info];

接下来,我们来跟踪一下[self _observe:object info:info];,内容如下:

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

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

  // 2. 
  _FBKVOInfo *existingInfo = [infos member:info];
  if (nil != existingInfo) {
    NSLog(@"observation info already exists %@", existingInfo);

    // unlock and return
    OSSpinLockUnlock(&_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
  OSSpinLockUnlock(&_lock);

  // 3.
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

抛开Facebook自身标记的注释,有三处比较值得我们注意:

  1. 根据被观察的object获取其对应的infos set。这个主要作用在于避免多次对同一个keyPath添加多次观察,避免crash。因为每调用一次addObserverForKeyPath就要有一个对应的removeObserverForKey

  2. infos set判断是不是已经有了与此次info相同的观察。

  3. 如果以上都顺利通过,将观察的信息及关系注册到_FBKVOSharedController中。

至此,FBKVOController的任务基本都结束,unObserve相关的任务逻辑大同小异,不再赘述。

FBKVOSharedController

初次看到这个类的时候,我的脑海中浮现了两个问题,FBKVOSharedController是干嘛的?为什么FBKVOController还需要将观察的信息转交呢?

其实我个人觉得这一层不是必要的,但是按照Facebook的理念来说就是将所有的观察信息统一交由一个FBKVOSharedController单例进行维护。如果大家读过Facebook出品的Flux架构,也会发现,Facebook经常喜欢维护一个类似于中间件的注册表,在这里,FBKVOSharedController承担的也是类似的职责。

于是,通过如下方法,我们像使用注册表一样将对KVOInfo注册。

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

  // register info
  OSSpinLockLock(&_lock);
  [_infos addObject:info];
  OSSpinLockUnlock(&_lock);

  // 1.
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
}
  1. 代表所有的观察信息都首先由FBKVOSharedController进行接受,随后进行转发。

实现observeValueForKeyPath:ofObject:Change:context
来接收通知。

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

  _FBKVOInfo *info;

  {
    // 1. 
    OSSpinLockLock(&_lock);
    info = [_infos member:(__bridge id)context];
    OSSpinLockUnlock(&_lock);
  }

  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) {
          info->_block(observer, object, change);
        } 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];
        }
      }
    }
  }
}
  1. 根据context上下文获取对应的KVOInfo
  2. 判断当前infoobservercontroller,是否仍然存在(因为之前我们采用的weak持有)
  3. 根据 infoblock或者selector或者override进行消息转发。

到这里,FBKVOController整体的实现就介绍完了,怎么样,是不是局部看自己都会实现,但是一结合起完整的设计思路,就觉得,不亏是Facebook呢。

NSMapTable

之前我们在前文中提到了NSMapTable,现在我们来详细介绍他一下。
我们在平常的开发中都使用过NSDictionary或者NSMutableDictionary,但是这两种数据结构有其的局限性。

NSDictionary为例,NSDictionarykeyhash值作为索引,存储对应的value。因此,key的要求是不能更改。所以,NSDictionary为了确保安全,对于key采用了copy的策略。

默认情况下,支持NSCopying协议的类型都可以作为key。但是考虑到copy带来的开销,一般情况下我们都使用简单的诸如数字或者字符串作为key。

那么,如果要使用Object作为key,想构建Object to Object的关系怎么办呢?这个时候就用到NSMapTable。我们可以通过NSFunctionsPointer来分别定义对key和value的储存关系,简单可以分类为strong,weak以及copy。而当利用object作为key的时候,可以定义评判相等的标准,如:use shifted pointer hash and direct equality, object description或者size

具体你需要去override如下几种方法:

// pointer personality functions
@property (nullable) NSUInteger (*hashFunction)(const void *item, NSUInteger (* __nullable size)(const void *item));
@property (nullable) BOOL (*isEqualFunction)(const void *item1, const void*item2, NSUInteger (* __nullable size)(const void *item));
@property (nullable) NSUInteger (*sizeFunction)(const void *item);
@property (nullable) NSString * __nullable (*descriptionFunction)(const void *item);

FBKVOController自定义的可以作为key的结构FBKVOInfo,就复写了

- (NSUInteger)hash
{
  return [_keyPath hash];
}

- (BOOL)isEqual:(id)object
{
  if (nil == object) {
    return NO;
  }
  if (self == object) {
    return YES;
  }
  if (![object isKindOfClass:[self class]]) {
    return NO;
  }
  return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath];
}