FBKVOController源碼剖析與學(xué)習(xí)

建議查看原文:http://www.reibang.com/p/4a3f9fe13e5a(不定時更新)

源碼剖析學(xué)習(xí)系列:(不斷更新)

1拐辽、FBKVOController源碼剖析與學(xué)習(xí)
2沐序、MJRefresh源碼剖析與學(xué)習(xí)
3、YYImage源碼剖析與學(xué)習(xí)


FBKVOController是對KVO的封裝,本文會分為兩大部分:

一打颤、針對FBKVOController進(jìn)行源碼解讀生蚁,剖析其封裝思路

二嗤放、針對源碼档悠,抽取其精要廊鸥,模仿學(xué)習(xí),變?yōu)榧河?/p>

優(yōu)勢

相對于原生 API 優(yōu)勢

1辖所、可以以數(shù)組形式惰说,同時對 model 的多個 不同成員變量進(jìn)行 KVO。

2缘回、利用提供的 block吆视,將 KVO 相關(guān)代碼集中在一塊,而不是四處散落切诀。比較清晰揩环,一目了然搔弄。

3幅虑、不需要在 dealloc 方法里取消對 object 的觀察,當(dāng) FBKVOController 對象 dealloc顾犹,會自動取消觀察倒庵。

使用

//1、在當(dāng)前類創(chuàng)建一個KVO的控制器炫刷,并且指明監(jiān)聽者為當(dāng)前類
// create KVO controller with observer
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;

//2擎宝、監(jiān)聽對象
// observe clock date property
[self.KVOController observe:clock keyPath:@"date" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(ClockView *clockView, Clock *clock, NSDictionary *change) {
  // 更新UI
  // update clock view with new value
  clockView.date = change[NSKeyValueChangeNewKey];
}];

使用步驟很簡短,我們關(guān)鍵是理解里面的封裝浑玛。

FBKVOController

一绍申、我們先看一下創(chuàng)建KVO controller實(shí)例的方法,以及銷毀方法--(生命周期)

#pragma mark Lifecycle - 
//1、
+ (instancetype)controllerWithObserver:(nullable id)observer
{
  return [[self alloc] initWithObserver:observer];
}
//2极阅、初始化observer胃碾,并依據(jù)retainObserved值決定內(nèi)存策略
- (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;
}
//3、
- (instancetype)initWithObserver:(nullable id)observer
{
  return [self initWithObserver:observer retainObserved:YES];
}
//4筋搏、在dealloc注銷所有監(jiān)聽并且銷毀上面的互斥鎖
- (void)dealloc
{
  [self unobserveAll];
  pthread_mutex_destroy(&_lock);
}

總結(jié):1仆百、NSPointerFunctionsStrongMemory創(chuàng)建了一個retain/release對象的集合,非常像常規(guī)的NSSet或NSArray奔脐。
NSPointerFunctionsWeakMemory使用等價的__weak來存儲對象并自動移除被銷毀的對象俄周。
2、比較陌生的是 NSMapTable 髓迎。簡單來說峦朗,它與 NSDictionary 類似。不同之處是 NSMapTable 可以自主控制 key / value 的內(nèi)存管理策略排龄。而 NSDictionary 的內(nèi)存策略是固定為 copy甚垦。當(dāng) key 為 object 時, copy 的開銷可能比較大涣雕!因此艰亮,在這里只能使用相對比較靈活的 NSMapTable。具體可以移步關(guān)于 NSMapTable

3挣郭、pthread_mutex:這是一種超級易用的互斥鎖迄埃,使用的時候,只需要初始化一個 pthread_mutex_t兑障,用 pthread_mutex_lock 來鎖定侄非, pthread_mutex_unlock 來解鎖,當(dāng)使用完成后流译,記得調(diào)用 pthread_mutex_destroy 來銷毀鎖

二逞怨、接下來看一下注冊監(jiān)聽對象的方法

- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
  if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
  }
  //創(chuàng)建FBKVOInfo
  // create info
  _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
  
  //利用FBKVOInfo觀察對象
  // observe object with info
  [self _observe:object info:info];
}

- (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)keyPaths options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
  NSAssert(0 != keyPaths.count && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPaths, block);
  if (nil == object || 0 == keyPaths.count || NULL == block) {
    return;
  }
//遍歷每個keyPath,再遞歸
  for (NSString *keyPath in keyPaths) {
    [self observe:object keyPath:keyPath options:options block:block];
  }
}

使用斷言,提示用戶缺少必要參數(shù);
為了避免保留循環(huán)福澡,該block必須避免引用KVO控制器或其所有者叠赦。觀察已經(jīng)觀察到的對象keyPath或nil的結(jié)果是沒有操作的。

看一下FBKVOInfo的init方法

- (instancetype)initWithController:(FBKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable FBKVONotificationBlock)block
                            action:(nullable SEL)action
                           context:(nullable void *)context
{
  self = [super init];
  if (nil != self) {
    _controller = controller;
    _block = [block copy];
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
  }
  return self;
}

重寫init方法革砸,把值分別賦值給屬性除秀,對于為什么要if (nil != self),我認(rèn)為算利,當(dāng)應(yīng)用程序在更有限的內(nèi)存中運(yùn)行册踩,這是一個傳統(tǒng)的編碼建議。具體請看各位大神的回答--> In Objective-C why should I check if self = [super init] is not nil?

看一下觀察FBInfo的方法

- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
  // lock
  pthread_mutex_lock(&_lock);
//1
  NSMutableSet *infos = [_objectInfosMap objectForKey:object];
//2
  // 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;
  }
//3
  // 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);
//4
  [[_FBKVOSharedController sharedController] observe:object info:info];
}

NSMutableSet是一個集合效拭,它有幾個特點(diǎn):
1暂吉、沒有順序胖秒,所有元素并非按照加入順序排列
2、重復(fù)元素只會添加一個慕的,因此不用擔(dān)心里面的元素有重復(fù)
NSMapTable是比Dicitionary更強(qiáng)大的一個類扒怖。我們定義一個Person類,用來記錄人名业稼,我們再創(chuàng)建一個Favourite類用來創(chuàng)建愛好對象盗痒,現(xiàn)在有Rose和Jack兩個人,分別的愛好是ObjC和Swift低散,人和愛好必須要用對象實(shí)現(xiàn)俯邓,而且必須關(guān)聯(lián)起來在一個表里,以便我們進(jìn)行查詢和記錄熔号。如果是以前的話需要自己建立一個Dictionary稽鞭,把人名的name字段作為key,favourite的對象作為value引镊。但是這樣有一個問題朦蕴,如果突然某一天,我Person里面增加了個字段age弟头,我這個表還要記錄每個人的年齡吩抓,供我以后來查詢不同年齡段的人統(tǒng)計使用呢?這下就很尷尬了赴恨,因?yàn)镈icitionary沒辦法實(shí)現(xiàn)我們要的這個效果疹娶,不過沒關(guān)系NSMapTable可以實(shí)現(xiàn),詳細(xì)請移步關(guān)于 NSMapTable

1伦连、根據(jù)被觀察的object獲取其對應(yīng)的infos set雨饺。這個主要作用在于避免多次對同一個keyPath添加多次觀察,避免crash惑淳。因?yàn)槊空{(diào)用一次addObserverForKeyPath就要有一個對應(yīng)的removeObserverForKey额港。

2、從infos set判斷是不是已經(jīng)觀察此次info了歧焦,避免重復(fù)觀察移斩。

3、如果infos為空倚舀,就把object當(dāng)做Key叹哭、infos當(dāng)做Object存入 NSMapTable,[infos addObject:info];再把info與infos關(guān)聯(lián)起來忍宋。這里聽起來可能有點(diǎn)別扭痕貌,我做個比喻:object是上面所說的是Rose,infos愛好ObjC糠排,而info則是他的age

4舵稠、使用了單例,將觀察的信息及關(guān)系注冊到_FBKVOSharedController中,并且調(diào)用iOS自帶的KVO方法觀察

_FBKVOSharedController作為一個傳達(dá)者哺徊,用來接收和轉(zhuǎn)發(fā)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);
  //1
  // 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];
  }
}

