FBKVOController詳解

前言

前段時間剛到公司,公司在使用FBKVOController,本人一直在使用系統的KVO,沒有使用過Facebook的這個框架,使用起來挺方便的,所以安利一波,并且讀讀源碼,本文只是略讀,了解了FBKVOController的結構和基本實現,可能他的設計思想還沒有深入理解,以后慢慢探討。

FBKVOController的使用

//
//  ViewController.m
//  FBKVOControllerDemo
//
//  Created by 李林 on 2017/5/17.
//  Copyright ? 2017年 lee. All rights reserved.
//

#import "ViewController.h"
#import <KVOController/KVOController.h>

@interface ViewController (){
    FBKVOController *_kvoCtrl;
}

@property (weak, nonatomic) IBOutlet UIButton *button;
@property (weak, nonatomic) IBOutlet UIView *colorView;
@property (nonatomic, assign) NSInteger index;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.index = 0;
    [self.button addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
    
    // FBKVOController
    _kvoCtrl = [FBKVOController controllerWithObserver:self];
    [_kvoCtrl observe:self keyPath:@"index" options:0 action:@selector(changeColor)];
}

- (void)buttonClick {
    self.index++;
}

- (void)changeColor {
    self.colorView.backgroundColor = [UIColor redColor];
}


@end

使用很簡單,監測某個對象的值,然后將selector寫入observe函數中,當值發生改變,就會調用通知的函數。效果如下圖。

FBKVOController.gif

代碼地址:https://github.com/lilianmao/FBKVOControllerDemo

源碼簡析

1. 系統KVO的問題

  1. 需要手動移除觀察者,且移除觀察者的時機必須合適;remove時機很重要
  2. 注冊觀察者的代碼和事件發生處的代碼上下文不同,傳遞上下文是通過 void * 指針;不懂
  3. 需要覆寫 -observeValueForKeyPath:ofObject:change:context: 方法,比較麻煩;覆蓋寫observeValueForKeyPath方法
  4. 在復雜的業務邏輯中,準確判斷被觀察者相對比較麻煩,有多個被觀測的對象和屬性時,需要在方法中寫大量的 if 進行判斷;當業務復雜的時候,覆蓋寫observeValueForKeyPath方法里有大量的if需要判斷

2. FBKVOController

在系統KVO存在很多問題的情況下,FBKVOController應運而生,這個KVOController是Facebook的開源框架,github地址

  1. 不需要手動移除觀察者;框架自動幫我們移除觀察者
  2. 實現 KVO 與事件發生處的代碼上下文相同,不需要跨方法傳參數;依舊不懂
  3. 使用 block 來替代方法能夠減少使用的復雜度,提升使用 KVO 的體驗;block或者selector的方式,方便使用
  4. 每一個 keyPath 會對應一個屬性,不需要在 block 中使用 if 判斷 keyPath;一個keyPath對應一個SEL或者block,不需要統一的observeValueForKeyPath方法里寫if判斷

3. 角色反轉

FBKVOController實現了觀察者和被觀察者的角色反轉,系統的KVO是被觀察者添加觀察者,而FBKVO實現了觀察者主動去添加被觀察者,實現了角色上的反轉,其實就是用的比較方便。

    // FBKVOController
    _kvoCtrl = [FBKVOController controllerWithObserver:self];
    [_kvoCtrl observe:_person keyPath:@"age" options:0 action:@selector(changeColor)];
    
    // 系統的KVO
    [_person addObserver:self
              forKeyPath:@"age"
                 options:NSKeyValueObservingOptionNew
                 context:nil];

4. KVOController的實現

4.1 結構圖

KVOProgress.png

4.2 NSObject分類和KVOController的初始化

給NSObject添加一個FBKVOController分類,用關聯對象動態添加屬性。

#import <Foundation/Foundation.h>

#import "FBKVOController.h"

@interface NSObject (FBKVOController)

@property (nonatomic, strong) FBKVOController *KVOController;
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;

@end

其中有一個是KVOControllerNonRetaining,防止循環引用。實現如下,要用weak指針。可能為了防止引起內存泄漏問題。

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
  self = [super init];
  if (nil != self) {
    _observer = observer;
    NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
    _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
    pthread_mutex_init(&_lock, NULL);
  }
  return self;
}

4.3 KVOController介紹

在KVOController.h文件,發現三個類:FBKVOInfo、FBKVOSharedController和FBKVOController。

  1. 其中FBKVOInfo主要是對需要觀測的信息的包裝,包含了action、block、options等等,改類中重寫了hash,isEqual等方法。_FBKVOInfo覆寫了-isEqual:方法用于對象之間的判等以及方便 NSMapTable 的存儲。
