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

前言

進(jìn)入 iOS 開發(fā)一年多拉背,大部分時間都在寫業(yè)務(wù)代碼师崎,鮮有對優(yōu)秀開源代碼的學(xué)習(xí)、總結(jié)椅棺。深知犁罩,是時候開始學(xué)習(xí)一些。萬事開頭難两疚,所以我準(zhǔn)備從比較簡短的開源代碼開始學(xué)習(xí)床估。第一篇準(zhǔn)備寫寫 Facebook 這個極度熱愛開源的公司的一套關(guān)于 KVO 的開源代碼——FBKVOController。閱讀本篇文章前诱渤,希望你對 KVO 已經(jīng)有一定的了解丐巫。

正文

先說說本文主要想講一下哪些東西。

概述

  • FBKVOController 做了什么
  • FBKVOController 使用姿勢
  • FBKVOController 源碼解析
  • FBKVOController 設(shè)計思路總結(jié)
  • FBKVOController 其它收獲

FBKVOController 做了什么勺美?

簡單來說递胧,Facebook 開源的這套代碼,主要是對我們經(jīng)常使用的 KVO 機(jī)制進(jìn)行了額外的一層封裝赡茸。其中最亮眼的特色是提供了一個 block 回調(diào)讓我們進(jìn)行處理缎脾,避免 KVO 的相關(guān)代碼四處散落,不再需要使用下面這個方法:


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

使用姿勢

利用開源框架占卧,我們這樣實現(xiàn)遗菠,其中第二種方法可以用一行代碼實現(xiàn) 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];

    //創(chuàng)建被觀察的 model 類
    KVOModel *model = [[KVOModel alloc] init];
    //初始化設(shè)置 model 的成員變量值
    model.name = @"wo";
    model.age = 5;
    self.model = model;
    
    //第一種方法:創(chuàng)建 FBKVOController 對象联喘,并被 VC 強(qiáng)引用,否則出了當(dāng)前作用域辙纬,就會被銷毀
    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]);
    }];

    //第二種方法:無需主動創(chuàng)建 FBKVOController 對象豁遭,self.KVOController 直接懶加載創(chuàng)建FBKVOController 對象
    //可以直接對某個對象的多個成員變量執(zhí)行 KVO
    //------真正實現(xiàn)一行代碼搞定 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 優(yōu)勢:

  • 1 可以以數(shù)組形式,同時對 model 的多個 不同成員變量進(jìn)行 KVO贺拣。
  • 2 利用提供的 block蓖谢,將 KVO 相關(guān)代碼集中在一塊,而不是四處散落纵柿。比較清晰蜈抓,一目了然。
  • 3 不需要在 dealloc 方法里取消對 object 的觀察昂儒,當(dāng) FBKVOController 對象 dealloc沟使,會自動取消觀察。

源碼解析

這套源代碼主要包括了FBKVOController.h渊跋、FBKVOController.m腊嗡、NSObject+FBKVOController.hNSObject+FBKVOController.m四個文件拾酝。
其中燕少,NSObject+FBKVOController 這個分類比較簡單。它主要干的事是通過 objc_setAssociatedObject (關(guān)聯(lián)對象)蒿囤,以懶加載的形式給 NSObject 客们,創(chuàng)建并關(guān)聯(lián)一個 FBKVOController 的對象。
接下來材诽,我會著重介紹一下今天的主角 FBKVOController類底挫。其文件中還包含另外兩個類,_FBKVOInfo脸侥、_FBKVOSharedController 建邓。下面都會介紹到。
先來看看 FBKVOController 指定初始化函數(shù):


- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    //一般情況下 observer 會持有 FBKVOController 為了避免循環(huán)引用睁枕,此處的_observer 的內(nèi)存管理語義是弱引用
    _observer = observer;
    //定義 NSMapTable key的內(nèi)存管理策略官边,在默認(rèn)情況,傳入的參數(shù) retainObserved = YES
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    //創(chuàng)建 NSMapTable  key 為 id 類型外遇,value 為 NSMutableSet<_FBKVOInfo *> 類型
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    //初始化互斥鎖注簿,避免多線程間的數(shù)據(jù)競爭
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

以上初始化代碼中,注釋都寫得比較清楚了跳仿。唯一比較陌生的是 NSMapTable 诡渴。簡單來說,它與 NSDictionary 類似塔嬉。不同之處是 NSMapTable 可以自主控制 key / value 的內(nèi)存管理策略玩徊。而 NSDictionary 的內(nèi)存策略是固定為 copy。當(dāng) key 為 object 時谨究, copy的開銷可能比較大恩袱!因此,在這里只能使用相對比較靈活的 NSMapTable胶哲。

執(zhí)行 KVO 的相關(guān)方法代碼解析


- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
    //當(dāng) keyPath 字符串長度為 0 或者 block 為空時畔塔,會產(chǎn)生斷言,程序會 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 (利用存儲的信息對 “被觀察對象” 進(jìn)行觀察3憾帧)
  [self _observe:object info:info];
}

