KVO 作為 iOS 中一種強大并且有效的機制登馒,為 iOS 開發(fā)者們提供了很多的便利;我們可以使用 KVO 來檢測對象屬性的變化圈纺、快速做出響應蛾娶,這能夠為我們在開發(fā)強交互潜秋、響應式應用以及實現(xiàn)視圖和模型的雙向綁定時提供大量的幫助半等。
但是在大多數(shù)情況下呐萨,除非遇到不用 KVO 無法解決的問題谬擦,筆者都會盡量避免它的使用惨远,這并不是因為 KVO 有性能問題或者使用場景不多话肖,主要的原因是 KVO 的使用實在是太麻煩了最筒。
使用 KVO 時蔚叨,既需要進行注冊成為某個對象屬性的觀察者蔑水,還要在合適的時間點將自己移除,再加上需要覆寫一個又臭又長的方法丹擎,并在方法里判斷這次是不是自己要觀測的屬性發(fā)生了變化蒂培,每次想用 KVO 解決一些問題的時候庶骄,作者的第一反應就是頭疼单刁,這篇文章會為各位為 KVO 所苦的開發(fā)者提供一種更優(yōu)雅的解決方案。
使用 KVO
不過在介紹如何優(yōu)雅地使用 KVO 之前肺樟,我們先來回憶一下么伯,在通常情況下卡儒,我們是如何使用 KVO 進行鍵值觀測的骨望。
首先,我們有一個Fizz類缀磕,其中包含一個number屬性袜蚕,它在初始化時會自動被賦值為@0:
// Fizz.h@interfaceFizz:NSObject
@property(nonatomic,strong)NSNumber*number;
@end
// Fizz.m
@implementationFizz
-(instancetype)init{
if(self=[superinit]){
_number=@0;
}returnself;
}
@end
我們想在Fizz對象中的number對象發(fā)生改變時獲得通知得到新的和舊的值,這時我們就要祭出-addObserver:forKeyPath:options:context方法來監(jiān)控number屬性的變化:
Fizz*fizz=[[Fizz alloc]init];
[fizz addObserver:selfforKeyPath:@"number"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld? ? ? ? ? context:nil];
fizz.number=@2;
在將當前對象self注冊成為fizz的觀察者之后遣疯,我們需要在當前對象中覆寫-observeValueForKeyPath:ofObject:change:context:方法:
-(void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context{
if([keyPath isEqualToString:@"number"]){
NSLog(@"%@",change);
}
}
在大多數(shù)情況下我們只需要對比keyPath的值另锋,就可以知道我們到底監(jiān)控的是哪個對象夭坪,但是在更復雜的業(yè)務場景下过椎,使用context上下文以及其它輔助手段才能夠幫助我們更加精準地確定被觀測的對象疚宇。
但是當上述代碼運行時敷待,雖然可以成功打印出change字典,但是卻會發(fā)生崩潰勾哩,你會在控制臺中看到下面的內(nèi)容:
2017-02-2623:44:19.666KVOTest[15888:513229]{
kind=1;
new=2;
old=0;
}
2017-02-2623:44:19.720KVOTest[15888:513229]***Terminating app due to uncaught exception'NSInternalInconsistencyException',reason:'An instance0x60800001dd20of class Fizz was deallocatedwhilekey value observers were still registered with it.Current observation info:(Context:0x0,Property:0x608000057400>)'
這是因為fizz對象沒有被其它對象引用思劳,在脫離viewDidLoad作用于之后就被回收了妨猩,然而在-dealloc時壶硅,并沒有移除觀察者庐椒,所以會造成崩潰。
我們可以使用下面的代碼來驗證上面的結(jié)論是否正確:
// Fizz.h
@interfaceFizz:NSObject
@property(nonatomic,strong)NSNumber*number;
@property(nonatomic,weak)NSObject*observer;
@end
// Fizz.m@implementationFizz
-(instancetype)init{
if(self=[superinit]){
_number=@0;
}
returnself;
}
-(void)dealloc{
[selfremoveObserver:self.observer forKeyPath:@"number"];
}
@end
在Fizz類的接口中添加一個observer弱引用來持有對象的觀察者,并在對象-dealloc時將它移除窗宇,重新運行這段代碼军俊,就不會發(fā)生崩潰了。
由于沒有移除觀察者導致崩潰使用 KVO 時經(jīng)常會遇到的問題之一担败,解決辦法其實有很多提前,我們在這里簡單介紹一個狈网,使用當前對象持有被觀測的對象笨腥,并在當前對象-dealloc時脖母,移除觀察者:
-(void)viewDidLoad{
[superviewDidLoad];
self.fizz=[[Fizz alloc]init];
[self.fizz addObserver:selfforKeyPath:@"number"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld? ? ? ? ? ? ? ? ? context:nil];
self.fizz.number=@2;
}
-(void)dealloc{
[self.fizz removeObserver:selfforKeyPath:@"number"];
}
這也是我們經(jīng)常使用來避免崩潰的辦法谆级,但是在筆者看來也是非常的不優(yōu)雅,除了上述的崩潰問題鸽凶,使用 KVO 的過程也非常的別扭和痛苦:
需要手動移除觀察者玻侥,且移除觀察者的時機必須合適亿蒸;
注冊觀察者的代碼和事件發(fā)生處的代碼上下文不同边锁,傳遞上下文是通過void *指針茅坛;
需要覆寫-observeValueForKeyPath:ofObject:change:context:方法则拷,比較麻煩煌茬;
在復雜的業(yè)務邏輯中坛善,準確判斷被觀察者相對比較麻煩邻眷,有多個被觀測的對象和屬性時肆饶,需要在方法中寫大量的if進行判斷抖拴;
雖然上述幾個問題并不影響 KVO 的使用,不過這也足夠成為筆者盡量不使用 KVO 的理由了候衍。
優(yōu)雅地使用 KVO
如何優(yōu)雅地解決上一節(jié)提出的幾個問題呢蛉鹿?我們在這里只需要使用 Facebook 開源的KVOController框架就可以優(yōu)雅地解決這些問題了往湿。
如果想要實現(xiàn)同樣的業(yè)務需求领追,當使用 KVOController 解決上述問題時绒窑,只需要以下代碼就可以達到與上一節(jié)中完全相同的效果:
[self.KVOController observe:self.fizz? ? ? ? ? ? ? ? ? ? keyPath:@"number"options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld? ? ? ? ? ? ? ? ? ? ? block:^(id? _Nullable observer,id? _Nonnull object,NSDictionary*_Nonnull change){NSLog(@"%@",change);}];
我們可以在任意對象上獲得KVOController對象些膨,然后調(diào)用它的實例方法-observer:keyPath:options:block:就可以檢測某個對象對應的屬性了,該方法傳入的參數(shù)還是非常容易理解的肢预,在 block 中也可以獲得所有與 KVO 有關(guān)的參數(shù)烫映。
使用 KVOController 進行鍵值觀測可以說完美地解決了在使用原生 KVO 時遇到的各種問題。
不需要手動移除觀察者擅威;
實現(xiàn) KVO 與事件發(fā)生處的代碼上下文相同,不需要跨方法傳參數(shù)瞧筛;
使用 block 來替代方法能夠減少使用的復雜度较幌,提升使用 KVO 的體驗白翻;
每一個keyPath會對應一個屬性滤馍,不需要在 block 中使用if判斷keyPath巢株;
KVOController 的實現(xiàn)
KVOController 其實是對 Cocoa 中 KVO 的封裝,它的實現(xiàn)其實也很簡單困檩,整個框架中只有兩個實現(xiàn)文件悼沿,先來簡要看一下 KVOController 如何為所有的NSObject對象都提供-KVOController屬性的吧糟趾。
分類和 KVOController 的初始化
KVOController 不止為 Cocoa Touch 中所有的對象提供了-KVOController屬性還提供了另一個KVOControllerNonRetaining屬性逢唤,實現(xiàn)方法就是分類和 ObjC Runtime鳖藕。
@interfaceNSObject(FBKVOController)
@property(nonatomic,strong)FBKVOController*KVOController;
@property(nonatomic,strong)FBKVOController*KVOControllerNonRetaining;
@end
從名字可以看出KVOControllerNonRetaining在使用時并不會持有被觀察的對象著恩,與它相比KVOController就會持有該對象了。
對于KVOController和KVOControllerNonRetaining屬性來說纵顾,其實現(xiàn)都非常簡單栋盹,對運行時非常熟悉的讀者都應該知道使用關(guān)聯(lián)對象就可以輕松實現(xiàn)這一需求例获。
-(FBKVOController*)KVOController{
id controller=objc_getAssociatedObject(self,NSObjectKVOControllerKey);if(nil==controller){controller=[FBKVOController controllerWithObserver:self];self.KVOController=controller;}returncontroller;}-(void)setKVOController:(FBKVOController*)KVOController{objc_setAssociatedObject(self,NSObjectKVOControllerKey,KVOController,OBJC_ASSOCIATION_RETAIN_NONATOMIC);}-(FBKVOController*)KVOControllerNonRetaining{id controller=objc_getAssociatedObject(self,NSObjectKVOControllerNonRetainingKey);
if(nil==controller){
controller=[[FBKVOController alloc]initWithObserver:selfretainObserved:NO];
self.KVOControllerNonRetaining=controller;
}
return controller;
}
-(void)setKVOControllerNonRetaining:(FBKVOController*)KVOControllerNonRetaining{
objc_setAssociatedObject(self,NSObjectKVOControllerNonRetainingKey,KVOControllerNonRetaining,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
兩者的setter方法都只是使用objc_setAssociatedObject按照鍵值簡單地存一下蠕搜,而getter中不同的其實也就是對于FBKVOController的初始化了妓灌。
到這里這個整個 FBKVOController 框架中的兩個實現(xiàn)文件中的一個就介紹完了蜜宪,接下來要看一下其中的另一個文件中的類KVOController端壳。
KVOController 的初始化
KVOController是整個框架中提供 KVO 接口的類损谦,作為 KVO 的管理者,其必須持有當前對象所有與 KVO 有關(guān)的信息颅湘,而在KVOController中闯参,用于存儲這個信息的數(shù)據(jù)結(jié)構(gòu)就是NSMapTable鹿寨。
為了使KVOController達到線程安全脚草,它還必須持有一把pthread_mutex_t鎖馏慨,用于在操作_objectInfosMap時使用。
再回到上一節(jié)提到的初始化問題倔撞,NSObject的屬性FBKVOController和KVOControllerNonRetaining的區(qū)別在于前者會持有觀察者痪蝇,使其引用計數(shù)加一霹俺。
-(instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved{
self=[superinit];
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);}returnself;
}
在初始化方法中使用各自的方法對KVOController對象持有的所有實例變量進行初始化,KVOController和KVOControllerNonRetaining的區(qū)別就體現(xiàn)在生成的NSMapTable實例時傳入的是NSPointerFunctionsStrongMemory還是NSPointerFunctionsWeakMemory選項觅玻。
KVO 的過程
使用KVOController實現(xiàn)鍵值觀測時溪厘,大都會調(diào)用實例方法-observe:keyPath:options:block來注冊成為某個對象的觀察者畸悬,監(jiān)控屬性的變化:
-(void)observe:(nullable id)object keyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block{
_FBKVOInfo*info=[[_FBKVOInfo alloc]initWithController:selfkeyPath:keyPath options:options block:block];
[self_observe:object info:info];
}
數(shù)據(jù)結(jié)構(gòu) _FBKVOInfo
這個方法中就涉及到另外一個私有的數(shù)據(jù)結(jié)構(gòu)_FBKVOInfo蹋宦,這個類中包含著所有與 KVO 有關(guān)的信息:
_FBKVOInfo在KVOController中充當?shù)淖饔脙H僅是一個數(shù)據(jù)結(jié)構(gòu)冷冗,我們主要用它來存儲整個 KVO 過程中所需要的全部信息蒿辙,其內(nèi)部沒有任何值得一看的代碼滨巴,需要注意的是恭取,_FBKVOInfo覆寫了-isEqual:方法用于對象之間的判等以及方便NSMapTable的存儲秽荤。
如果再有點別的什么特別作用的就是,其中的state表示當前的 KVO 狀態(tài)牍氛,不過在本文中不會具體介紹烟阐。
typedefNS_ENUM(uint8_t,_FBKVOInfoState){
_FBKVOInfoStateInitial=0,
_FBKVOInfoStateObserving,
_FBKVOInfoStateNotObserving,
};
observe 的過程
在使用-observer:keyPath:options:block:監(jiān)聽某一個對象屬性的變化時蜒茄,該過程的核心調(diào)用棧其實還是比較簡單:
我們從棧底開始簡單分析一下整個封裝 KVO 的過程檀葛,其中棧底的方法屿聋,也就是我們上面提到的-observer:keyPath:options:block:初始化了一個名為_FBKVOInfo的對象:
-(void)observe:(nullable id)object keyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block{
_FBKVOInfo*info=[[_FBKVOInfo alloc]initWithController:selfkeyPath:keyPath options:options block:block]
;[self_observe:object info:info];
}
在創(chuàng)建了_FBKVOInfo之后執(zhí)行了另一個私有方法-_observe:info::
-(void)_observe:(id)object info:(_FBKVOInfo*)info{
pthread_mutex_lock(&_lock);
NSMutableSet*infos=[_objectInfosMap objectForKey:object];
_FBKVOInfo*existingInfo=[infos member:info];
if(nil!=existingInfo){
pthread_mutex_unlock(&_lock);
return;
}
if(nil==infos){
infos=[NSMutableSet set];
[_objectInfosMap setObject:infos forKey:object];}[infos addObject:info];
pthread_mutex_unlock(&_lock);
[[_FBKVOSharedController sharedController]observe:object info:info];
}
這個私有方法通過自身持有的_objectInfosMap來判斷當前對象转锈、屬性以及各種上下文是否已經(jīng)注冊在表中存在了楚殿,在這個_objectInfosMap中保存著對象以及與對象有關(guān)的_FBKVOInfo集合:
在操作了當前KVOController持有的_objectInfosMap之后砌溺,才會執(zhí)行私有的_FBKVOSharedController類的實例方法-observe:info::
-(void)observe:(id)object info:(nullable _FBKVOInfo*)info{
pthread_mutex_lock(&_mutex);
[_infos addObject:info];
pthread_mutex_unlock(&_mutex);
[object addObserver:selfforKeyPath:info->_keyPath options:info->_options context:(void*)info];
if(info->_state==_FBKVOInfoStateInitial){
info->_state=_FBKVOInfoStateObserving;
}elseif(info->_state==_FBKVOInfoStateNotObserving){
[object removeObserver:selfforKeyPath:info->_keyPath context:(void*)info];
}
}
_FBKVOSharedController才是最終調(diào)用 Cocoa 中的-observe:forKeyPath:options:context:方法開始對屬性的監(jiān)聽的地方抚吠;同時楷力,在整個應用運行時萧朝,只會存在一個_FBKVOSharedController實例:
+(instancetype)sharedController{
static_FBKVOSharedController*_controller=nil;
staticdispatch_once_t onceToken;dispatch_once(&onceToken,^{_controller=[[_FBKVOSharedController alloc]init];});
return_controller;
}
這個唯一的_FBKVOSharedController實例會在 KVO 的回調(diào)方法中將事件分發(fā)給 KVO 的觀察者检柬。
-(void)observeValueForKeyPath:(nullable NSString*)keyPath? ? ? ? ? ? ? ? ? ? ? ofObject:(nullable id)object? ? ? ? ? ? ? ? ? ? ? ? change:(nullable NSDictionary*)change? ? ? ? ? ? ? ? ? ? ? context:(nullablevoid*)context{
_FBKVOInfo*info;pthread_mutex_lock(&_mutex);
info=[_infos member:(__bridge id)context];pthread_mutex_unlock(&_mutex);
FBKVOController*controller=info->_controller;id observer=controller.observer;
if(info->_block){
NSDictionary*changeWithKeyPath=change;
if(keyPath){
NSMutableDictionary*mChange=[NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey];
[mChange addEntriesFromDictionary:change];
changeWithKeyPath=[mChange copy];
}
info->_block(observer,object,changeWithKeyPath);
}elseif(info->_action){
[observer performSelector:info->_action withObject:change withObject:object];
}else{
[observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
}
}
在這個-observeValueForKeyPath:ofObject:change:context:回調(diào)方法中,_FBKVOSharedController會根據(jù) KVO 的信息_KVOInfo選擇不同的方式分發(fā)事件何址,如果觀察者沒有傳入 block 或者選擇子里逆,就會調(diào)用觀察者 KVO 回調(diào)方法。
上圖就是在使用 KVOController 時用爪,如果一個 KVO 事件觸發(fā)之后原押,整個框架是如何對這個事件進行處理以及回調(diào)的。
如何 removeObserver
在使用 KVOController 時偎血,我們并不需要手動去處理 KVO 觀察者的移除诸衔,因為所有的 KVO 事件都由私有的_KVOSharedController來處理;
當每一個KVOController對象被釋放時笨农,都會將它自己持有的所有 KVO 的觀察者交由_KVOSharedController的-unobserve:infos:方法處理:
-(void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo*>*)infos{
pthread_mutex_lock(&_mutex);
for(_FBKVOInfo*infoininfos){
[_infos removeObject:info];
}
pthread_mutex_unlock(&_mutex);
for(_FBKVOInfo*infoininfos){
if(info->_state==_FBKVOInfoStateObserving){
[object removeObserver:selfforKeyPath:info->_keyPath context:(void*)info];
}
info->_state=_FBKVOInfoStateNotObserving;
}
}
該方法會遍歷所有傳入的_FBKVOInfo,從其中取出keyPath并將_KVOSharedController移除觀察者帖渠。
除了在KVOController析構(gòu)時會自動移除觀察者谒亦,我們也可以通過它的實例方法-unobserve:keyPath:操作達到相同的效果;不過在調(diào)用這個方法時空郊,我們能夠得到一個不同的調(diào)用棧:
功能的實現(xiàn)過程其實都是類似的诊霹,都是通過-removeObserver:forKeyPath:context:方法移除觀察者:
-(void)unobserve:(id)object info:(nullable _FBKVOInfo*)info{
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
if(info->_state==_FBKVOInfoStateObserving){
[object removeObserver:selfforKeyPath:info->_keyPath context:(void*)info];
}
info->_state=_FBKVOInfoStateNotObserving;
}
不過由于這個方法的參數(shù)并不是一個數(shù)組,所以并不需要使用for循環(huán)渣淳,而是只需要將該_FBKVOInfo對應的 KVO 事件移除就可以了。
總結(jié)
KVOController 對于 Cocoa 中 KVO 的封裝非常的簡潔和優(yōu)秀伴箩,我們只需要調(diào)用一個方法就可以完成一個對象的鍵值觀測入愧,同時不需要處理移除觀察者等問題,能夠降低我們出錯的可能性嗤谚。
在筆者看來 KVOController 中唯一不是很優(yōu)雅的地方就是棺蛛,需要寫出object.KVOController才可以執(zhí)行 KVO,如果能將KVOController換成更短的形式可能看起來更舒服一些:
[self.kvo observer:keyPath:options:block:];
不過這并不是一個比較大的問題巩步,同時也只是筆者自己的看法旁赊,況且不影響 KVOController 的使用,所以各位讀者也無須太過介意椅野。