iOS开发-知识合集第三方源码解析iOS - KVO

教你一行代码使用 KVO(Facebook 出品 FBKVOCo

2017-07-11  本文已影响1010人  郑一一一一

前言

进入 iOS 开发一年多,大部分时间都在写业务代码,鲜有对优秀开源代码的学习、总结。深知,是时候开始学习一些。万事开头难,所以我准备从比较简短的开源代码开始学习。第一篇准备写写 Facebook 这个极度热爱开源的公司的一套关于 KVO 的开源代码——FBKVOController。阅读本篇文章前,希望你对 KVO 已经有一定的了解。

正文

先说说本文主要想讲一下哪些东西。

概述

FBKVOController 做了什么?

简单来说,Facebook 开源的这套代码,主要是对我们经常使用的 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 优势:

源码解析

这套源代码主要包括了FBKVOController.hFBKVOController.mNSObject+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];
}

上述代码中,出现了一个前面提及到的 _FBKVOInfo 类,其存储的信息包括了 FBKVOControllerkeypathoptionsblock

接上段代码的最后一句 [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];
}

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

FBKVOController.png
追踪一下这句代码
[[_FBKVOSharedController sharedController] observe:object info:info];

_FBKVOSharedController 是会在 app 生命周期一直存在的单例,其职责是:接收并转发 KVO 通知。因此 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];
  }
}

以上代码中想单独说一下下面的代码,其中的 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];
        }
      }
    }
  }
}

设计思路总结

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

小尾巴

第一次写源码解析,感觉思路都还比较混乱,认识也还比较浅薄,需要逐渐摸索一下。有什么问题欢迎提给我!

一些相关知识的链接
NSHashTable的特性和使用
互斥锁

上一篇 下一篇

猜你喜欢

热点阅读