FBKVOController 源碼解析
來自: http://satanwoo.github.io/2016/02/27/FBKVOController/
開發過iOS的app已經不計其數了,在不同的項目中采用的架構也各不相同,有傳統的 MVC ,簡化的 VIPER ,以及一些簡單的 MVVM 。
這其中,我最不推薦的就是 VIPER ,誰寫誰知道,,絕對是增加了項目的復雜性。 MVVM 由于自己總是受限于傳統的 Object-Oriented 的思路,總是想不出真正的Functional Programming的代碼,因此,絕大多數情況,寫著寫著都回歸到了 MVC 。
其實,相較于網上大家總喜歡提到的 Massive View Controller 問題,我更想說的是這種傳統架構中對于信息流的不友好。
在一個典型的iOS的問題中,我們的代碼執行流程,通常都是從View Controller的 生命周期 開始,如果是一個完全基于順序執行的應用,那整個app的信息流是 單向可跟蹤的 。但是往往事情并不會那么簡單,我們會包含至少如下這些潛在打亂信息流的 壞蛋
- Delegate回調
- NSNotification
- UIView控件的Target-Action
- KVO
在這里,你可能會以為我想談談 ReactiveCocoa 和 RxSwift ,那你錯啦,那個開源項目我暫時還沒有能力去深究,所以我想從KVO事件入手,讀一讀非死book出品的 FBKVOController 。
FBKVOController
簡單來說,FBKVOController是對KVO機制的一層封裝,同時提供了線程安全的特性和并對如下這個 臭名昭著 的函數進行了封裝,提供了干凈的block的回調,避免了處理這個函數的邏輯散落的到處都是。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
源碼分析
整個項目的結構非常簡單,包含如下四個文件:
- FBKVOController.h/.m
- NSObject+FBKVOController.h/.m
其中, NSObject+FBKVOController 只是通過 AssociateObject 給NSObject提供了一個 retain 及一個 非retain 型的KVOController。
這兩種不同類型的KVOController有啥區別,我們稍后再提,我們將重點投向 FBKVOController 這個文件。
打開這個 FBKVOController.m 文件,哎呀,600多行文件,有點蛋疼。沒事,配合頭文件粗略掃一眼以后,可以發現其中很多方法都是 convenience method 。
簡單剝離一下數據結構以后,我們可以發現,主要的數據結構有如下三個。
- FBKVOInfo
- FBKVOSharedController
- FBKVOController
FBKVOController
既然我們前面通過 NSObject+FBKVOController 知道了每個對象都會有其對應的 FBKVOController ,那我們就先來看看這個類吧。
//1. @implementation FBKVOController { NSMapTable *_objectInfosMap; OSSpinLock _lock; } //2. - (instancetype)initWithObserver:(id)observer retainObserved:(BOOL)retainObserved { self = [super init]; if (nil != self) { // 2. _observer = observer; // 3. NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality; _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0]; // 4. _lock = OS_SPINLOCK_INIT; } return self; }
-
首先我們看到,這個對象持有一個 OSSpinLock 及一個 NSMapTable 。其中 OSSpinLock 即為自旋鎖,當多個線程競爭相同的 critical section 時,起到保護作用。 NSMapTable 可能大家接觸不是很多,我們在后文會詳細介紹,這里大家可以先理解為一個高級的NSDictionary。
-
在構造函數中,首先將傳入的observer進行 weak 持有,這主要為了避免 Retain Cycle 。
-
這一段的內容可能大家不太熟悉, NSPointerFunctionsOptions 簡單來說就是定義 NSMapTable 中的key和value采用何種內存管理策略,包括 strong 強引用, weak 弱引用以及 copy (要支持NSCopying協議)
-
初始化自旋鎖
接下來,使我們通過 FBKVOController 來對一個對象的某個或者某些keypath進行觀察。
- (void)observe:(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; } // 1. create info _FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block]; // 2. observe object with info [self _observe:object info:info]; }
- 對于傳入的參數,構建一個內部的FBKVOInfo數據結構
- 調用 [self _observe:object info:info];
接下來,我們來跟蹤一下 [self _observe:object info:info]; ,內容如下:
- (void)_observe:(id)object info:(_FBKVOInfo *)info { // lock OSSpinLockLock(&_lock); // 1. NSMutableSet *infos = [_objectInfosMap objectForKey:object]; // 2. _FBKVOInfo *existingInfo = [infos member:info]; if (nil != existingInfo) { NSLog(@"observation info already exists %@", existingInfo); // unlock and return OSSpinLockUnlock(&_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 OSSpinLockUnlock(&_lock); // 3. [[_FBKVOSharedController sharedController] observe:object info:info]; }
拋開非死book自身標記的注釋,有三處比較值得我們注意:
-
根據被觀察的object獲取其對應的 infos set 。這個主要作用在于避免多次對同一個keyPath添加多次觀察,避免crash。 因為每調用一次 addObserverForKeyPath 就要有一個對應的 removeObserverForKey 。
-
從 infos set 判斷是不是已經有了與此次info相同的觀察。
-
如果以上都順利通過,將觀察的信息及關系注冊到 _FBKVOSharedController 中。
至此,FBKVOController的任務基本都結束, unObserve 相關的任務邏輯大同小異,不再贅述。
FBKVOSharedController
初次看到這個類的時候,我的腦海中浮現了兩個問題,FBKVOSharedController是干嘛的?為什么FBKVOController還需要將觀察的信息轉交呢?
其實我個人覺得這一層不是必要的,但是按照非死book的理念來說就是將所有的觀察信息統一交由一個 FBKVOSharedController 的 單例 進行維護。如果大家讀過非死book出品的 Flux 架構,也會發現,非死book經常喜歡維護一個類似于中間件的注冊表,在這里, FBKVOSharedController 承擔的也是類似的職責。
于是,通過如下方法,我們像使用注冊表一樣將對KVOInfo注冊。
- (void)observe:(id)object info:(_FBKVOInfo *)info { if (nil == info) { return; } // register info OSSpinLockLock(&_lock); [_infos addObject:info]; OSSpinLockUnlock(&_lock); // 1. [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info]; }
- 代表所有的觀察信息都首先由 FBKVOSharedController 進行接受,隨后進行轉發。
實現 observeValueForKeyPath:ofObject:Change:context
來接收通知。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change); _FBKVOInfo *info; { // 1. OSSpinLockLock(&_lock); info = [_infos member:(__bridge id)context]; OSSpinLockUnlock(&_lock); } 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) { info->_block(observer, object, change); } 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]; } } } } }
- 根據context上下文獲取對應的KVOInfo
- 判斷當前 info 的 observer 和 controller ,是否仍然存在(因為之前我們采用的weak持有)
- 根據 info 的 block 或者 selector 或者 override 進行消息轉發。
到這里, FBKVOController 整體的實現就介紹完了,怎么樣,是不是局部看自己都會實現,但是一結合起完整的設計思路,就覺得,不虧是非死book呢。
NSMapTable
之前我們在前文中提到了 NSMapTable ,現在我們來詳細介紹他一下。
我們在平常的開發中都使用過 NSDictionary 或者 NSMutableDictionary ,但是這兩種數據結構有其的局限性。
以 NSDictionary 為例, NSDictionary 將 key 的 hash 值作為索引,存儲對應的 value 。因此, key 的要求是不能更改。所以, NSDictionary 為了確保安全,對于 key 采用了 copy 的策略。
默認情況下,支持 NSCopying 協議的類型都可以作為key。但是考慮到copy帶來的開銷,一般情況下我們都使用簡單的諸如數字或者字符串作為key。
那么,如果要使用 Object 作為key,想構建 Object to Object 的關系怎么辦呢?這個時候就用到 NSMapTable 。我們可以通過NSFunctionsPointer來分別定義對key和value的儲存關系,簡單可以分類為 strong , weak 以及 copy 。而當利用 object 作為key的時候,可以定義評判相等的標準,如: use shifted pointer hash and direct equality, object description或者size 。
具體你需要去override如下幾種方法:
// pointer personality functions @property (nullable) NSUInteger (*hashFunction)(const void *item, NSUInteger (* __nullable size)(const void *item)); @property (nullable) BOOL (*isEqualFunction)(const void *item1, const void*item2, NSUInteger (* __nullable size)(const void *item)); @property (nullable) NSUInteger (*sizeFunction)(const void *item); @property (nullable) NSString * __nullable (*descriptionFunction)(const void *item);
在 FBKVOController 自定義的可以作為key的結構 FBKVOInfo ,就復寫了
- (NSUInteger)hash { return [_keyPath hash]; } - (BOOL)isEqual:(id)object { if (nil == object) { return NO; } if (self == object) { return YES; } if (![object isKindOfClass:[self class]]) { return NO; } return [_keyPath isEqualToString:((_FBKVOInfo *)object)->_keyPath]; }