FBKVOController 源碼閱讀理解
簡介
蘋果原生API
提供的KVO
有一些顯而易見的缺點(diǎn)场躯。
- 添加和移除觀察者要配對出現(xiàn);
- 移除一個未添加的觀察者纬霞,程序會crash;
- 添加觀察者恩够,移除觀察者引有,通知回調(diào)蠢挡,三塊兒代碼過于分散;
那么,有沒有改良版的KVO呢化撕?FBKVOController
是Facebook
開源的代碼几晤,主要是對我們經(jīng)常使用的 KVO
機(jī)制進(jìn)行了額外的一層封裝,源碼簡單,設(shè)計感好植阴。其中最亮眼的特色是提供了一個block
回調(diào)讓我們進(jìn)行處理蟹瘾,避免KVO
的相關(guān)代碼四處散落。
使用
[observer.KVOControllerNonRetaining observe:object keyPath:@"keyPath"
options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull
object, NSDictionary<NSString *,id> * _Nonnull change) {
}];
使用非常簡單掠手,提供了block
回調(diào)憾朴,而且并不需要考慮remove observer
的事情。同時還可以以數(shù)組形式喷鸽,同時對一個被觀察者object
的多個不同成員變量進(jìn)行KVO
众雷。
閱讀
KVOController
一共只有兩個文件NSObject+FBKVOController
和FBKVOController
。
NSObject+FBKVOController
中的代碼和邏輯非常簡單,通過Category
的形式結(jié)合Runtime
的特性报腔,通過objc_setAssociatedObject
株搔,并支持懶加載的形式剖淀,給所有NSObject
類添加了兩個FBKVOController
類型的屬性纯蛾。
FBKVOController
中定義了三個類,FBKVOController
纵隔、_FBKVOSharedController
和_FBKVOInfo
翻诉。因為代碼中直接使用的是FBKVOController
,我們先看它捌刮。
初始化
// 在FBKVOController.h中
@property (nullable, nonatomic, weak, readonly) id observer;
- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
self = [super init];
if (nil != self) {
// observer 本身會持有 FBKVOController,而如果FBKVOController再持有observer,那么必須使用weak
_observer = observer;
// 定義 NSMapTable key的內(nèi)存管理策略
// retainObserved : 是否對 NSMapTable中的key(key是Observed-被觀察者)進(jìn)行retain操作
// 在默認(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];
// C語言中的互斥鎖绅作,一般在開發(fā)跨平臺的框架時使用
pthread_mutex_init(&_lock, NULL);
}
return self;
}
我在看源碼的時候比價陌生的是NSMapTable芦圾,于是查閱了一波資料,推薦一篇文章NSMapTable: 不只是一個能放weak指針的 NSDictionary俄认。
簡單來說个少,NSMapTable
與NSDictionary
類似。
NSDictionary
對Key
的內(nèi)存策略是固定為copy
,因此key
應(yīng)該是小且高效的眯杏,以至于復(fù)制的時候不會對CPU
和內(nèi)存造成負(fù)擔(dān)夜焦。NSDictionary
中真的只適合將值類型的對象作為key
(如簡短字符串和數(shù)字)。當(dāng)key
為object
時岂贩,copy
的開銷可能比較大茫经!并不適合自己的模型類來做對象到對象的映射。
NSMapTable
可以自主控制key -> value
的內(nèi)存管理策略,在這里只能使用相對比較靈活的 NSMapTable萎津。
注冊observe
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:
(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block
{
// Debug模式下:不滿足條件的注冊卸伞,會產(chǎn)生斷言直接crash
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters
observe:%@ keyPath:%@ block:%p", object, keyPath, block);
// 非Debug模式下:不滿足條件的注冊,直接返回
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath
options:options block:block];
// observe object with info
[self _observe:object info:info];
}
用NSAssert()
預(yù)處理宏捕獲錯誤锉屈,它與NSLog
一樣荤傲,如果使用過多, 也會影響程序運(yùn)行。不用擔(dān)心部念,Xcode
已經(jīng)幫我們設(shè)置好了弃酌,在debug
模式下放心使用,Xcode
已經(jīng)默認(rèn)將release
環(huán)境下的斷言取消了, 免除了忘記關(guān)閉斷言造成的程序不穩(wěn)定儡炼。
在這里創(chuàng)建了一個_FBKVOInfo對象妓湘,使用調(diào)用者傳入的參數(shù)(除了object)和 self進(jìn)行初始化。_FBKVOInfo是一個模型類乌询,負(fù)責(zé)將記錄這些數(shù)據(jù)榜贴。
接上段代碼的最后一句[self _observe:object info:info];
。
- (void)_observe:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
// _objectInfosMap : 初始化時創(chuàng)建的 NSMapTable
// 其結(jié)構(gòu)是以 被觀察者 object 為 key唬党。并不像我們常用的 NSDictionary 那樣是以 NSString 為 key
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// check for info existence
// 使用 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;
}
// lazilly create set of infos
// 如果沒有 關(guān)于這個 object(被觀察者)的相關(guān)信息鹃共,則創(chuàng)建 NSMutableSet,并添加到 NSMapTable 中
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];
}
NSMapTable
通過key
(object
:被觀察的對象)驶拱,來查找對應(yīng)的value
(NSMutableSet
:存放_FBKVOInfo
類型的info
)霜浴,然后從NSMutableSet
中查找是否已經(jīng)保存了相同_FBKVOInfo
。如果已經(jīng)存在了相同的_FBKVOInfo
蓝纲,那么就可以return
不做操作阴孟;如果不存在該_FBKVOInfo
,查看是否存在value
(NSMutableSet
)税迷,不存在則創(chuàng)建NSMutableSet
永丝,與key
(object
)映射添加到NSMapTable
中。之后再將傳入的_FBKVOInfo
添加到NSMutableSet
中箭养。
避免添加重復(fù)的keypath
當(dāng)我在查看的時候比較在意的是NSSet
這個方法:
// 判斷集合是否包含對象object
- (nullable ObjectType)member:(ObjectType)object;
它用來查看NSSet
中是否已經(jīng)包含了相同的object
慕嚷。之前的代碼中我們知道,傳入相同的keyPath
也會創(chuàng)建不同的_FBKVOInfo
毕泌,那么是如何做到避免了相同的keyPath
重復(fù)添加的喝检?
通過重寫 - (NSUInteger)hash;
以及 - (BOOL)isEqual:(id)anObject;
這兩個方法,來告訴NSSet
“相等”的含義懈词。
為了優(yōu)化判等的效率, 基于
hash
的NSSet
和NSDictionary
在判斷成員是否相等時, 會這樣做
Step 1: 集成成員的hash
值是否和目標(biāo)hash
值相等, 如果相同進(jìn)入Step 2, 如果不等, 直接判斷不相等
Step 2: 在hash
值相同的情況下, 再進(jìn)行對象判等(- (BOOL)isEqual:
), 作為判等的結(jié)果
hash值是對象判等的必要非充分條件
數(shù)據(jù)結(jié)構(gòu)
觀察者observe
持有FBKVOController
蛇耀,同時FBKVOController
又弱引用(weak
)了observe
,而FBKVOController
擁有成員變量NSMapTable
坎弯,NSMapTable
以被觀察者(object
)為key
纺涤,NSMutableSet
為value
,在NSMutableSet
中,存儲了不同info
抠忘。如圖:
_FBKVOSharedController
_FBKVOSharedController
是單例類撩炊,其職責(zé)是:接收并轉(zhuǎn)發(fā)KVO
通知,通過FBKVOController
框架添加的KVO
都由_FBKVOSharedController
來處理。
- (instancetype)init
{
self = [super init];
if (nil != self) {
NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
_infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
} else {
// silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
_infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
}
#endif
pthread_mutex_init(&_mutex, NULL);
}
return self;
}
初始化方法中有一個NSHashTable
崎脉,同NSMapTable
一樣拧咳,接觸不是很多。NSHashTable
效仿了NSSet
囚灼,但提供了比NSSet
更多的操作選項骆膝,尤其是在對弱引用關(guān)系的支持上,NSHashTable
在對象/內(nèi)存處理時更加的靈活灶体。相較于NSSet
阅签,NSHashTable
具有以下特性:
- NSSet(NSMutableSet)持有其元素的強(qiáng)引用,同時這些元素是使用hash值及isEqual:方法來做hash檢測及判斷是否相等的蝎抽。
- NSHashTable是可變的政钟,它沒有不可變版本。
- 它可以持有元素的弱引用,而且在對象被銷毀后能正確地將其移除养交。而這一點(diǎn)在NSSet是做不到的精算。
- 它的成員可以在添加時被拷貝。
- 它的成員可以使用指針來標(biāo)識是否相等及做hash檢測碎连。
- 它可以包含任意指針灰羽,其成員沒有限制為對象。我們可以配置一個NSHashTable實例來操作任意的指針破花,而不僅僅是對象谦趣。
在初始化中使用了NSPointerFunctionsWeakMemory
,簡單來說就是定義NSHashTable中的元素采用弱引用內(nèi)存管理策略疲吸。當(dāng)里面存放的_FBKVOInfo
銷毀時座每,NSHashTable
自動將它移除。猜想由于在FBKVOController
中摘悴,已經(jīng)通過NSMutableSet對_FBKVOInfo
持有了一個強(qiáng)引用峭梳,那么這里采用弱引用特性的集合類型,給自己省去了很多麻煩的操作蹂喻,交由系統(tǒng)完成葱椭。
繼續(xù)追蹤之注冊observe
的方法,最后一句代碼[[_FBKVOSharedController sharedController] observe:object info:info];
- (void)observe:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// register 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);
// add observer
// _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) {
// 這里是做容錯的處理,避免意外情況,與移除觀察者的邏輯相關(guān)
// 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];
}
}
在這段代碼中旷赖,思路非常清晰,將所有本該在觀察者中寫的邏輯更卒,改為統(tǒng)一由_FBKVOSharedController
中進(jìn)行注冊等孵,由_FBKVOSharedController
這個單例類來注冊和接收。
有些特殊的是context
參數(shù)使用的是(void *)info
的指針蹂空,這樣可以保證context
的唯一性,同時會將info
傳遞給回調(diào)方法俯萌,也是為了做容錯處理,讓代碼更加嚴(yán)謹(jǐn)上枕。在修改_state
狀態(tài)時咐熙,也考慮到了在移除觀察者方法中存在的某個漏洞,在這里進(jìn)行安全的移除姿骏。注釋的清晰糖声,嚴(yán)謹(jǐn)?shù)倪壿嫞?xì)心的設(shè)計,吾輩要多多學(xué)習(xí)蘸泻。
實現(xiàn)observeValueForKeyPath:ofObject:Change:context
來接收通知:
- (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
// 這里就很巧妙啊,通過注冊觀察時,將info傳過來,然后在NSHashTable中查看是否存在這個info
pthread_mutex_lock(&_mutex);
info = [_infos member:(__bridge id)context];
pthread_mutex_unlock(&_mutex);
}
if (nil != info) {
// take strong reference to controller
// 從這里拿到了FBKVOController
FBKVOController *controller = info->_controller;
if (nil != controller) {
// take strong reference to observer
// 從這里拿到了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) {
// 字典合并琉苇,并重新拷貝一份,
// 包含信息有:1悦施、改變了哪個值 mChange 2并扇、 原先的 change 字典
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 {
// 默認(rèn)情況 調(diào)用觀察者的原生函數(shù)B盏穷蛹!
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
}
}
}
-
context
傳遞的是info
,在NSHashTable
中昼汗,查看是否還存在這個info
肴熏。因為在NSHashTable
是弱引用,所以它有可能已經(jīng)被釋放或者已經(jīng)被移除顷窒。 - 在傳遞參數(shù)和對象初始化賦值成員變量的時候蛙吏,考慮到安全性,像
block
鞋吉,NSString
等這些要進(jìn)行copy
操作鸦做。
移除
不用像使用原生的KVO
考慮移除的問題,當(dāng)被觀察者object
銷毀時谓着,注冊的觀察者也就不會再收到回調(diào)泼诱。
有些場景需要我們手動移除注冊,FBKVOController
也提供了相關(guān)的方法赊锚。
/**
移除被觀察的對象的某個屬性
*/
- (void)unobserve:(nullable id)object keyPath:(NSString *)keyPath;
/**
移除被觀察對象的所有
*/
- (void)unobserve:(nullable id)object;
/**
移除所有
*/
- (void)unobserveAll;
在實現(xiàn)中有這樣一段修改_state
的代碼:
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
根據(jù)_state
來進(jìn)行移除,并修改了_state
的狀態(tài)治筒,這樣就和之前注冊observe
時的一段移除邏輯相對應(yīng),如果在_FBKVOSharedController
中還未注冊成功改抡,就被移除掉的話矢炼,那么_state
狀態(tài)值是_FBKVOInfoStateNotObserving
,那么在注冊時就會將這個注冊移除掉阿纤。
自釋放
FBKVOController
是如何做到自釋放的句灌?可以歸納為四個字——動態(tài)屬性。其為觀察者綁定動態(tài)屬性self.KVOController
欠拾,動態(tài)綁定的KVOController
會隨著觀察者的釋放而釋放胰锌,KVOController
在自己的dealloc
函數(shù)中移除KVO
監(jiān)聽,巧妙的將觀察者的remove
轉(zhuǎn)移到其動態(tài)屬性的dealloc
函數(shù)中藐窄。
注意
其還是有一定的局限性——對象無法監(jiān)聽自己的屬性资昧,如果你的代碼是這樣的:
[self.KVOController observe:self keyPath:@"date"
options:NSKeyValueObservingOptionNew block:^(NSDictionary *change) {
// to do
}];
很遺憾,循環(huán)引用的問題又出現(xiàn)荆忍,因為FBKVOController
中的NSMapTable
對象會retain
key
對象:
[_objectInfosMap setObject:infos forKey:object];
在NSObject+FBKVOController
中格带,動態(tài)添加了兩個屬性
@interface NSObject (FBKVOController)
// 會對被觀察者強(qiáng)引用
@property (nonatomic, strong) FBKVOController *KVOController;
// 會對被觀察者弱引用
@property (nonatomic, strong) FBKVOController *KVOControllerNonRetaining;
@end
當(dāng)在使用KVOController
時撤缴,如果不手動取消對被觀察者的注冊,那么只有在observe
的FBKVOController
被釋放時叽唱,被觀察者object
才會被釋放掉屈呕。
總結(jié)
FBKVOController對于喜好使用kvo的工程師來說,是一個好的棺亭,精簡的開發(fā)框架虎眨。源碼優(yōu)雅,可讀性高镶摘,利于自己維護(hù)嗽桩。
優(yōu)點(diǎn)如下:
- 提供了干凈的
block
的回調(diào),避免了處理這個函數(shù)的邏輯散落的到處都是凄敢。 - 不用擔(dān)心
remove
問題碌冶,不用再在dealloc
中寫remove
代碼。當(dāng)然贡未,如果你需要在其他時機(jī)進(jìn)行remove observer
,你大可放心的remove
种樱,不會出現(xiàn)因為沒有添加而crash
的問題。
缺點(diǎn):
- 對象無法監(jiān)聽自己的屬性俊卤,否則會出現(xiàn)循環(huán)引用。