教你一行代码使用 KVO(Facebook 出品 FBKVOController 源码使用及解读)

前言

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

正文

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

概述

  • FBKVOController 做了什么
  • FBKVOController 使用姿势
  • FBKVOController 源码解析
  • FBKVOController 设计思路总结
  • FBKVOController 其它收获

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 优势:

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

源码解析

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

设计思路总结

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

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

  • 1 NSSet / NSHashTableNSDictionary/ NSMapTable 的学习
    • NSSet 是过滤掉重复 object 的集合类,NSHashTableNSSet 的升级版容器,并且只有可变版本,允许对添加到容器中的对象是弱引用的持有关系, 当NSHashTable 中的对象销毁时,该对象也会从容器中移除。
    • NSMapTableNSDictionary 类似,唯一区别是多了个功能:可以设置 keyvalueNSPointerFunctionsOptions 特性! NSDictionarykey 策略固定是 copy,考虑到开销问题,一般使用简单的数字或者字符串为 key。但是如果碰到需要用 object 作为 key 的应用场景呢?NSMapTable 就可以派上用场了!可以通过 NSFunctionsPointer 来分别定义对 keyvalue 的内存管理策略,简单可以分为 strong,weak以及 copy
  • 2 几个比较有用的宏
    • NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END,如果需要每个属性或每个方法都去指定 nonnullnullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了这两个宏。在这两个宏之间的代码,所有比较简单指针对象都被假定为 nonnull,因此我们只需要去指定那些 nullable 的指针。如果我们强行通过点语法将一个非空指针置空,编译器会报 warning
    • NS_UNAVAILABLE 当我们不想要其他开发人员,用普通的 init 方法去初始化一个类,我们可以在.h 文件里这样写:
      - (instancetype)init NS_UNAVAILABLE;
      编译器不但不会提示补全 init 方法,就算开发人员强制发送 init 消息,编译器会直接报错。
    • NS_DESIGNATED_INITIALIZER 指定的初始化方法。当一个类提供多种初始化方法时,所有的初始化方法最终都会调用这个指定的初始化方法。比较常见的有:
      - (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
  • 3 断言的使用
    NSAssert(x,y);xBOOL 值,y 为 字符串类型。当 x = YES,则不产生断言。当 x = NO,则产生断言,app 会 crash,并在控制台中打印 y 字符串内容。合理利用断言,可以保证 app 的健壮性。
  • 4 互斥锁的使用
    • pthread_mutex_init(&_lock, NULL);(初始化)&_lock 是互斥锁的指针,第二个参数是互斥锁的属性。缺省值是:当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
    • pthread_mutex_destroy(&_lock);(销毁)
    • pthread_mutex_lock(&_lock);(加锁)
    • pthread_mutex_unlock(&_lock);(解锁)
    • 涉及到数据的读写操作时,都需要加锁来保证避免数据竞争。
    • 顺便复习一下死锁的概念:如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。

小尾巴

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容