iOS之深入定制基于PLeakSniffer和MLeaksFinder的内存泄漏检测工具
生活随笔
收集整理的這篇文章主要介紹了
iOS之深入定制基于PLeakSniffer和MLeaksFinder的内存泄漏检测工具
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
一、背景
- 在編寫日常業(yè)務(wù)代碼時(shí),或多或少都會(huì)引入一些導(dǎo)致內(nèi)存泄漏的代碼,而這種行為又很難被監(jiān)控,這就導(dǎo)致應(yīng)用內(nèi)存泄漏的口子越開越大,直接影響到線上應(yīng)用的穩(wěn)定性。
- 雖然 Xcode 的 Instrucment 提供了 Leaks 和 Allocations 工具能精準(zhǔn)地定位內(nèi)存泄漏問題,但是這種方式相對(duì)比較繁瑣,需要開發(fā)人員頻繁地去操作應(yīng)用界面,以觸發(fā)泄漏場(chǎng)景,所以 Leaks 和 Allocations 更加適合定期組織的大排查,作為監(jiān)測(cè)手段,則顯得笨重。
- 對(duì)于內(nèi)存泄漏的監(jiān)測(cè),業(yè)內(nèi)已經(jīng)有了兩款成熟的開源工具,分別是 PLeakSniffer 和 MLeaksFinder。
-
- PLeakSniffer 使用 Ping-Pong 方式監(jiān)測(cè)對(duì)象是否存活,在進(jìn)入頁面時(shí),創(chuàng)建控制器關(guān)聯(lián)的一系列對(duì)象代理,根據(jù)這些代理在控制器銷毀時(shí)能否響應(yīng) Ping 判斷代理對(duì)應(yīng)的對(duì)象是否泄漏。
-
- MLeaksFinder 則是在控制器銷毀時(shí),延遲 3s 后再向監(jiān)測(cè)對(duì)象發(fā)送消息,根據(jù)監(jiān)測(cè)對(duì)象能否響應(yīng)消息判斷其是否泄漏。
- PLeakSniffer 和 MLeaksFinder 這兩個(gè)基本能覆蓋大部分對(duì)象泄漏或者延遲釋放的場(chǎng)景,考慮到性能損耗以及內(nèi)存占用因素,個(gè)人更偏向于第二種方案。
- 個(gè)人使用 MLeaksFinder,還存在以下問題:
-
- 沒有處理集合對(duì)象;
-
- 沒有處理對(duì)象持有的屬性;
-
- 每個(gè)對(duì)象都觸發(fā) 3s 延遲機(jī)制,沒有緩存后統(tǒng)一處理;
-
- 檢測(cè)結(jié)果輸出分散。
- PLeakSniffer 存在以下問題:
-
- 沒有處理集合對(duì)象;
-
- 處理對(duì)象持有屬性時(shí),系統(tǒng)類過濾不全面;
-
- 處理對(duì)象持有屬性時(shí),通過 KVC 訪問屬性導(dǎo)致一些懶加載的觸發(fā);
-
- 無法處理未添加到視圖棧中的泄漏視圖;
-
- 檢測(cè)結(jié)果輸出分散。
- 對(duì)于檢測(cè)到泄漏對(duì)象的交互處理,兩者都提供了終端 log 輸出和 alert 提示功能,MLeaksFinder 甚至可以直接通過斷言中斷應(yīng)用,這種提示在開發(fā)階段尚可接受,但是在提測(cè)階段,強(qiáng)交互會(huì)給測(cè)試人員造成困擾。至于為什么在提測(cè)階段還要集成泄漏監(jiān)測(cè)工具,主要有兩個(gè)原因:
-
- 應(yīng)用功能過多的情況下,開發(fā)人員無法兼顧到老頁面,一些老頁面的泄漏場(chǎng)景可以通過測(cè)試人員在測(cè)試時(shí)觸發(fā),收集之后再統(tǒng)一處理;
-
- 在組件化開發(fā)環(huán)境下,開發(fā)人員可能并沒有集成泄漏監(jiān)測(cè)工具,這種情況下,需要在提測(cè)階段統(tǒng)一收集沒有解決的泄漏問題。
- 因此,對(duì)于監(jiān)測(cè)輸出的訴求有兩點(diǎn):
-
- 開發(fā)時(shí),通過終端日志提示開發(fā)者出現(xiàn)了內(nèi)存泄漏;
-
- 提測(cè)時(shí),收集內(nèi)存泄漏的信息并上傳至效能后臺(tái),統(tǒng)一分配處理;
二、監(jiān)測(cè)入口
- 和 MLeaksFinder 一樣,選擇延遲 3s 的機(jī)制來判斷對(duì)象是否泄漏,但是實(shí)現(xiàn)的細(xì)節(jié)略有差別。首先,監(jiān)測(cè)入口變更為 viewDidDisappear: 方法,只需在控制器被父控制器中移除或者被 Dismissed 時(shí),觸發(fā)監(jiān)測(cè)動(dòng)作即可:
- 在應(yīng)用中,還有一種監(jiān)測(cè)入口出現(xiàn)在變更根控制器時(shí),由于直接設(shè)置根控制器不會(huì)觸發(fā) viewDidDisappear 方法,所以需要另外設(shè)置 :
- 為了能夠統(tǒng)一處理控制器及其持有對(duì)象,可以像 PLeakSniffer 一樣,給每個(gè)對(duì)象包裝一層代理 :
- 只要 host 釋放了而 target 沒釋放,則視 target 已泄漏,如果 host 未釋放,則不檢測(cè) target,然后使用一個(gè) collector 去收集這些對(duì)象對(duì)應(yīng)的 proxy ,在收集完之后統(tǒng)一監(jiān)測(cè) collector 中的所有 proxy ,這樣就可以在一個(gè)控制器監(jiān)測(cè)完成后,統(tǒng)一上傳監(jiān)測(cè)出的泄漏點(diǎn) :
三、收集對(duì)象信息
- 因?yàn)橐獙?duì)不同的類做特異化處理,因此先定義一個(gè)協(xié)議,通過這個(gè)協(xié)議中的 collect 方法去收集不同類實(shí)例化對(duì)象的 proxy :
- 關(guān)鍵在于如何讓 NSObject 實(shí)現(xiàn)此協(xié)議,主要有四個(gè)步驟 :
-
- 過濾系統(tǒng)類調(diào)用;
-
- 向 collector 添加封裝的 proxy;
-
- 循環(huán)遍歷對(duì)象對(duì)應(yīng)的非系統(tǒng)類 / 父類屬性,找出 copy / strong 類型屬性,并獲取其對(duì)應(yīng)的成員變量值;
-
- 向收集的所有成員變量對(duì)象發(fā)送 collect 方法。
- NSObject 實(shí)現(xiàn) collect 協(xié)議方法后,其子類就可以通過這個(gè)方法遞歸地收集名下需要監(jiān)測(cè)的屬性信息。比如對(duì)于集合類型 NSArray ,實(shí)現(xiàn)協(xié)議方法如下,表示收集自身和每個(gè)集合元素的信息,不過由于 NSArray 是系統(tǒng)類,所以其實(shí)例化對(duì)象并不會(huì)被收集進(jìn) collector ,如果要收集系統(tǒng)類的屬性信息,只能通過讓系統(tǒng)類實(shí)現(xiàn)協(xié)議并重載 collect 方法,手動(dòng)向?qū)傩灾蛋l(fā)送 collect 消息實(shí)現(xiàn),UIViewController 的 childViewControllers、presentedViewController、view 屬性也同理 :
- 需要注意的是,直接調(diào)用屬性的 getter 方法獲取屬性值,可能會(huì)觸發(fā)屬性懶加載,導(dǎo)致出現(xiàn)意料之外的問題 (比如調(diào)用 UIViewController 的 view 會(huì)觸發(fā) viewDidLoad),所以要通過 object_getIvar 去獲取屬性對(duì)應(yīng)的成員變量值。當(dāng)然,這種處理方式會(huì)導(dǎo)致無法收集某些沒有對(duì)應(yīng)成員變量值的屬性,比如關(guān)聯(lián)對(duì)象、控制器的 view 等屬性,權(quán)衡利弊之后,可以選擇忽略這種屬性的監(jiān)測(cè)。
- 除了收集必要的對(duì)象信息之外,我還記錄了監(jiān)測(cè)對(duì)象的引用路徑信息,也就是上面 LM_CTX_D 宏做的事情。有些情況下,對(duì)象的引用路徑能幫助我們發(fā)現(xiàn),路徑上的哪些操作導(dǎo)致了對(duì)象的泄漏,特別是在網(wǎng)頁上瀏覽泄漏信息時(shí),如果只有泄漏對(duì)象類和引用泄漏對(duì)象類兩個(gè)信息,脫離了對(duì)象泄漏時(shí)的上下文環(huán)境,會(huì)增加修復(fù)的難度。有了引用路徑信息后,輸出的泄漏信息如下 :
四、過濾系統(tǒng)類
- 系統(tǒng)類信息并不是需要關(guān)心的,過濾掉并不會(huì)影響到最終的監(jiān)測(cè)結(jié)果。目前我嘗試了兩種方式來確定一個(gè)類是否為系統(tǒng)類:
-
- 通過類所在 NSBundle 的路徑;
-
- 通過類所在地址。
- 第一種的邏輯較為簡(jiǎn)單,代碼如下:
- 應(yīng)用的主二進(jìn)制文件,和開發(fā)者添加的 embeded frameworks 都會(huì)在固定的文件目錄下,所以直接比對(duì)路徑前綴即可。
- 第二種方式的實(shí)現(xiàn)步驟如下:
-
- 遍歷所有的 image ,通過 image 的名稱判斷是否為系統(tǒng) image;
-
- 緩存所有系統(tǒng) image 的起始位置,也就是 mach_header 的地址;
-
- 判斷類是否為系統(tǒng)類時(shí),使用 dladdr 函數(shù)獲取類所在 image 的信息,通過 dli_fbase 字段獲取起始地址;
-
- 比對(duì) image 的起始地址得知是否為系統(tǒng)類。
- 實(shí)際嘗試下來后,發(fā)現(xiàn)第二種方式耗時(shí)會(huì)比第一種多,dladdr 函數(shù)占用了大部分時(shí)間(內(nèi)部會(huì)遍歷所有 image 的開始結(jié)束地址,和傳入的地址進(jìn)行比對(duì)),所以最終選擇了第一種方式作為判斷依據(jù)。
- 過濾系統(tǒng)類時(shí),針對(duì)那種會(huì)自泄漏的對(duì)象,需要進(jìn)行特殊處理,不予過濾。比如 NSTimer / CADisplayLink 對(duì)象的常見內(nèi)存泄漏場(chǎng)景,除了 target 強(qiáng)引用控制器造成循環(huán)引用域外,還有一種是打破了循環(huán)引用但沒有在控制器銷毀時(shí)執(zhí)行 invalidate 操作,因?yàn)?NSTimer 由 RunLoop 持有,不手動(dòng)停止的情況下,就會(huì)造成泄漏。
五、局限性
- 基于延時(shí)的內(nèi)存泄漏監(jiān)測(cè)機(jī)制雖然適用于大部分視圖、控制器和一般屬性的泄漏場(chǎng)景,但是還有少部分情況,這種機(jī)制無法處理,比如單例對(duì)象和共享對(duì)象。
- 首先說下單例對(duì)象,假設(shè)有 singleton 屬性,其 getter 方法返回 Singleton 單例,這時(shí)延時(shí)監(jiān)測(cè)機(jī)制無法自動(dòng)過濾這種情況,依然會(huì)認(rèn)為 singleton 泄漏了。有一種檢測(cè)屬性返回值是否為單例的方法,就是向返回值對(duì)應(yīng)類發(fā)送 init 或者 share 相關(guān)方法,通過方法返回值和屬性返回值的對(duì)比結(jié)果來判斷,但是事實(shí)上我們無法確定業(yè)務(wù)方的單例是否重寫了 init,也無法獲知具體的單例類方法,所以這種方案適用面比較局限。單例對(duì)象的處理,目前還是通過白名單的方式處理較為穩(wěn)妥。
- 共享對(duì)象的應(yīng)用場(chǎng)景就比較普遍了,比如現(xiàn)有 A,B 頁面,A 頁面持有模型 M ,在跳轉(zhuǎn)至 B 頁面時(shí),會(huì)將 M 傳遞給 B ,B 強(qiáng)引用了 M ,當(dāng) B 銷毀時(shí), M 不會(huì)銷毀,而 M 又是 B 某個(gè)屬性的值,所以監(jiān)測(cè)機(jī)制會(huì)判斷 M 泄漏了,實(shí)際上 M 只是 A 傳遞給 B 的共享對(duì)象。在一個(gè)控制器做完檢測(cè)就需要上傳至效能后臺(tái)的情況下,共享對(duì)象還沒有很好的處理方法,后期考慮結(jié)合 FBRetainCycleDetector 查找泄漏對(duì)象的循環(huán)引用信息,然后一并上傳至效能后臺(tái),方便排查這種情況。因?yàn)槊看?pop 都使用 FBRetainCycleDetector 檢測(cè)控制器會(huì)比較耗時(shí)、甚至?xí)斐裳舆t釋放和卡頓,所以先用延時(shí)機(jī)制找出潛在的泄漏對(duì)象,再使用 FBRetainCycleDetector 檢測(cè)這些泄漏對(duì)象,能極大得減少需要處理的對(duì)象數(shù)量。最終網(wǎng)頁呈現(xiàn)的效果如下:
六、總結(jié)
- 像內(nèi)存泄露這種問題,最好在應(yīng)用初期就開始著手監(jiān)測(cè)和解決,否則當(dāng)應(yīng)用功能代碼逐漸增多后,回過頭來處理這種問題費(fèi)時(shí)費(fèi)力,還是比較麻煩的。
- 基于 PLeakSniffer 和 MLeaksFinder 監(jiān)測(cè)工具的基礎(chǔ)上,結(jié)合團(tuán)隊(duì)業(yè)務(wù)情況,進(jìn)行了一些的改造,添加了集合對(duì)象的處理、引用路徑的記錄、對(duì)象的統(tǒng)一檢測(cè)等功能,優(yōu)化了部分有問題的代碼,在一定程度上提升了延時(shí)機(jī)制的可用性。
總結(jié)
以上是生活随笔為你收集整理的iOS之深入定制基于PLeakSniffer和MLeaksFinder的内存泄漏检测工具的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS之深入解析WKWebView的坑点
- 下一篇: Swift之深入解析异步函数async/