FBKVOController 源码使用及解读

Posted 想名真难

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FBKVOController 源码使用及解读相关的知识,希望对你有一定的参考价值。

源码地址: KVOController 

 系统标准KVO的使用

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
}

// 某个方法修改了self.person的name属性 
...


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if (object == self.person && [keyPath isEqualToString:@"name"]) {
        NSLog(@"change: %@",change);
        NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
        NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
        self.secondLabel.text = change[NSKeyValueChangeNewKey];
    }
}

- (void)dealloc {
    // ios9之后,不移除也不会有问题了
    [self.person removeObserver:self forKeyPath:@"name"];
    NSLog(@"%s",__func__);
}

 Apple原生KVO也有一些显而易见的缺点。

  1. 添加和移除观察者要配对出现。移除一个未添加的观察者,程序会crash;重复添加观察者会造成回调方法多次调用,给程序造成逻辑上的混乱。
  2. 添加观察者,移除观察者,通知回调,三块儿代码过于分散。
  3. 观察对象很多时, observeValueForKeyPath: 太多if判断, 看起来比较乱

FBKVOController 做了什么?

简单来说,Facebook 开源的这套代码, 很少, 只有2个类+1个类别,主要是对我们经常使用的 KVO 机制进行了额外的一层封装。其中最亮眼的特色是提供了一个 block 回调让我们进行处理,避免 KVO 的相关代码四处散落,不再需要使用下面这个方法:

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

使用姿势

利用开源框架,我们这样实现,其中第二种方法可以用一行代码实现 KVO

#import "ViewController.h"
#import "FBKVOController.h"
#import "NSObject+FBKVOController.h"

@interface KVOModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end

@implementation KVOModel
@end

NS_ASSUME_NONNULL_BEGIN

@interface ViewController ()
@property (nonatomic, strong) KVOModel *model;
@property (nonatomic, strong) FBKVOController *kvoController;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //创建被观察的 model 类
    KVOModel *model = [[KVOModel alloc] init];
    //初始化设置 model 的成员变量值
    model.name = @"wo";
    model.age = 5;
    self.model = model;
    
    //第一种方法:创建 FBKVOController 对象,并被 VC 强引用,否则出了当前作用域,就会被销毁
    FBKVOController *kvoController = [[FBKVOController alloc] initWithObserver:self];
    _kvoController = kvoController;
   
    //添加 观察
    [kvoController observe:model keyPath:@"name" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id  _Nullable observer, id  _Nonnull object, NSDictionary<NSKeyValueChangeKey,id> * _Nonnull change) {
            NSLog(@"我的旧名字是:%@", change[NSKeyValueChangeOldKey]);
            NSLog(@"我的新名字是:%@", change[NSKeyValueChangeNewKey]);
    }];

    //第二种方法:无需主动创建 FBKVOController 对象,self.KVOController 直接懒加载创建FBKVOController 对象
    //可以直接对某个对象的多个成员变量执行 KVO
    //------真正实现一行代码搞定 KVO------
    [self.KVOController observe:model keyPaths:@[@"name", @"age"] options:  NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
        
        NSString *changedKeyPath = change[FBKVONotificationKeyPathKey];
        

        if ([changedKeyPath isEqualToString:@"name"]) {
            NSLog(@"修改了名字");
        } else if ([changedKeyPath isEqualToString:@"age"]) {
            NSLog(@"修改了年龄");
        }
        
        NSLog(@"旧值是:%@", change[NSKeyValueChangeOldKey]);
        NSLog(@"新值是:%@", change[NSKeyValueChangeNewKey]);
    }];

    //修改 model 的 name 成员变量
    model.name = @"ni";
}

@end

NS_ASSUME_NONNULL_END

相比于原生 API 优势:

  • 1 可以以数组形式,同时对 model 的多个 不同成员变量进行 KVO
  • 2 利用提供的 block,将 KVO 相关代码集中在一块,而不是四处散落。比较清晰,一目了然。
  • 3 不需要在 dealloc 方法里取消对 object 的观察,当 FBKVOController 对象 dealloc,会自动取消观察。