@implementation _FBKVOInfo
{
@public
  __weak FBKVOController *_controller;
  NSString *_keyPath;
  NSKeyValueObservingOptions _options;
  SEL _action;
  void *_context;
  FBKVONotificationBlock _block;
  _FBKVOInfoState _state;
}
  1. FBKVOController是核心類,包含MapTable和pthread_mutex_lock(貌似原來用的是OSSpinLock),其中_objectInfosMap是存儲一個對象對應的KVOInfo的映射關系,也就是說這里<id, NSMutableSet<_FBKVOInfo *> *> 中的id就是對象,
    MutableSet就是KVOInfos,各種鍵值觀測的包裝。
@implementation FBKVOController
{
  NSMapTable<id, NSMutableSet<_FBKVOInfo *> *> *_objectInfosMap;
  pthread_mutex_t _lock;
}

這張圖表現了就是每個被觀測者對象和KVOInfo的關系。

objectInfosMap.png
  1. FBKVOSharedController是一個實際操作類,負責將FBKVOController發送過來的信息轉發給系統的KVO處理。這里實現了KVO的觀測和remove。
@implementation _FBKVOSharedController
{
  NSHashTable<_FBKVOInfo *> *_infos;
  pthread_mutex_t _mutex;
}

4.4 KVO過程

我們從寫的例子開始走起,可以把FBKVOCOntroller的流程看一遍。
(PS:這也是我現在看框架原碼的方法)

[_kvoCtrl observe:self keyPath:@"index" options:0 action:@selector(changeColor)];

這個函數- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action是FBKVOController的,首先是斷言判斷是否為空,然后創造一個FBKVOInfo,最后調用本身的observe方法,將包裝的FBKVOInfo的info傳過去。

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options action:(SEL)action
{
  NSAssert(0 != keyPath.length && NULL != action, @"missing required parameters observe:%@ keyPath:%@ action:%@", object, keyPath, NSStringFromSelector(action));
  NSAssert([_observer respondsToSelector:action], @"%@ does not respond to %@", _observer, NSStringFromSelector(action));
  if (nil == object || 0 == keyPath.length || NULL == action) {
    return;
  }

  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options action:action];

  // observe object with info
  [self _observe:object info:info];
}

這個函數- (void)_observe:(id)object info:(_FBKVOInfo *)info是FBKVOController的,首先是加鎖,防止讀寫干擾。然后我們查找一下這個object對應的MutableSet,如果有對應的KVOInfo的話,那么就不需要再添加入_objectInfosMap中了;如果沒有,則創建info,并且加入_objectInfosMap中。最后解鎖,將object傳給_FBKVOSharedController處理。

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);

  NSMutableSet *infos = [_objectInfosMap objectForKey:object];

  // check for info existence
  _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;
  }

  // lazilly create set of infos
  if (nil == infos) {
    infos = [NSMutableSet set];
    [_objectInfosMap setObject:infos forKey:object];
  }

  // add info and oberve
  [infos addObject:info];

  // unlock prior to callout
  pthread_mutex_unlock(&_lock);

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

這個- (void)observe:(id)object info:(nullable _FBKVOInfo *)info函數應該是調用系統的addObserver,添加觀察者。其中[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];這句話就是系統的KVO觀測,object添加觀測者,這里添加的是FBKVOShare,最后KVO的相應函數也在這里,正好呼應。

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

這里是KVO的相應函數,有木有很熟悉?這里將當初我們傳入的一些action或者block執行以下,完美。

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSString *, 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];
    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<NSString *, 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];
            [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];
        }
      }
    }
  }

4.4 循環引用的分析

VC1持有_kvoCtrl,_kvoCtrl持有一個_objectInfosMap,這是一個可以存放弱指針的NSDictionary,這個函數[_objectInfosMap setObject:infos forKey:object];就是將object和其需要監聽的info加入map中。故VC1持有KVOCtrl,KVOCtrl持有map,map持有VC2,也就是VC1持有VC2。這是要如果我們VC2里觀測VC1,就會VC2持有VC1。造成循環引用。

致謝

本文主要參考的博客:
https://github.com/facebook/KVOController
https://github.com/Draveness/Analyze/blob/master/contents/KVOController/KVOController.md
http://chaosgsc.com/kvo.html
https://satanwoo.github.io/2016/02/27/FBKVOController/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • KVO 作為 iOS 中一種強大并且有效的機制,為 iOS 開發者們提供了很多的便利;我們可以使用 KVO 來檢測...
    JzRo閱讀 953評論 0 2
  • KVO 作為 iOS 中一種強大并且有效的機制,為 iOS 開發者們提供了很多的便利;我們可以使用 KVO 來檢測...
    Draveness閱讀 6,913評論 11 59
  • FBKVOController是一個簡單易用的鍵值觀察框架,KVOController 對于 Cocoa 中 KV...
    我有小尾巴快看閱讀 1,396評論 0 0
  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,755評論 0 9
  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放,涉及到進度條,當時做android時候處理的不太好,由于...
    DaZenD閱讀 3,034評論 0 26