關(guān)于KVO的那些事 之 KVO安全用法封裝

關(guān)于KVO的那些事 之 KVO安全用法封裝

KVO (Key Value Observering) 是iOS用于監(jiān)聽某個(gè)對象某個(gè)變量一種簡潔便利的機(jī)制。但是,對于KVO的穩(wěn)定性蘋果卻做得沒有那么好,在以下三種情況下會(huì)無情Crash:

  1. 監(jiān)聽者dealloc時(shí),監(jiān)聽關(guān)系還存在。當(dāng)監(jiān)聽值發(fā)生變化時(shí),會(huì)給監(jiān)聽者的野指針發(fā)送消息,報(bào)野指針Crash。(猜測底層是保存了unsafe_unretained指向監(jiān)聽者的指針);
  2. 被監(jiān)聽者dealloc時(shí),監(jiān)聽關(guān)系還存在。在監(jiān)聽者內(nèi)存free掉后,直接會(huì)報(bào)監(jiān)聽者還存在監(jiān)聽關(guān)系而Crash;
  3. 移除監(jiān)聽次數(shù)大于添加監(jiān)聽次數(shù)。報(bào)出多次移除的錯(cuò)誤;

我們考慮到KVO帶來的傷害,平時(shí)十分小心翼翼在工程內(nèi)使用KVO,甚至能不用的時(shí)候就不用。但我們不甘心將如此好用的機(jī)制淪為帶刺玫瑰-----為KVO打造"紫金鎧甲"。

根據(jù)崩潰類型,我們的目標(biāo)也有三個(gè):

  1. 監(jiān)聽者dealloc時(shí),自動(dòng)移除監(jiān)聽者對其他對象的監(jiān)聽,No Crash;
  2. 被監(jiān)聽者dealloc時(shí),自動(dòng)移除對被監(jiān)聽者所有的監(jiān)聽,No Crash;
  3. 移除監(jiān)聽次數(shù)大于添加監(jiān)聽次數(shù)時(shí),多次監(jiān)聽/移除,只執(zhí)行一次,No Crash;

實(shí)現(xiàn)的源碼請點(diǎn)這里。

1. 解決監(jiān)聽者(listener)dealloc的Crash

解決監(jiān)聽者dealloc的Crash,最直接的辦法就是在監(jiān)聽者(listener)的dealloc調(diào)用[listened removeObserver:listener forKeyPath:path]。但此時(shí)我們會(huì)遇到3個(gè)問題:

  1. 如若不采用hook dealloc方法添加移除邏輯(采用hook風(fēng)險(xiǎn)比較大,會(huì)對所有NSObject對象的方法hook),還有什么方法能對所有對象的dealloc插上一腳呢?
  2. listener(監(jiān)聽者)不知道listened(被監(jiān)聽者)們是誰?
  3. listener不知道listened的keypath有哪些?

第一個(gè)問題的解決方案是 關(guān)聯(lián)對象。當(dāng)一個(gè)對象釋放時(shí),會(huì)進(jìn)行以下三個(gè)步驟:第一步,銷毀對象的所有屬性及實(shí)例變量,第二步,移除對象上的所有關(guān)聯(lián)對象;第三步,移除所有對該對象的weak引用。

KVO Crash三類

關(guān)聯(lián)對象的釋放是在listener dealloc過程的第二個(gè)步驟當(dāng)中,此時(shí)對象并沒有完全釋放。因此,我們可以給listener添加一個(gè)關(guān)聯(lián)對象解決第一個(gè)問題。

根據(jù)第一個(gè)問題解決方案,第二和第三個(gè)解決方案也不難想出。同樣地,我們可用關(guān)聯(lián)對象保存監(jiān)聽者和keypath數(shù)組實(shí)現(xiàn)。但我們不想這么做,為了讓 listener 看起來更加干凈,也為了讓邏輯更加清晰,可將監(jiān)聽者和keypath作為第一個(gè)關(guān)聯(lián)對象的實(shí)例變量。而且為了不強(qiáng)引用監(jiān)聽者,監(jiān)聽者是weak保存的。另外,為了全權(quán)管理listener對listened的行為,我們將監(jiān)聽行為轉(zhuǎn)給這個(gè)關(guān)聯(lián)對象,關(guān)聯(lián)對象收到監(jiān)聽消息再轉(zhuǎn)遞給listener。至此,我們稱這個(gè)關(guān)聯(lián)對象為代理者(proxy)。

最終,listener,listened,proxy三者關(guān)系及監(jiān)聽者移除監(jiān)聽過程如下:

解listener Crash

proxy定義如下:

proxy 定義

我們來詳細(xì)捋一遍過程:

添加過程

  1. 添加監(jiān)聽時(shí),取出關(guān)聯(lián)在listener上對象proxy(沒有就創(chuàng)建,并建立新的監(jiān)聽關(guān)系和轉(zhuǎn)發(fā)關(guān)系),且proxy以weak的方式保存了listener和listened兩個(gè)對象;
  2. 如果keypath在proxy保存中,說明已經(jīng)監(jiān)聽過了,不需要再監(jiān)聽。此時(shí)解決了我們第三大類的移除次數(shù)大于監(jiān)聽次數(shù)的crash。如不在keypath中,則添加keypath到keypath容器中,并建立對于新的keypath的監(jiān)聽關(guān)系;

