精準 iOS 內存泄露檢測工具

MLeaksFinder:精準 iOS 內存泄露檢測工具

背景

日常咱們都會用 Instrument 的 Leaks / Allocations 或其餘一些開源庫進行內存泄露的排查,但它們都存在各類問題和不便,咱們逐個來看這些工具的使用和存在的問題。html

Leaks

先看看 Leaks,從蘋果的開發者文檔裏能夠看到,一個 app 的內存分三類:git

  • Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).github

  • Abandoned memory: Memory still referenced by your application that has no useful purpose.網絡

  • Cached memory: Memory still referenced by your application that might be used again for better performance.app

其中 Leaked memory 和 Abandoned memory 都屬於應該釋放而沒釋放的內存,都是內存泄露,而 Leaks 工具只負責檢測 Leaked memory,而無論 Abandoned memory。在 MRC 時代 Leaked memory 很常見,由於很容易忘了調用 release,但在 ARC 時代更常見的內存泄露是循環引用致使的 Abandoned memory,Leaks 工具查不出這類內存泄露,應用有限。ide

Allocations

對於 Abandoned memory,能夠用 Instrument 的 Allocations 檢測出來。檢測方法是用 Mark Generation 的方式,當你每次點擊 Mark Generation 時,Allocations 會生成當前 App 的內存快照,並且 Allocations 會記錄從上回內存快照到此次內存快照這個時間段內,新分配的內存信息。舉一個最簡單的例子:工具

咱們能夠不斷重複 push 和 pop 同一個 UIViewController,理論上來講,push 以前跟 pop 以後,app 會回到相同的狀態。所以,在 push 過程當中新分配的內存,在 pop 以後應該被 dealloc 掉,除了前幾回 push 可能有預熱數據和 cache 數據的狀況。若是在數次 push 跟 pop 以後,內存還不斷增加,則有內存泄露。所以,咱們在每回 push 以前跟 pop 以後,都 Mark Generation 一下,以此觀察內存是否是無限制增加。這個方法在 WWDC 的視頻裏:Session 311 - Advanced Memory Analysis with Instruments,以及蘋果的開發者文檔:Finding Abandoned Memory 裏有介紹。post

用這種方法來發現內存泄露仍是很不方便的:ui

  • 首先,你得打開 Allocations
  • 其次,你得一個個場景去重複的操做
  • 沒法及時得知泄露,得專門作一遍上述操做,十分繁瑣

開源庫

在 GitHub 上有一些內存泄露檢測相關的項目,例如 HeapInspector-for-iOS 和 MSLeakHunterspa

HeapInspector-for-iOS 能夠說是 Allocations 的改進。它經過 hook 掉 alloc,dealloc,retain,release 等方法,來記錄對象的生命週期。具體的檢測內存泄露的方法和原理,與 Instrument 的 Allocations 一致。然而它跟 Allocations 同樣,存在的問題是,你須要一個個場景去重複的操做,還有檢測不及時。

MSLeakHunter 就簡單得多,它只檢測 UIViewController 和 UIView,經過 hook 掉 UIViewController 的 -viewDidDisappear: 方法,並認爲 -viewDidDisappear: 後,UIViewController 將很快被釋放,若是 UIViewController 沒有被釋放,則打個建議日誌。這種作法其實不是很好,-viewDidDisappear: 被調用多是由於又 push 進來一個新的 ViewController,把當前的 ViewController 擋住了,因此可能有不少錯誤的建議,須要結合你實際的操做去具體地分析日誌。

MLeaksFinder

MLeaksFinder 提供了內存泄露檢測更好的解決方案。只須要引入 MLeaksFinder,就能夠自動在 App 運行過程檢測到內存泄露的對象並當即提醒,無需打開額外的工具,也無需爲了檢測內存泄露而一個個場景去重複地操做。MLeaksFinder 目前能自動檢測 UIViewController 和 UIView 對象的內存泄露,並且也能夠擴展以檢測其它類型的對象。

MLeaksFinder 的使用很簡單,參照 https://github.com/Zepo/MLeaksFinder,基本上就是把 MLeaksFinder 目錄下的文件添加到你的項目中,就能夠在運行時(debug 模式下)幫助你檢測項目裏的內存泄露了,無需修改任何業務邏輯代碼,並且只在 debug 下開啓,徹底不影響你的 release 包。

當發生內存泄露時,MLeaksFinder 會中斷言,並準確的告訴你哪一個對象泄露了。這裏設計爲中斷言而不是打日誌讓程序繼續跑,是由於不少人不會去看日誌,斷言則能強制開發者注意到並去修改,而不是犯拖延症。

