Crash 防護(hù)方案(五):KVO

原文 : 與佳期的個人博客(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-isa.png

對于 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

后記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锥累,隨后出現(xiàn)的幾起案子缘挑,更是在濱河造成了極大的恐慌,老刑警劉巖桶略,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件语淘,死亡現(xiàn)場離奇詭異,居然都是意外死亡际歼,警方通過查閱死者的電腦和手機(jī)惶翻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹅心,“玉大人吕粗,你說我怎么就攤上這事⌒窭ⅲ” “怎么了颅筋?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長输枯。 經(jīng)常有香客問我垃沦,道長,這世上最難降的妖魔是什么用押? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮靶剑,結(jié)果婚禮上蜻拨,老公的妹妹穿的比我還像新娘。我一直安慰自己桩引,他們只是感情好缎讼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坑匠,像睡著了一般血崭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天夹纫,我揣著相機(jī)與錄音咽瓷,去河邊找鬼。 笑死舰讹,一個胖子當(dāng)著我的面吹牛茅姜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播月匣,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼钻洒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锄开?” 一聲冷哼從身側(cè)響起素标,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎萍悴,沒想到半個月后头遭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡退腥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年任岸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狡刘。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡享潜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嗅蔬,到底是詐尸還是另有隱情剑按,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布澜术,位于F島的核電站艺蝴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鸟废。R本人自食惡果不足惜猜敢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盒延。 院中可真熱鬧缩擂,春花似錦、人聲如沸添寺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽计露。三九已至博脑,卻和暖如春憎乙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叉趣。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工泞边, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人君账。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓繁堡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乡数。 傳聞我的和親對象是個殘疾皇子椭蹄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 上半年有段時間做了一個項目,項目中聊天界面用到了音頻播放净赴,涉及到進(jìn)度條绳矩,當(dāng)時做android時候處理的不太好,由于...
    DaZenD閱讀 3,017評論 0 26
  • 該文章屬于劉小壯原創(chuàng)玖翅,轉(zhuǎn)載請注明:劉小壯[http://www.reibang.com/u/2de707c93d...
    劉小壯閱讀 48,398評論 35 227
  • [深入淺出Cocoa]詳解鍵值觀察(KVO)及其實(shí)現(xiàn)機(jī)理羅朝輝 (http://www.cppblog.com/k...
    Crazy2015閱讀 683評論 0 1
  • KVO原理淺析 KVO翼馆,即Key-Value Observing,官方文檔中的介紹是 Key-value obse...
    wilsonhan閱讀 1,706評論 1 7
  • 春茶季金度,高端貨应媚,我們屯了不少,這一切都要?dú)w功于看老領(lǐng)導(dǎo)的計劃周全猜极! 沒有做好預(yù)算中姜,不敢壓貨的門店,急得直...
    小稀里糊涂閱讀 153評論 0 0