根據(jù)info的狀態(tài)來選擇添加或移除觀察者

1室琢、代表所有的觀察信息都首先由FBKVOSharedController進(jìn)行接收,隨后進(jìn)行轉(zhuǎn)發(fā)落追。

//當(dāng)屬性的值發(fā)生變化時盈滴,自動調(diào)用此系統(tǒng)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
    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];
            [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];
        }
      }
    }
  }
}

根據(jù)info的block回調(diào)或者actioin等等進(jìn)行消息轉(zhuǎn)發(fā)。

至此轿钠,對FBKVOController的源碼剖析基本結(jié)束巢钓,下面是剖析后的學(xué)習(xí)

學(xué)習(xí)

1、NSHashTable
+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    //1疗垛、待會替換
    person.family = [[NSMutableArray alloc] init];
    [person.family addObject:person];
    return [person autorelease];
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"%@'s retainCount is %lu",self.name,[self retainCount]];
}
- (void)dealloc
{
    self.name = nil;
    self.family = nil;
    [super dealloc];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        DWPerson *person_1 = [DWPerson personWithName:@"iOS"];
        DWPerson *person_2 = [DWPerson personWithName:@"swift"];
        DWPerson *person_3 = [DWPerson personWithName:@"android"];
        DWPerson *person_4 = [DWPerson personWithName:@"java"];
        DWPerson *person_5 = [DWPerson personWithName:@"ruby"];
        
        id list = @[person_1, person_2, person_3, person_4, person_5];
        NSLog(@"%@",list);

    }
    return 0;
}

打又⑿凇:

(
"iOS's retainCount is 3",
"swift's retainCount is 3",
"android's retainCount is 3",
"java's retainCount is 3",
"ruby's retainCount is 3"
)

可以看出每個person的retainCount為3,因?yàn)閒amily持有person,person持有family贷腕,如果我們運(yùn)用NSHashTable背镇,則可以完美解決此問題

我們替換1中的代碼,

+ (instancetype)personWithName:(NSString *)name
{
    DWPerson *person = [[DWPerson alloc] init];
    person.name = name;
    person.family = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
    [person.family addObject:person];
    return [person autorelease];
}

打印:

(
"iOS's retainCount is 2",
"swift's retainCount is 2",
"android's retainCount is 2",
"java's retainCount is 2",
"ruby's retainCount is 2"

)
可看出,已解決循環(huán)引用

2泽裳、宏定義魔法

先看一下系統(tǒng)的KVO方法

[testPerson addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];

這樣寫keyPath瞒斩,如果age屬性不存在,也不會告知涮总,導(dǎo)致后續(xù)的排查困難济瓢,但這種低級錯誤在FBKVOController不復(fù)存在,因?yàn)槠涫褂昧撕甓x

FBKVOController中的宏定義