源码解析

这套源代码主要包括了
FBKVOController.hFBKVOController.m
NSObject+FBKVOController.hNSObject+FBKVOController.m四个文件。
其中,NSObject+FBKVOController 这个分类比较简单。它主要干的事是通过 objc_setAssociatedObject (关联对象),以懒加载的形式给 NSObject ,创建并关联一个 FBKVOController 的对象。
接下来,我会着重介绍一下今天的主角 FBKVOController类。其文件中还包含另外两个类,_FBKVOInfo_FBKVOSharedController 。下面都会介绍到。
先来看看 FBKVOController 指定初始化函数:

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    //一般情况下 observer 会持有 FBKVOController 为了避免循环引用,此处的_observer 的内存管理语义是弱引用
    _observer = observer;
    //定义 NSMapTable key的内存管理策略,在默认情况,传入的参数 retainObserved = YES
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    //创建 NSMapTable  key 为 id 类型,value 为 NSMutableSet<_FBKVOInfo *> 类型
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    //初始化互斥锁,避免多线程间的数据竞争
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

以上初始化代码中,注释都写得比较清楚了。唯一比较陌生的是 NSMapTable 。简单来说,它与 NSDictionary 类似。不同之处是 NSMapTable 可以自主控制 key / value 的内存管理策略。而 NSDictionary 的内存策略是固定为 copy。当 key 为 object 时, copy的开销可能比较大!因此,在这里只能使用相对比较灵活的 NSMapTable

执行 KVO 的相关方法代码解析

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
    //当 keyPath 字符串长度为 0 或者 block 为空时,会产生断言,程序会 crash
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    //如果 “被观察对象” 为 nil,同样会直接返回
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }

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

  // observe object with info (利用存储的信息对 “被观察对象” 进行观察!)
  [self _observe:object info:info];
}

上述代码中主要干了2件事情, 

  1. 对于传入的参数,构建一个内部的FBKVOInfo数据结构,其存储的信息包括了 FBKVOControllerkeypathoptionsblock
  2. 调用[self _observe:object info:info];

 接上段代码的最后一句 [self _observe:object info:info];

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock 互斥锁加锁
  pthread_mutex_lock(&_lock);
  //还记得初始化 FBKVOController 时创建的 NSMapTable 么?
  //其结构是以 被观察者 object 为 key。并不像我们常用的 NSDictionary 那样是以 NSString 为 key
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence 
  // 必须重写 _FBKVOInfo hash 以及 isEqual 方法,这样才能使用 NSSet 的 member 方法。
  _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;
  }

    //如果没有 关于这个 object(被观察者)的相关信息,则创建 NSMutableSet,并添加到 NSMapTable 中
  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve -- NSMutableSet 加 info
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);
    
    //sharedController 是 干嘛的?  将所有观察信息统一交由一个单例来完成
  [[_FBKVOSharedController sharedController] observe:object info:info];
}
  1. 根据被观察的object获取其对应的infos 集合。这个主要作用在于避免多次对同一个keyPath添加多次观察。因为每调用一次addObserverForKeyPath就要有一个对应的removeObserverForKey。

  2. 从infos 集合判断是不是已经有了与此次info相同的观察(重写了_FBKVOInfo的isEqual来协助判断集合中的重复元素)。
    如果集合中已有相同的keypath,解锁,返回; 
    如果没有,进行第3步.

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

总结一下上面一段的数据结构。FBKVOController 拥有成员变量 NSMapTableNSMapTable被观察者(object)为 key, NSMutableSet 为 value 。在 NSMutableSet 中,存储了不同 info。其关系图如下图:

 追踪一下这句代码

[[_FBKVOSharedController sharedController] observe:object info:info];