中斷言時,控制檯會有以下提示,View-ViewController stack 從上往下看,該 stack 告訴你,MyTableViewController 的 UITableView 的 subview UITableViewWrapperView 的 subview MyTableViewCell 沒被釋放。並且,這裏咱們能夠確定的是 MyTableViewController,UITableView,UITableViewWrapperView 這三個已經成功釋放了。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Possibly Memory Leak.
In case that MyTableViewCell should not be dealloced, override -willDealloc in MyTableViewCell by returning NO.
View-ViewController stack: (
MyTableViewController,
UITableView,
UITableViewWrapperView,
MyTableViewCell
)'

從 MLeaksFinder 的使用方法能夠看出,MLeaksFinder 具有如下優勢:

  • 使用簡單,不侵入業務邏輯代碼,不用打開 Instrument
  • 不須要額外的操做,你只需開發你的業務邏輯,在你運行調試時就能幫你檢測
  • 內存泄露發現及時,更改完代碼後一運行即能發現(這點很重要,你立刻就能意識到哪裏寫錯了)
  • 精準,能準確地告訴你哪一個對象沒被釋放

原理

MLeaksFinder 一開始從 UIViewController 入手。咱們知道,當一個 UIViewController 被 pop 或 dismiss 後,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設計成單例,或者持有它的強引用,但通常不多這樣作)。因而,咱們只需在一個 ViewController 被 pop 或 dismiss 一小段時間後,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在。

具體的方法是,爲基類 NSObject 添加一個方法 -willDealloc 方法,該方法的做用是,先用一個弱指針指向 self,並在一小段時間(3秒)後,經過這個弱指針調用 -assertNotDealloc,而 -assertNotDealloc 主要做用是直接中斷言。

- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @「」);
}

這樣,當咱們認爲某個對象應該要被釋放了,在釋放前調用這個方法,若是3秒後它被釋放成功,weakSelf 就指向 nil,不會調用到 -assertNotDealloc 方法,也就不會中斷言,若是它沒被釋放(泄露了),-assertNotDealloc 就會被調用中斷言。這樣,當一個 UIViewController 被 pop 或 dismiss 時(咱們認爲它應該要被釋放了),咱們遍歷該 UIViewController 上的全部 view,依次調 -willDealloc,若3秒後沒被釋放,就會中斷言。

在這裏,有幾個問題須要解決:

  1. 不入侵開發代碼

    這裏使用了 AOP 技術,hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法,關於如何 hook,請參考 Method Swizzling

  2. 遍歷相關對象

    在實際項目中,咱們發現有時候一個 UIViewController 被釋放了,但它的 view 沒被釋放,或者一個 UIView 被釋放了,但它的某個 subview 沒被釋放。這種內存泄露的狀況很常見,所以,咱們有必要遍歷基於 UIViewController 的整棵 View-ViewController 樹。咱們經過 UIViewController 的 presentedViewController 和 view 屬性,UIView 的 subviews 屬性等遞歸遍歷。對於某些 ViewController,如 UINavigationController,UISplitViewController 等,咱們還須要遍歷 viewControllers 屬性。

  3. 構建堆棧信息

    須要構建 View-ViewController stack 信息以告訴開發者是哪一個對象沒被釋放。在遞歸遍歷 View-ViewController 樹時,子節點的 stack 信息由父節點的 stack 信息加上子結點信息便可。

  4. 例外機制

    對於有些 ViewController,在被 pop 或 dismiss 後,不會被釋放(好比單例),所以須要提供機制讓開發者指定哪一個對象不會被釋放,這裏能夠經過重載上面的 -willDealloc 方法,直接 return NO 便可。

  5. 特殊狀況

    對於某些特殊狀況,釋放的時機不大同樣(好比系統手勢返回時,在劃到一半時 hold 住,雖然已被 pop,但這時還不會被釋放,ViewController 要等到徹底 disappear 後才釋放),須要作特殊處理,具體的特殊處理視具體狀況而定。

  6. 系統View

    某些系統的私有 View,不會被釋放(多是系統 bug 或者是系統出於某些緣由故意這樣作的,這裏就不去深究了),所以須要創建白名單

  7. 手動擴展

    MLeaksFinder目前只檢測 ViewController 跟 View 對象。爲此,MLeaksFinder 提供了一個手動擴展的機制,你能夠從 UIViewController 跟 UIView 出發,去檢測其它類型的對象的內存泄露。以下所示,咱們能夠檢測 UIViewController 底下的 View Model:

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}

這裏的原理跟上面的是同樣的,宏 MLCheck() 作的事就是爲傳進來的對象創建 View-ViewController stack 信息,並對傳進來的對象調用 -willDealloc 方法。

將來

MLeaksFinder 目前還在起步階段,它的內存泄露檢測的想法是很簡單,很直接的。雖然目前只能自動地檢測 UIViewController 和 UIView 相關的對象,然而在咱們幾個大的項目中,已經起到很大的做用,幫助咱們發現不少歷史存在的內存泄露,並且確保新提交的 UI 相關代碼不會引進新的問題。MLeaksFinder 會繼續探索覆蓋更廣的狀況,提供更全面的檢測,包括網絡層,數據存儲層等等。

相關文章
相關標籤/搜索