dealloc過程

listener dealloc,觸發(fā)proxy的dealloc,proxy根據(jù)保存的keypath信息依次移除監(jiān)聽關(guān)系,至此監(jiān)聽關(guān)系完美解除。

第一個(gè)和第三個(gè)Crash問題解決了,接下來,我們來fix第二大問題~

2. 解決被監(jiān)聽者(listened)dealloc的Crash

和第一個(gè)問題類似,要解決listened的dealloc的Crash,同樣地可以給listened添加一個(gè)關(guān)聯(lián)對象用于檢測listened的釋放時(shí)機(jī)。如下圖所示:

圖中的D對象,也就是監(jiān)聽被監(jiān)聽者的監(jiān)聽者(有點(diǎn)繞...),Listened's Dealloc Listener簡稱LDL??梢钥吹剿粌H跟listened有關(guān)系,還弱持有了proxy。為什么呢?因?yàn)閷τ趌istened來說,它自己并不知道誰監(jiān)聽了它,而正好proxy知道監(jiān)聽中的所有秘密。當(dāng)listened釋放時(shí),LDL被釋放,根據(jù)保存的proxy關(guān)系,就能釋放對listened的所有監(jiān)聽關(guān)系,并且還可以移除listener的關(guān)聯(lián)對象proxy。

最終,我們來看看LDL的實(shí)現(xiàn)代碼:

眼尖的同學(xué)會(huì)發(fā)現(xiàn)兩個(gè)有趣的地方:

  1. listened 是 __unsafe_unretained的指針保存;
  2. proxy 是 用 weak NSHashTable 容器保存;

為什么要用__unsafe_unretained保存listened,而不是weak

原因是若使用weak,在LDL dealloc 的過程中,指針獲取到的值已經(jīng)為nil了(在proxy保存的listened也一樣),拿不到我們要用的對象指針,那么好奇的寶寶又會(huì)問了:這又是為什么呢?這是因?yàn)槊看问褂?code>weak變量時(shí),最終會(huì)調(diào)用id objc_loadWeakRetained(id *)方法,方法發(fā)現(xiàn)當(dāng)前對象如果在dealloc過程中就會(huì)直接返回nil。所以,我們這里使用了__unsafe_unretained指針來保有對象的指針,既能一直訪問到對象,又不會(huì)影響對象的引用計(jì)數(shù)。

第二個(gè)問題,NSHashTable 容器保存proxy,NSHashTable類似于數(shù)組,但它可以保存任何指針,而且可以有各種方法存儲他們,比如,retain, weak, copy...。而我們這里使用的目的是保存監(jiān)聽關(guān)系proxy列表,而且弱引用他們。這種特殊容器平時(shí)開發(fā)用的很少,想了解更多關(guān)于這些特殊容器的,請點(diǎn)這里

用法

原理講完了,說說最終的用法,和系統(tǒng)源碼API一樣簡單,提供了三個(gè)接口如下:

總結(jié)

本文從KVO三種類型的Crash進(jìn)行分析,使用了代理模式做轉(zhuǎn)發(fā),用關(guān)聯(lián)對象監(jiān)聽dealloc時(shí)機(jī),使用__unsafe_unretained來持有不會(huì)增加引用計(jì)數(shù)但一直保有對象的指針,使用了不常見的NSHashTable來弱持有代理,最終實(shí)現(xiàn)了健壯的KVO,減少了KVO系統(tǒng)實(shí)現(xiàn)的問題導(dǎo)致的不愉快的使用體驗(yàn),讓更多人感受到使用KVO機(jī)制帶來的幸福感。

源碼請點(diǎn)我~。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 上半年有段時(shí)間做了一個(gè)項(xiàng)目,項(xiàng)目中聊天界面用到了音頻播放,涉及到進(jìn)度條,當(dāng)時(shí)做android時(shí)候處理的不太好,由于...
    DaZenD閱讀 3,109評論 0 26
  • 寫在前面 程序設(shè)計(jì)語言中有各種各樣的設(shè)計(jì)模式(pattern)和與此對應(yīng)的反設(shè)計(jì)模式(anti-pattern),...
    Frankxp閱讀 5,018評論 0 23
  • 寫在前面 每次使用KVO和通知我就覺得是一件麻煩的事情,即便談不上麻煩,也可說是不方便吧,對于KVO,你需要注冊,...
    zhong_JF閱讀 503評論 1 3
  • 本文結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開會(huì)閱讀 1,736評論 1 21
  • 大白健康系統(tǒng)--iOS APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng) 前言 大白(Baymax),迪士尼動(dòng)畫《超能陸戰(zhàn)隊(duì)》中...
    鼠犬玉閱讀 17,753評論 22 158

友情鏈接更多精彩內(nèi)容