上述代碼中,出現(xiàn)了一個前面提及到的 _FBKVOInfo 類寄摆,其存儲的信息包括了 FBKVOController谅辣、keypathoptions婶恼、block桑阶。

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


- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock 互斥鎖加鎖
  pthread_mutex_lock(&_lock);
  //還記得初始化 FBKVOController 時創(chuàng)建的 NSMapTable 么?
  //其結(jié)構(gòu)是以 被觀察者 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;
  }

    //如果沒有 關(guān)于這個 object(被觀察者)的相關(guān)信息眷篇,則創(chuàng)建 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 是 干嘛的?  將所有觀察信息統(tǒng)一交由一個單例來完成
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

總結(jié)一下上面一段的數(shù)據(jù)結(jié)構(gòu)蕉饼。FBKVOController 擁有成員變量 NSMapTable虐杯,NSMapTable被觀察者(object)為 key, NSMutableSet 為 value 椎椰。在 NSMutableSet 中厦幅,存儲了不同 info。其關(guān)系圖如下圖:

FBKVOController.png

追蹤一下這句代碼

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

_FBKVOSharedController 是會在 app 生命周期一直存在的單例慨飘,其職責(zé)是:接收并轉(zhuǎn)發(fā) KVO 通知确憨。因此 app 當(dāng)中所有 KVO 的通知都是由這個單例來完成的。


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

  // register info   向 NSHashTable 添加 info
  //注意:在 _FBKVOController 類中的 NSMutableSet 已經(jīng)強(qiáng)引用了 info
  //這里是為了弱引用 info瓤的,才使用 NSHashTable休弃,當(dāng) info dealloc 時,同時會從容器中刪除
  pthread_mutex_lock(&_mutex);
  [_infos addObject:info];
  pthread_mutex_unlock(&_mutex);

    //_FBKVOSharedController 是實際的觀察者圈膏! 隨后會進(jìn)行轉(zhuǎn)發(fā) 塔猾,
   //context 是 void * 無類型指針,是 info 的指針稽坤!
  [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];
    //如果 state 是原始狀態(tài)丈甸,則改為正在觀察的狀態(tài)糯俗,表明是在正在觀察的狀態(tài)
  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 參數(shù)使用的是 (void *)info 的指針睦擂,這樣可以保證 context 的唯一性得湘。

接收 KVO 通知,并做相應(yīng)處理


- (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  * 轉(zhuǎn)換為 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 {
            //默認(rèn)情況 調(diào)用觀察者的原生函數(shù)J瞿拧惩淳!
          [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
        }
      }
    }
  }
}

設(shè)計思路總結(jié)

  • 1 FBKVOController 持有 NSMapTable,以 objectkey 得到相對應(yīng)的 NSMutableSet乓搬。NSMutableSet 中存儲了不同的 _FBKVOInfo黎泣。這套數(shù)據(jù)結(jié)構(gòu)的主要作用是防止開發(fā)人員重復(fù)添加相同的 KVO。當(dāng)檢查到其中已存在相同的 _FBKVOInfo 對象時缤谎,不再執(zhí)行后面的代碼抒倚。
  • 2 _FBKVOSharedController 持有 NSHashTableNSHashTable 以弱引用的方式持有不同的 _FBKVOInfo坷澡。此處實際執(zhí)行 KVO 代碼托呕。_FBKVOInfo 有一個重要的成員變量 _FBKVOInfoState,根據(jù)這個枚舉值(_FBKVOInfoStateInitial频敛、_FBKVOInfoStateObserving项郊、_FBKVOInfoStateNotObserving) 來決定新增或者刪除 KVO

