美文网首页
FBKVOController源码分析

FBKVOController源码分析

作者: 充满活力的早晨 | 来源:发表于2018-08-16 11:52 被阅读37次

前段时间有个同事问我,KVO有什么缺点,我一脸懵逼。因为用的相对较少,也不知道出现过啥问题,不过他提到了FBKVOController 这个脸书出的KVO。因此,就以此开始学习源码。(作者是第一次看源码,有不好之处欢迎指针)

FBKVOController 源码地址


基本用法

脸书的example

FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {

  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

看上去很简单的样子。
通过上面的例子我们能看出一下几点

首先我们要创建FBKVOController
FBKVOController 要绑定观察的类类的属性
FBKVOController 保存类属性变化的回调block


源码分析

这里起始类是FBKVOController 我们就开始分析FBKVOController 类。
分析一个类我习惯是先看属性或者成员变量,在分析初始化,接着看使用调用,从使用调用中看具体类库是如何设计的。

FBKVOController 属性或者成员变量

FBKVOControllerProperty.png

从上图我们能看出,这里就三个变量,

observer 是id类型的
_objectInfosMap 是NSMapTable 类型的
还有一把锁 _lock 是pthread_mutex_t

FBKVOController 初始化

+ (instancetype)controllerWithObserver:(nullable id)observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObserver:(nullable id)observer;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;

这里有五个初始化,最后两个是无效的。因此有效的是前面三个。
在前面三个中, 第二个函数的结尾标记是NS_DESIGNATED_INITIALIZER,这个就是真正的初始化,其他两个的初始化函数其实最终都是调用第二个函数进行真正的初始化。(这里是人家大厂写的规范,我们看api就知道了主要看什么了,我们其实以后写api也可以学习借鉴下)
接下来我们看第二个函数

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

初始化函数很简单

1.保存observer。
2.初始化 _objectInfosMap 成员变量。这里根据传入的参数retainObserved,来改变_objectInfosMap集合 的初始化参数。
retainObserved 是yes ,_objectInfosMap 保存对象是强引用
retainObserved 是no, _objectInfosMap 保存对象是若引用
不过,不管强引用还是弱引用,_objectInfosMap对象支持指针之间的isEqual:和hash。
3.初始化锁

NSPointerFunctionsStrongMemory创建了一个retain/release对象的集合,非常像常规的NSSet或NSArray。
NSPointerFunctionsWeakMemory使用等价的__weak来存储对象并自动移除被销毁的对象。
NSPointerFunctionsCopyIn在对象被加入到集合前拷贝它们。
NSPointerFunctionsObjectPersonality使用对象的hash和isEqual:(默认)。
NSPointerFunctionsObjectPointerPersonality对于isEqual:和hash使用直接的指针比较。

函数调用 值增加KVO

block 形式
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
SEL 形式
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action;
apple官方的回调函数
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options action:(SEL)action;
- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

看api的样子,前面三个像是一组api,后面三个像是一组api。

我们先看block形式的api

- (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];
}

从这个函数我们看到

1 对参数进行校验
2 初始化类_FBKVOInfo。(具体干嘛的等以后在看)
3 调用- (void)_observe:(id)object info:(_FBKVOInfo )info 函数 。传入的参数是外界传入的object上面生成的_FBKVOInfo对象* .接着看这个函数的实现

- (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];
}

*- (void)_observe:(id)object info:(_FBKVOInfo )info 函数

1.锁
2.从_objectInfosMap 成员变量中找object对象对应的数据。这里看object对应的是value 值是NSMutableSet
3.从NSMutableSet 中查找_FBKVOInfo对象 是否存在,存在就解锁返回了。
4.不存在。那么就检查object 对应的NSMutableSet 是否存在,不存在,就生成一个NSMutableSet ,将object 关联到 生成的NSMutableSet上。

  1. 将_FBKVOInfo 对象存入到 NSMutableSet 中
    6.解锁(这里我们知道对_objectInfosMap 成员变量的操作是线程安全的)
    7 调用_FBKVOSharedController 中的+ (instancetype)sharedController 方法和 *- (void)observe:(id)object info:(nullable _FBKVOInfo )info方法

