美文网首页iOS开发
FBKVOViewController 是怎样帮我们优雅、安全的

FBKVOViewController 是怎样帮我们优雅、安全的

作者: lyuf | 来源:发表于2018-09-01 19:11 被阅读0次

    在开发中,相信不到朋友都会用到kvo来监听对象的属性值,其低耦合的特性在很多场合都可以排上用场。首先说一下使用系统提供的KVO接口来完成kvo的缺点

    1. 步骤相对比较 :
      a.添加监听
      b.实现监听回调
      c.在适当的地方移除监听

    2. 如果一个对象同事监听多个属性值,或需要监听不同的对象属性这需要在同一个回调方法中用if - else 来判别是来自哪个对象的哪个属性改变时的回调。这使的回调方法的代码相对比较臃肿,不利于后期阅读维护。

    3. 最重要的一点 , 必须在正确的地方移除监听。不然很容易会出现野指针错误。

    FBKVOViewController是facebook团队开源的框架,提供简介的接口帮我调用kvo的功能。其核心的设计是 :利用单例对象(_FBKVOSharedController)来接受对象属性改变的回调。保证了回调的安全(解决了上面说的缺点3)。当接受到观察对象的属性改变回调时 ,在根据context,向上转发回调到你定义的代码块中。

    逻辑关系.png

    我们看下FBKVOViewControllerd的简单使用

        // create KVO controller instance
        //strong  reference  _KVOController  
        _KVOController = [FBKVOController controllerWithObserver:self];
        
        // handle clock change, including initial value
        [_KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
          
          // update observer with new value
          CLOCK_LAYER(clockView).date = change[NSKeyValueChangeNewKey];
        }];
    

    就是这样几句简单的代码就已经完成了kvo的所有步骤,简介明了,用block来接收回调,在也不用写if - else 这要的区分逻辑的代码,完全符合了 代码设计的 高内聚 低耦合 的理念。

    接下来我们分析一下框架源码的实现

    框架中用到了一个比较特殊的键值对存储容器NSMapTable ,这个类与 NSDictority最大区别在于它对键、值的引用关系和相等性的判断可以自定义

    NSDictority的键值对引用关系
    [key (copy) : object (strong)]

    NSMapTable的键值对引用关系
    [key (copy,strong , weak...) : object (copy,strong , weak...)]
    如果使用的是weak,当key、value在被释放的时候,会自动从NSMapTable中移除对应的项。

    NSMapTable初始化中可以对键值对的持有和对比策略进行初始化,我们来看下这个NSPointerFunctionsOptions位移枚举值代表的意思

    1. NSPointerFunctionsStrongMemory 创建了一个retain/release对象的集合,非常像常规的NSSet或NSArray。
    2. NSPointerFunctionsWeakMemory使用等价的__weak来存储对象并自动移除被销毁的对象。
    3. NSPointerFunctionsCopyIn在对象被加入到集合前拷贝它们。
      NSPointerFunctionsObjectPersonality使用对象的hash和isEqual 来确定相等性。
    4. NSPointerFunctionsObjectPointerPersonality 使用哈希值的移位指针和直接比较指针来确定相等性

    框架中考虑到用对象作为key , copy对象的话性能消耗比较大,因此采用了NSMapTable容器的方案

    FVKVOController
    属性介绍

    //上层观察者,最终处理回调的对象
    @property (nullable, nonatomic, weak, readonly) id observer;
    //存放被观察对象及属性的数据 (类似字典, 到时其存储的键值关系为 [object :  NSMutableSet<_FBKVOInfo *> *>]
    NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
    //线程锁 ,解决多线程,资源竞争问题
    pthread_mutex_t _lock;
    

    方法介绍

    初始化方法

    
    // 参数retainObserved  : 是否强引用被观察的对象。默认是 YES 强引用被观察者 , 但是 ,如果你的被观察者对象对观察者对象有强引用关系的话,必须要 NO 不然会引起 retain cycle ,从而导致内存泄漏
    
    - (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
    {
      self = [super init];
      if (nil != self) {
    
        //这里是弱引用
        _observer = observer; 
        
        // keypotions 用了 强引用 + 指针比较取值的策略来管理这个map容器
        // valueOptions用了  强引用 + 指针比较
        NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
        _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
        pthread_mutex_init(&_lock, NULL);
      }
      return self;
    }
    
    

    添加观察者方法 - (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block 主要是 创建一个 FBKVOInfo 类来记录要用到的信息 ,我们来看下这个的成员变量

    __weak FBKVOController *_controller; //对应的KVOController
      NSString *_keyPath; //要监听的keypath
      NSKeyValueObservingOptions _options;//监听的option
      SEL _action; //最终属性改变的回调方法
      void *_context;//你自己定义回调时传进的context
      FBKVONotificationBlock _block; //最终属性改变的代码块
      _FBKVOInfoState _state;//监听信息的状态
    

    然后把这个info传进来下面这个方法

    - (void)_observe:(id)object info:(_FBKVOInfo *)info
    {
      // lock
      pthread_mutex_lock(&_lock);
    
    //重map中取出object 对应监听的kepath容器
      NSMutableSet *infos = [_objectInfosMap objectForKey:object];
    
      // 检测是否已经监听object 的kepath
      _FBKVOInfo *existingInfo = [infos member:info];
      if (nil != existingInfo) { //如果监听过就直接返回了 ,避免重复监听
        pthread_mutex_unlock(&_lock); //解锁
        return;
      }
    
      // lazilly create set of infos
      if (nil == infos) { //这里是从来没有监听过object的任何对象 
        infos = [NSMutableSet set];
        [_objectInfosMap setObject:infos forKey:object];
      }
    
    //把对应kepath的info加入到 object对应的 infos容器(NSMutableSet)中
      // add info and oberve
      [infos addObject:info];
    
      // unlock prior to callout
      pthread_mutex_unlock(&_lock);
    
    //请看下面对该类的介绍
      [[_FBKVOSharedController sharedController] observe:object info:info];
    }
    

    _FBKVOSharedController这个就是最终执行addobserve的类。我们先看下它的的两个成员变量

    NSHashTable<_FBKVOInfo *> *_infos; //存放所用使用框架添加kvoInfo
     pthread_mutex_t _mutex; //线程所锁
    

    NSHashTableNSSet的区别 跟 NSMapTableNSDictority 的区别相似,就是对对象的持有策略和比较对象的相等性可以自定义。

    添加监听的方法实现 , 很简单,就是判断上面传过来的info为非空时 就把其加入到 _infos容器中记录下来,并调用系统添加kvo的方法吧自己作为监听回调者来处理属性改变的回调,并设置info的状态 。最后 把info作为context传给监听方法

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

    收到被观察者属性改变是的回调实现 : 完成转发回你的自己想收到监听的对象

    - (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
        pthread_mutex_lock(&_mutex);
        info = [_infos member:(__bridge id)context]; //判断是否是曾经添加的有效的info (未被移除监听的info)
        pthread_mutex_unlock(&_mutex);
      }
    
      if (nil != info) { //有效的info
     
        // 这个就是我们最初强引用的 FBKVOController , 如果你要最终要收到监听的对象(持有 FBKVOController的对象)销毁了 , 这里就会直接过滤调不处理了,这也是框架安全转发kvo处理的原因。
        FBKVOController *controller = info->_controller; 
        if (nil != controller) {
          // 真正处理kvo回调的对象。weak
          id observer = controller.observer;
          if (nil != observer) {
            //转发监听处理
            if (info->_block) { //执行你自己传进的block
              NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
                
              //重组键值对数据 加入@{ FBKVONotificationKeyPathKey : keyPath }
              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) { //执行你自己定义的监听回调sel
    #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 的系统回调方法 
              [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
            }
          }
        }
      }
    }
    
    

    移除监听实现
    简单看下移除监听的实现,在FBKVOControllerinfos中移除掉对应的[ objctkey(被观察者)value(infos)], 或者移除移除infos中的某个对应项(单独移除某个kepath),最终会调用到_FBKVOSharedController 中 让其把 _infos里的每个info移除,并移除系统的监听关系[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];

    _FBKVOInfo 重写了 isEqualhash方法是为了 给NSHashTable的判别相等性提供依据。

    关于 hash的重写意义可以看下这边文章的介绍

    相关文章

      网友评论

        本文标题:FBKVOViewController 是怎样帮我们优雅、安全的

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