_FBKVOSharedController 是一个单例, 是真正观察的类。其职责是:负责将FBKVOController发送过来的信息转发给系统的KVO处理, 当系统的KVO发出回调时, 在根据KVOInfo 调用Block或者selector。因此 app 当中所有 KVO 的通知都是由这个单例来完成的。

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

  // register info   向 NSHashTable 添加 info
  //注意:在 _FBKVOController 类中的 NSMutableSet 已经强引用了 info
  //这里是为了弱引用 info,才使用 NSHashTable,当 info dealloc 时,同时会从容器中删除
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

    //_FBKVOSharedController 是实际的观察者! 随后会进行转发 ,
   //context 是 void * 无类型指针,是 info 的指针!
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    //如果 state 是原始状态,则改为正在观察的状态,表明是在正在观察的状态
  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. 代表所有的观察信息都首先由FBKVOSharedController进行接受,随后进行转发。
  2. 对应的添加代码 有一个 移除代码,设计的相当细心啊

以上代码中想单独说一下下面的代码,其中的 context 参数使用的是 (void *)info 的指针,这样可以保证 context 的唯一性。

接收 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
    // 利用 context 查找 info,其中用到了 void  * 转换为 id 型变量 (__bridge id)
    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];
              //字典合并,并重新拷贝一份,
              //包含信息有:1、改变了哪个值 mChange 2、 原先的 change 字典
            [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];
        }
      }
    }
  }
}
  1. 根据context上下文获取对应的KVOInfo
  2. 判断当前info的observer和controller,是否仍然存在(因为之前我们采用的weak持有)
  3. 根据 info的block或者selector或者overwrite进行消息转发。

设计思路总结

  • 1 FBKVOController 持有 NSMapTable,以 objectkey 得到相对应的 NSMutableSetNSMutableSet 中存储了不同的 _FBKVOInfo。这套数据结构的主要作用是防止开发人员重复添加相同的 KVO。当检查到其中已存在相同的 _FBKVOInfo 对象时,不再执行后面的代码。
  • 2 _FBKVOSharedController 持有 NSHashTableNSHashTable 以弱引用的方式持有不同的 _FBKVOInfo。此处实际执行 KVO 代码。_FBKVOInfo 有一个重要的成员变量 _FBKVOInfoState,根据这个枚举值(_FBKVOInfoStateInitial_FBKVOInfoStateObserving_FBKVOInfoStateNotObserving) 来决定新增或者删除 KVO

收获(通读、研究源代码后)

NSSet / NSHashTableNSDictionary/ NSMapTable 的学习

NSSet 是过滤掉重复 object 的集合类,NSHashTableNSSet 的升级版容器,并且只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable 中的对象销毁时,该对象也会从容器中移除。

NSMapTableNSDictionary 类似,唯一区别是多了个功能:可以设置 keyvalueNSPointerFunctionsOptions 特性! NSDictionarykey 策略固定是 copy,考虑到开销问题,一般使用简单的数字或者字符串为 key。但是如果碰到需要用 object 作为 key 的应用场景呢?NSMapTable 就可以派上用场了!可以通过 NSFunctionsPointer 来分别定义对 keyvalue 的内存管理策略,简单可以分为 strong,weak以及 copy

什么“自释放”?

可以简单的理解为对象在生命周期结束后自动清理回收所有与其相关的资源或链接,这个清理不仅仅包括对象内存的回收,还包括对象解耦以及附属事件的清理等,比如定时器的自我停止、KVO对象的监听移除等。
那么,FBKVOController是如何做到自释放的?可以归纳为四个字——动态属性。其为观察者绑定动态属性self.KVOController,动态绑定的KVOController会随着观察者的释放而释放,KVOController在自己的dealloc函数中移除KVO监听,巧妙的将观察者的remove转移到其动态属性的dealloc函数中。

教你一行代码使用 KVO

​​​​​ FBKVOController解析

仿写简化demo地址

以上是关于FBKVOController 源码使用及解读的主要内容,如果未能解决你的问题,请参考以下文章

RPC框架之Thrift架构及源码解读

RPC框架之Thrift架构及源码解读

RPC框架之Thrift架构及源码解读

源码解读及如何保证线程安全

常用集合及源码解读

sklearn中LinearRegression使用及源码解读