从上面的函数我们知道了_objectInfosMap 的结构


image.png

这里FBKVOController 类里面的调用结束了,跳转到_FBKVOSharedController 类里面了。看到这里我们还没有看 _FBKVOInfo_FBKVOSharedController 这两个类。

接下来我们先弄明白 _FBKVOInfo.我大概浏览了下这个类,比较简单。我们还是先看结构

_FBKVOInfo.png

这里还是比较简单的看字段就能明白,保存kvo相关信息,这里的FBKVOController 使用的是__weak修饰的不是强引用。相当于MVC的model类。防止循环引用。这里注意下**

接下来我们看 _FBKVOSharedController
这个类是个单例

+ (instancetype)sharedController
{
  static _FBKVOSharedController *_controller = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    _controller = [[_FBKVOSharedController alloc] init];
  });
  return _controller;
}

这里我们看该类的结构

_FBKVOSharedController.png

两个成员变量
我们看初始化,

- (instancetype)init
{
  self = [super init];
  if (nil != self) {
    NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
    _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
    if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
      _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
    } else {
      // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
      _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
    }

#endif
    pthread_mutex_init(&_mutex, NULL);
  }
  return self;
}

这里就是初始化一个若引用的NSHashTable 和一把pthread_mutex_t 锁。

这里我们应该关注下面这个方法,这个方法是才FBKVOController中调用的

- (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];
  }
}

1这里检查 传入的_FBKVOInfo 是nil 就结束
2将_FBKVOInfo 对象线程安全的添加到 _FBKVOSharedController 单例对象的NSHashTable 中(弱引用,相当于set),

  1. object对象KVO。这里context 传入的值是Info,info 包含keyPath值。
  2. 如果 info的状态变成_FBKVOInfoStateNotObserving 。就移除
    object对象KVO

看到这里其实FBKVOController 还是用的是苹果的KVO。只是把apple的kvo进行了重新封装。为什么这么封装等下看结尾。
这里我们就应该找苹果KVO的回调了

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, 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<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];
        }
      }
    }
  }
}

这里我们获取下context 。其实就是_FBKVOInfo 对象。包含keyPath值。

1 从_FBKVOSharedController 成员NSHashTable类型的_infos 中加锁获取 下_FBKVOInfo 对象。
2 获取到_FBKVOInfo 对象,从_FBKVOInfo中获取下FBKVOController 。没有就结束

  1. 从FBKVOController 获取到 observer。没有就结束
    4.检测_FBKVOInfo 测成员变量block,有,就组装下参数,调用block返回
  2. 如果是action ,就调用action 。不过这里action需要有两个参数
    6 都没有就调用 官方的观察者回调。到此KVO增加一个观察者结束了。

一个完整的流程分析完毕了。但是三个之间的关系还是比较乱,关系图如下。

image.png

这样我们在看kvo的回调函数

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

就很清晰了

如果这里看懂了的话,那FBKVOController的其他的增加通知的方法就应该很容易看懂了。不做介绍了

函数调用 之移除KVO