#define FBKVOKeyPath(KEYPATH) \
@(((void)(NO && ((void)KEYPATH, NO)), \
({ const char *fbkvokeypath = strchr(#KEYPATH, '.'); NSCAssert(fbkvokeypath, @"Provided key path is invalid."); fbkvokeypath + 1; })))

#define FBKVOClassKeyPath(CLASS, KEYPATH) \
@(((void)(NO && ((void)((CLASS *)(nil)).KEYPATH, NO)), #KEYPATH))

該宏定義使用了C語言的逗號表達(dá)式妹卿,(3+5,6+8)稱為逗號表達(dá)式旺矾,其求解過程先表達(dá)式1,后表達(dá)式2夺克,整個表達(dá)式值是表達(dá)式2的值箕宙,如:(3+5,6+8)的值是14铺纽,a=(a=3 x 5,a x 4)的值是60柬帕,而(a=3 x 5,a x 4)的值是60, a的值是15狡门。

使用逗號表達(dá)式陷寝,我覺得主要是為了FBKVOClassKeyPath

FBKVOClassKeyPath(DWPerson, name)==(((void)(NO && ((void)((DWPerson *)(nil)).name, NO)), #KEYPATH)),其會檢查DWPerson中是否有name屬性

3其馏、自釋放

FBKVOController通過自釋放的機(jī)制來實(shí)現(xiàn)observer的自動移除凤跑,其實(shí)就是給observer的類中添加一個FBKVOController的成員變量,然后在FBKVOController中的dealloc移除observer叛复,下面是個例子

#import "DWTestViewController.h"
#import "DWObserViewController.h"

@interface DWTestViewController ()
@property (nonatomic, strong) DWObserViewController *obserVC;
@end

@implementation DWTestViewController

- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        _obserVC = [[DWObserViewController alloc] init];
        NSLog(@"DWTestVC創(chuàng)建");
        NSLog(@"DWObserVC創(chuàng)建");
    }
    return self;
}
#import "DWObserViewController.h"

@implementation DWObserViewController

- (void)dealloc {
    NSLog(@"DWObserVC跟著銷毀");
}

打印:

2018-02-05 15:32:39.299859+0800 FBKVOController_Demo[6804:208216] DWTestVC創(chuàng)建
2018-02-05 15:32:39.300209+0800 FBKVOController_Demo[6804:208216] DWObserVC創(chuàng)建
2018-02-05 15:32:41.271585+0800 FBKVOController_Demo[6804:208216] DWTestVC銷毀
2018-02-05 15:32:46.520148+0800 FBKVOController_Demo[6804:208216] DWObserVC跟著銷毀

參考:

NSHashTable和NSMapTable用法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仔引,一起剝皮案震驚了整個濱河市扔仓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咖耘,老刑警劉巖翘簇,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異儿倒,居然都是意外死亡版保,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門夫否,熙熙樓的掌柜王于貴愁眉苦臉地迎上來找筝,“玉大人,你說我怎么就攤上這事慷吊⌒湓#” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵溉瓶,是天一觀的道長急鳄。 經(jīng)常有香客問我,道長堰酿,這世上最難降的妖魔是什么疾宏? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮触创,結(jié)果婚禮上坎藐,老公的妹妹穿的比我還像新娘。我一直安慰自己哼绑,他們只是感情好岩馍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抖韩,像睡著了一般蛀恩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上茂浮,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天双谆,我揣著相機(jī)與錄音,去河邊找鬼席揽。 笑死顽馋,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幌羞。 我是一名探鬼主播寸谜,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼新翎!你這毒婦竟也來了程帕?” 一聲冷哼從身側(cè)響起住练,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤地啰,失蹤者是張志新(化名)和其女友劉穎愁拭,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亏吝,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岭埠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蔚鸥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惜论。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖止喷,靈堂內(nèi)的尸體忽然破棺而出馆类,到底是詐尸還是另有隱情,我是刑警寧澤弹谁,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布乾巧,位于F島的核電站,受9級特大地震影響预愤,放射性物質(zhì)發(fā)生泄漏沟于。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一植康、第九天 我趴在偏房一處隱蔽的房頂上張望旷太。 院中可真熱鬧,春花似錦销睁、人聲如沸供璧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗜傅。三九已至,卻和暖如春檩赢,著一層夾襖步出監(jiān)牢的瞬間吕嘀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工贞瞒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留偶房,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓军浆,卻偏偏與公主長得像棕洋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子乒融,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355