原文 : 與佳期的個人博客(gonghonglou.com)
Apple 使用了 isa 混寫(isa-swizzling)來實(shí)現(xiàn) KVO 百拓。當(dāng)觀察對象 A 時尝盼,KVO 機(jī)制動態(tài)創(chuàng)建一個新的名為:NSKVONotifying_A 的新類,該類繼承自對象 A 的本類鸦致,Apple 還重寫了該類的 -class 方法杉畜,返回父類裁良,即對象 A 的本類。且 KVO 為 NSKVONotifying_A 重寫觀察屬性的 setter 方法腾夯,setter 方法會負(fù)責(zé)在調(diào)用原 setter 方法之前和之后颊埃,通知所有觀察對象屬性值的更改情況。
isa 指針的作用:每個對象都有 isa 指針俯在,指向該對象的類竟秫,它告訴 Runtime 系統(tǒng)這個對象的類是什么。所以對象注冊為觀察者時跷乐,isa 指針指向新子類肥败,那么這個被觀察的對象就變成新子類的對象(或?qū)嵗┝恕?因而在該對象上對 setter 的調(diào)用就會調(diào)用已重寫的 setter,從而激活鍵值通知機(jī)制愕提。
我們可以通過斷點(diǎn)看到馒稍,被觀察者對象的 isa 指針已經(jīng)變成了 NSKVONotifying_ 開頭的類:
對于 KVO 使用不當(dāng)?shù)脑捄苋菀壮霈F(xiàn) Crash,比如添加和移除觀察不對應(yīng)浅侨,重復(fù) removeObserver: 或者移除一個不存在的觀察者就會造成 Crash纽谒,尤其是在多線程操作時防不勝防:
2019-07-13 17:50:14.805177+0800 GHLCrashGuard_Example[77448:5047850] *** Terminating app due to uncaught exception 'NSRangeException', reason: 'Cannot remove an observer <GHLKVOViewController 0x7fd072c17720> for the key path "name" from <GHLTestObject 0x60000106c160> because it is not registered as an observer.'
為了避免這種重復(fù)添加或者重復(fù)移除觀察造成的崩潰,可以對 KVO 包裝一層如输。創(chuàng)建一個額外的觀察者對象鼓黔,所有的添加觀察和移除觀察都通過這個額外的對象,這樣在添加和移除的時候就可以做安全判斷了不见。
FaceBook 出品的 KVOController 就是做的這樣的事情澳化。
self.kvo = [FBKVOController controllerWithObserver:self];
[self.kvo observe:self.obj keyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"oldName:%@", [change objectForKey:NSKeyValueChangeOldKey]);
NSLog(@"newName:%@", [change objectForKey:NSKeyValueChangeNewKey]);
}];
FBKVOController 的關(guān)鍵主要就在以下三個方法上:
實(shí)例化
+ (instancetype)controllerWithObserver:(nullable id)observer;
創(chuàng)建 FBKVOController 對象,主要做了兩件事:1稳吮、存儲了觀察者 _observer缎谷,2、創(chuàng)建了 _objectInfosMap灶似,用于存儲被觀察對象的信息列林。
添加觀察
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(FBKVONotificationBlock)block;
1、
// create info
_FBKVOInfo *info = [[_FBKVOInfo alloc] initWithController:self keyPath:keyPath options:options block:block];
主要在創(chuàng)建 _FBKVOInfo 對象酪惭,存儲
FBKVOController(存儲著觀察者 _observer)
keyPath(觀察屬性)
options(觀察時機(jī))
block(回調(diào))
2希痴、
- (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];
}
添加觀察者之前做的判斷,避免重復(fù)添加觀察撞蚕。并且添加了 pthread_mutex_lock 互斥鎖保證線程安全润梯。
3、
- (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];
}
}
調(diào)用系統(tǒng)方法 addObserver: 添加觀察者,并且在這后判斷了 info->_state 如果是非觀察狀態(tài)則執(zhí)行 removeObserver:
移除觀察
1纺铭、
- (void)_unobserve:(id)object info:(_FBKVOInfo *)info
{
// lock
pthread_mutex_lock(&_lock);
// get observation infos
NSMutableSet *infos = [_objectInfosMap objectForKey:object];
// lookup registered info instance
_FBKVOInfo *registeredInfo = [infos member:info];
if (nil != registeredInfo) {
[infos removeObject:registeredInfo];
// remove no longer used infos
if (0 == infos.count) {
[_objectInfosMap removeObjectForKey:object];
}
}
// unlock
pthread_mutex_unlock(&_lock);
// unobserve
[[_FBKVOSharedController sharedController] unobserve:object info:registeredInfo];
}
同樣是做了安全判斷寇钉,并通過 pthread_mutex_lock 鎖保證線程安全
2、
- (void)unobserve:(id)object info:(nullable _FBKVOInfo *)info
{
if (nil == info) {
return;
}
// unregister info
pthread_mutex_lock(&_mutex);
[_infos removeObject:info];
pthread_mutex_unlock(&_mutex);
// remove observer
if (info->_state == _FBKVOInfoStateObserving) {
[object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
}
info->_state = _FBKVOInfoStateNotObserving;
}
最后一步調(diào)用系統(tǒng)方法 removeObserver: 移除觀察者舶赔,info->_state 設(shè)置為非觀察狀態(tài)
Demo 地址:GHLCrashGuard
后記
小白出手扫倡,請多指教。如言有誤竟纳,還望斧正撵溃!
轉(zhuǎn)載請保留原文地址:http://gonghonglou.com/2019/07/07/crash-guard-kvo/