移除对象的某一个keyPath
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
移除对象的所有keyPath
- (void)unobserve:(nullable id)object;
移除所有对象的keyPath
- (void)unobserveAll;
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  // get observation infos
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // lookup registered info instance
  _FBKVOInfo *registeredInfo = [infos member:info];

  if (nil != registeredInfo) {
    [infos removeObject:registeredInfo];

    // remove no longer used infos
    if (0 == infos.count) {
      [_objectInfosMap removeObjectForKey:object];
    }
  }
  // unlock
  pthread_mutex_unlock(&_lock);

  // unobserve
  [[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}

这里我们看下移除某一个对象的keyPath

1 加锁
2 获取object 对应的set集合 infos
3 从infos 查看是否包含 传入的_FBKVOInfo 类型的info
4 如果有info,set结合infos移除 info
5 这里检测 infos 集合中是否还有数据,没有数据,那么就从 _objectInfosMap(NSMapTable) 中移除object 对应的set集合(释放空间)
6 解锁
7 调用_FBKVOSharedController 对象的 *- (void)unobserve:(id)object info:(nullable _FBKVOInfo )info 方法

这里看看第七步的方法

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

  // unregister info
  pthread_mutex_lock(&_mutex);
  [_infos removeObject:info];
  pthread_mutex_unlock(&_mutex);

  // remove observer
  if (info->_state == _FBKVOInfoStateObserving) {
    [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
  }
  info->_state = _FBKVOInfoStateNotObserving;
}

解析改函数

  1. 加锁 从_infos 集合中移除info
    2.检查 info的状态state是不是_FBKVOInfoStateObserving 是就移除通知。
    3 将info的状态state变更为_FBKVOInfoStateNotObserving

这些比较简单。就不啰嗦了。就是添加通知的逆过程。

这里我们对看api的总结

1 为了保证map 和set 操作的唯一性,对所有使用到的map和set集合都加锁操作,安全
2 这里我们看见了重写了_FBKVOInfo 的hash 和 isEqual方法。这里为什么要重写这两个方法呢?
hash 是因为我们在增加到set中的时候会调用,检测对象是否再set中重合。这里我们是对keypath hash。保证set中保存的对象的keyPath肯定是唯一的。(这里我们知道,我们每次增加一个observer,都会重新生成一个_FBKVOInfo对象的,如果不重写 _FBKVOInfo对象的hash,每个对象的hash值都是不一样的,相同的keyPath也是不一样。)
重写isEqual:方法,这里我们看到下面的代码

- (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];
}

这里判断_FBKVOInfo 相等的条件是 必须是两个对象必须都是_FBKVOInfo 对象。 成立的条件有两种情况,要不是指针相等,再就是检测keyPath。要是keyPath相等,也认为是这两个对象相等。这个方法调用是在set 调用member:方法的时候检测的。


宏定义

我们知道keyPath在KVO中是以字符吃形式传递的,在编译阶段是没有办法检查是否正确的,怎么解决这个问题呢?FB给出了两个宏定义,可以在编译阶段检查keyPath是否正确。

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))

宏定义用法

用法1 
  Clock * date = [Clock new];
    [self.KVOController observe:date keyPath:FBKVOKeyPath(date.date) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ViewController * vc, Clock *clock, NSDictionary *change) {
        NSLog(@"%@%@%@",vc,clock,change);
    }];
    date.date = @"dddd";

用法2
    Clock * date = [Clock new];
    [self.KVOController observe:date keyPath:FBKVOClassKeyPath(Clock, date) options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ViewController * vc, Clock *clock, NSDictionary *change) {
        NSLog(@"%@%@%@",vc,clock,change);
    }];

    date.date = @"dddd";

宏定义分析

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

这个宏定义怎么看呢?分三部分看

1 FBKVOKeyPath(KEYPATH) 宏定义部分,传入参数KEYPATH
2 第二部分应该看着这样的结构
@(((第一部分),(第二部分))) 这个结构依次运行第一部分,第二部分。最终将第二部分的结果返回
3 第二部分 是大括号里面运算,最终返回结果是 fbkvokeypath + 1
4 这里有个大括号的用法,大括号的最后一条是一条语句,不是一条表达式。

举例大括号的用法。

    NSString * strTest = ({
       const  NSString * strddd = @"mmmm";
        strddd;
    });
必须要有一条strddd ,代表返回值。

宏定义中应该学习到了我们如何进行类型检测了


FB为什么要这么封装呢?

这就要从KVO的坑说起了
苹果原生API提供的KVO有一些显而易见的缺点。

1.添加和移除观察者要配对出现;
2.移除一个未添加的观察者,程序会crash;
3.添加观察者,移除观察者,通知回调,三块儿代码过于分散;


优点

FBKVOController对于喜好使用kvo的工程师来说,是一个好的,精简的开发框架。源码优雅,可读性高,利于自己维护。

优点如下:

提供了干净的block的回调,避免了处理这个函数的逻辑散落的到处都是。
不用担心remove问题,不用再在dealloc中写remove代码。当然,如果你需要在其他时机进行remove observer,你大可放心的remove,不会出现因为没有添加而crash的问题。

缺点:
对象无法监听自己的属性,否则会出现循环引用。

相关文章

网友评论

      本文标题:FBKVOController源码分析

      本文链接:https://www.haomeiwen.com/subject/ibrwbftx.html