收獲(通讀斟赚、研究源代碼后)

  • 1 NSSet / NSHashTable 着降、NSDictionary/ NSMapTable 的學(xué)習(xí)
    • NSSet 是過濾掉重復(fù) object 的集合類,NSHashTableNSSet 的升級版容器拗军,并且只有可變版本任洞,允許對添加到容器中的對象是弱引用的持有關(guān)系, 當(dāng)NSHashTable 中的對象銷毀時发侵,該對象也會從容器中移除交掏。
    • NSMapTableNSDictionary 類似,唯一區(qū)別是多了個功能:可以設(shè)置 keyvalueNSPointerFunctionsOptions 特性! NSDictionarykey 策略固定是 copy刃鳄,考慮到開銷問題盅弛,一般使用簡單的數(shù)字或者字符串為 key。但是如果碰到需要用 object 作為 key 的應(yīng)用場景呢?NSMapTable 就可以派上用場了挪鹏!可以通過 NSFunctionsPointer 來分別定義對 keyvalue 的內(nèi)存管理策略见秽,簡單可以分為 strong,weak以及 copy
  • 2 幾個比較有用的宏
    • NS_ASSUME_NONNULL_BEGIN讨盒、NS_ASSUME_NONNULL_END张吉,如果需要每個屬性或每個方法都去指定 nonnullnullable,是一件非常繁瑣的事催植。蘋果為了減輕我們的工作量,專門提供了這兩個宏勺择。在這兩個宏之間的代碼创南,所有比較簡單指針對象都被假定為 nonnull,因此我們只需要去指定那些 nullable 的指針省核。如果我們強(qiáng)行通過點語法將一個非空指針置空稿辙,編譯器會報 warning
    • NS_UNAVAILABLE 當(dāng)我們不想要其他開發(fā)人員气忠,用普通的 init 方法去初始化一個類邻储,我們可以在.h 文件里這樣寫:
      - (instancetype)init NS_UNAVAILABLE;
      編譯器不但不會提示補(bǔ)全 init 方法,就算開發(fā)人員強(qiáng)制發(fā)送 init 消息旧噪,編譯器會直接報錯吨娜。
    • NS_DESIGNATED_INITIALIZER 指定的初始化方法。當(dāng)一個類提供多種初始化方法時淘钟,所有的初始化方法最終都會調(diào)用這個指定的初始化方法宦赠。比較常見的有:
      - (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
  • 3 斷言的使用
    NSAssert(x,y);xBOOL 值,y 為 字符串類型米母。當(dāng) x = YES勾扭,則不產(chǎn)生斷言。當(dāng) x = NO铁瞒,則產(chǎn)生斷言妙色,app 會 crash,并在控制臺中打印 y 字符串內(nèi)容慧耍。合理利用斷言身辨,可以保證 app 的健壯性。
  • 4 互斥鎖的使用
    • pthread_mutex_init(&_lock, NULL);(初始化)&_lock 是互斥鎖的指針芍碧,第二個參數(shù)是互斥鎖的屬性栅表。缺省值是:當(dāng)一個線程加鎖以后,其余請求鎖的線程將形成一個等待隊列师枣,并在解鎖后按優(yōu)先級獲得鎖怪瓶。這種鎖策略保證了資源分配的公平性。
    • pthread_mutex_destroy(&_lock);(銷毀)
    • pthread_mutex_lock(&_lock);(加鎖)
    • pthread_mutex_unlock(&_lock);(解鎖)
    • 涉及到數(shù)據(jù)的讀寫操作時,都需要加鎖來保證避免數(shù)據(jù)競爭洗贰。
    • 順便復(fù)習(xí)一下死鎖的概念:如果線程A鎖住了記錄1并等待記錄2找岖,而線程B鎖住了記錄2并等待記錄1,這樣兩個線程就發(fā)生了死鎖現(xiàn)象敛滋。

小尾巴

第一次寫源碼解析许布,感覺思路都還比較混亂,認(rèn)識也還比較淺薄绎晃,需要逐漸摸索一下蜜唾。有什么問題歡迎提給我!

一些相關(guān)知識的鏈接
NSHashTable的特性和使用
互斥鎖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庶艾,一起剝皮案震驚了整個濱河市袁余,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咱揍,老刑警劉巖颖榜,帶你破解...
    沈念sama閱讀 221,695評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異煤裙,居然都是意外死亡掩完,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評論 3 399
  • 文/潘曉璐 我一進(jìn)店門硼砰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來且蓬,“玉大人,你說我怎么就攤上這事题翰∶迮保” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評論 0 360
  • 文/不壞的土叔 我叫張陵遍愿,是天一觀的道長存淫。 經(jīng)常有香客問我,道長沼填,這世上最難降的妖魔是什么桅咆? 我笑而不...
    開封第一講書人閱讀 59,648評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮坞笙,結(jié)果婚禮上岩饼,老公的妹妹穿的比我還像新娘。我一直安慰自己薛夜,他們只是感情好籍茧,可當(dāng)我...
    茶點故事閱讀 68,655評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著梯澜,像睡著了一般寞冯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,268評論 1 309
  • 那天吮龄,我揣著相機(jī)與錄音俭茧,去河邊找鬼。 笑死漓帚,一個胖子當(dāng)著我的面吹牛母债,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播尝抖,決...
    沈念sama閱讀 40,835評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼毡们,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了昧辽?” 一聲冷哼從身側(cè)響起衙熔,我...
    開封第一講書人閱讀 39,740評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奴迅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體挺据,經(jīng)...
    沈念sama閱讀 46,286評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡取具,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,375評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了扁耐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暇检。...
    茶點故事閱讀 40,505評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婉称,靈堂內(nèi)的尸體忽然破棺而出块仆,到底是詐尸還是另有隱情,我是刑警寧澤王暗,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布悔据,位于F島的核電站,受9級特大地震影響俗壹,放射性物質(zhì)發(fā)生泄漏科汗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,873評論 3 333
  • 文/蒙蒙 一绷雏、第九天 我趴在偏房一處隱蔽的房頂上張望头滔。 院中可真熱鬧,春花似錦涎显、人聲如沸坤检。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽早歇。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缺前,已是汗流浹背蛀醉。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留衅码,地道東北人拯刁。 一個月前我還...
    沈念sama閱讀 48,921評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像逝段,于是被迫代替她去往敵國和親垛玻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,515評論 2 359

推薦閱讀更多精彩內(nèi)容