本文列舉的不是查找 iOS 應(yīng)用內(nèi)存問(wèn)題的必要流程,只是講述筆者在干這檔子事兒的時(shí)候,可能會(huì)用到的手段而已。??
Clang Static Analyzer
在應(yīng)用運(yùn)行起來(lái)之前,我們就能通過(guò) Xcode 集成的靜態(tài)分析工具 Clang Static Analyzer,分析下面幾種類(lèi)型的代碼問(wèn)題:
- Logic flaws: 讀取未初始化的變量、解引用空指針等等;
- Memory management flaws:內(nèi)存泄露等;
- Dead store:沒(méi)被使用的變量;
- API usage flaws:返回值等不符合 API 的要求;
靜態(tài)分析的結(jié)果通常是基于某種假設(shè)邏輯而推斷產(chǎn)生的,所以是否要修改代碼這一決定不能完全交給 analyzer。諸如“如何忽略特定的 dead store”、“如何告訴 analyzer 循壞塊一定會(huì)進(jìn)入”等常見(jiàn)問(wèn)題,在這個(gè) FAQ 中都列了出來(lái)。
更方便的是,使用 scan-build 能在命令行中分析,還能導(dǎo)出 html 方便查看分析結(jié)果:
scan-build -o DIR_TO_STORE_RESULTS xcodebuild -configuration Debug -workspace PATH_TO_YOUR_WORKSPACE -scheme YOUR_SCHEME_NAME
如果你的是 project 而不是 workspace 就寫(xiě) -project PATH_TO_YOUR_PROJECT
……好吧這就是 xcodebuild 的參數(shù)而已跟 scan-build 無(wú)關(guān)。
上圖僅僅是 Bug Summary,下面才是 Report,比較私密就不截了,內(nèi)容和用 Xcode 分析一樣,給出具體邏輯的判定流程,指出錯(cuò)誤。筆者覺(jué)得這一步可以添加到持續(xù)集成中,這樣每天早上都能看到前一天可能新增的代碼問(wèn)題。
Leaks
Leaked Memory 指的是丟失引用的內(nèi)存。打開(kāi) Leaks,盡管操作你的業(yè)務(wù)邏輯就好,Leaks Check 那一欄出現(xiàn)紅色交叉就是檢測(cè)到泄露的地方,選中 Leaks Object,點(diǎn)擊右邊的 E 按鈕(Extended Detail)可以看到調(diào)用棧,幫助定位問(wèn)題。
不過(guò)要注意的是,任何一個(gè)大型軟件都不可能是完美的,所以 Cocoa Touch 本身也是可能發(fā)生泄漏的,所以對(duì)于那些調(diào)用棧沒(méi)有用戶代碼而且你絞盡腦汁都想不明白的泄漏,說(shuō)不定可以無(wú)視它。
但對(duì)于 Abandoned Memory 來(lái)說(shuō),Leaks 就無(wú)能為力了。Abandoned Memory 是那些引用沒(méi)有丟失,但不再被使用的內(nèi)存,在 ARC 下常見(jiàn)于由循環(huán)引用導(dǎo)致的內(nèi)存不能釋放。譬如說(shuō) Push 進(jìn)入某個(gè) ViewController 之后,再 Pop 回去,假設(shè)這個(gè) ViewController 與其實(shí)例變量有循環(huán)引用,那么它們將不會(huì)被釋放,Leaks 沒(méi)辦法檢測(cè)到這種內(nèi)存泄露,于是乎需要 Allocations 這個(gè)工具(通常會(huì)新建個(gè)模板把這兩個(gè)工具放在一起用,嗯還有 VM Tracker)。
Allocations
在 Push 之前,使用 Allocations 對(duì)當(dāng)前內(nèi)存中的 Heap 和 VM Region 進(jìn)行 Mark Generation,記錄內(nèi)存的使用狀態(tài),接著 Push 進(jìn)一個(gè) ViewController 后 Pop 回去,再 Mark 一遍,這樣重復(fù)業(yè)務(wù)邏輯好幾次,就能發(fā)現(xiàn)每次內(nèi)存的增長(zhǎng)情況。前一兩次的快照可能有一些 lib 的緩存,所以要特別留意之后的內(nèi)存增長(zhǎng)情況。
每次 Mark Generation 之后,Growth 還是會(huì)不斷變化的,這里筆者不清楚是系統(tǒng)延遲釋放內(nèi)存,還是工具還來(lái)不及作對(duì)比。我嘗試了下,多點(diǎn) Mark Generation 幾次或者手動(dòng)觸發(fā) Memory Warning 可以讓前面的 Growth 快一點(diǎn)穩(wěn)定下來(lái)。
將 Track Display 改為 Allocation Density 可以留意到內(nèi)存分配的動(dòng)態(tài),突然出現(xiàn)的尖峰就是在那段時(shí)間拼命地在分配內(nèi)存,稍微注意下是什么操作引起,說(shuō)不定能優(yōu)化是吧?畢竟堆上的內(nèi)存分配可不是個(gè)輕松活。
我們還能再下面看到 Allocation Type 這個(gè)選項(xiàng),Heap 和 VM Regions上的內(nèi)存都是我們要關(guān)注的點(diǎn)。我們實(shí)例化的對(duì)象、malloc 分配的內(nèi)存都在 Heap 上,而 VM Regions 上是我們不能直接接觸的內(nèi)存,但這不代表我們不用理會(huì) VM Regions 任由其增長(zhǎng)。比如 UIImage 對(duì)象在 Heap 上可能只有幾十字節(jié),但它代表的圖片在解壓縮后在 Image Region 上可能會(huì)占用幾十kb,甚至更大。再比如,UIView 對(duì)象本身也不大,但是 CALayer backing stores 就不一定不起眼了。
還有,回到 Record Settings,我們常常需要在 Recorded Types 上設(shè)置忽略或追蹤特定類(lèi)型的對(duì)象以便于我們分析(這里凸顯了給自己項(xiàng)目的類(lèi)添加前綴的好處),特別是當(dāng)你已經(jīng)將目標(biāo)鎖定在某些類(lèi)上時(shí),這么做可以讓我們事半功倍。
Debug Memory Graph
這是 Xcode 8 的新功能,就是點(diǎn)了 Debug Memory Graph 之后能看到當(dāng)前在內(nèi)存中的各種對(duì)象間引用關(guān)系。如果出現(xiàn)紫色框框的感嘆號(hào),那么可能是有內(nèi)存問(wèn)題,具體看上面的提示。有一點(diǎn)要提及的是,如果想看與某個(gè)對(duì)象的 backtrace,要去 Edit-Scheme-Diagnostics 中把 Malloc Stack 的復(fù)選框給勾上,否則你可能找不這玩意兒是在哪里創(chuàng)建的……
第三方的檢測(cè)工具
除了官方的提供的工具之外,筆者還會(huì)使用一些的第三個(gè)工具,最早使用過(guò) HeapInspector-for-iOS 代替 Allocations,直接就在應(yīng)用內(nèi)分析 Heap 的增長(zhǎng)情況。還有 FBRetainCycleDetector,通過(guò) DFS 尋找以對(duì)象為節(jié)點(diǎn)、強(qiáng)引用關(guān)系為邊的有向圖中的環(huán)路,從而達(dá)到找到循環(huán)引用的目的。上面的兩個(gè)庫(kù)都有些缺點(diǎn),比如前者也是要不斷重復(fù)業(yè)務(wù)邏輯并推斷,后者要修改代碼才能查找環(huán)路。而 WeRead 團(tuán)隊(duì)出品的 MLLeaksFinder 解決了這兩個(gè)問(wèn)題,具體可以看他們博客的介紹《MLeaksFinder:精準(zhǔn) iOS 內(nèi)存泄露檢測(cè)工具》以及《MLeaksFinder 新特性》,這個(gè)工具在實(shí)踐中還是比較好用的。
最后
還是那句話,上面的所羅列不是查找 iOS 應(yīng)用內(nèi)存問(wèn)題的必要流程,僅僅是常用的手段。另外,還有一點(diǎn)是調(diào)試的心態(tài),當(dāng)筆者要去查一個(gè)應(yīng)用的內(nèi)存問(wèn)題時(shí),筆者會(huì)像個(gè)XX一樣,總是想找到內(nèi)存問(wèn)題,不找到不甘心??,然后會(huì)懷疑自己對(duì)項(xiàng)目代碼不夠了解、自己調(diào)試經(jīng)驗(yàn)不足以及工具使用的姿勢(shì)不正確等等。這顯然是一種心理疾病,望各位以此為鑒,將更多的精力投入到新需求的開(kāi)發(fā)中,而不是深陷調(diào)試旋渦無(wú)法自拔。
話說(shuō)剛來(lái)公司一個(gè)月就給項(xiàng)目寫(xiě)了兩個(gè) bug,也是個(gè)挺悲傷的故事??。