美文网首页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