iOS內存泄漏檢查&原理
前面羅列了iOS中常見的會導致內存泄漏的場景, 這篇文章主要說一下內存泄漏的常見檢測方式和原理.
1 內存分類
要想檢查內存泄漏, 首先我們要了解一個 app 的內存分類. 蘋果的開發者文檔里可以看到,一個 app 的內存分三類:
- Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
- 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.
Leaked memory 和 Abandoned memory 都屬於應該釋放而沒釋放的內存, 都是內存泄露.
2 常見的檢測內存泄漏的手段
2.2 系統提供的檢測手段
Leaked memory 可以用 Instrument 的 Leaks 檢測出來. Leaks的實現思路是搜索所有可能包含指向malloc內存塊指針的內存區域,比如全局數據內存塊,寄存器和所有的棧。如果malloc內存塊的地址被直接或者間接引用,則是reachable的,反之,則是leaks.
Abandoned memory,可以用 Instrument 的 Allocations 檢測出來。檢測方法是用 Mark Generation 的方式,當你每次點擊 Mark Generation 時,Allocations 會生成當前 App 的內存快照,而且 Allocations 會記錄從上回內存快照到這次內存快照這個時間段內,新分配的內存信息.
2.3 MSLeakHunter
MSLeakHunter原理很簡單, 它只檢測UIViewController和UIView,通過hook掉UIViewController的-viewDidDisappear
方法,並認為-viewDidDisappear
執行后,UIViewController會很快被釋放,如果UIViewController沒有被釋放,則打個建議日志.
這種做法比較簡單粗暴,只適合小場景,畢竟-viewDidDisappear
被調用可能是因為有push進來一個新的ViewController,把當前的ViewController擋住了,所以存在很多錯誤的建議日志,需要結合實際情況具體分析.
2.4 MLeaksFinder
MLeaksFinder是微信讀書團隊使用的內存泄漏檢測工具. 他的主要原理如下.
- 當一個 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秒后沒被釋放,就會中斷言
2.5 PLeakSniffer
PLeakSniffer, PLeakSniffer的核心監測思路是: 如果Controller被釋放了,但其曾經持有過的子對象如果還存在,那么這些子對象就是泄漏的可疑目標.
子對象(比如view)建立一個對controller的weak引用,如果Controller被釋放,這個weak引用也隨之置為nil。那怎么知道子對象沒有被釋放呢?
通過Objective C的runtime機制,遞歸的將一個Controller所有強引用的property找出,並安裝proxy監聽Ping通知.用一個單例對象每個一小段時間發出一個ping通知去ping這個子對象,如果子對象還活着就會一個pong通知。所以結論就是:如果子對象的controller已不存在,但還能響應這個ping通知,那么這個對象就是可疑的泄漏對象.
2.6 FBMemoryProfiler
FBMemoryProfiler是Facebook開源的一個用於分析iOS內存使用和檢測循環引用的工具庫.
主要是通過runtime的兩個方法, 來獲取類中的哪些 ivar 是 strong 或是 weak,都未記錄的就是基本類型和 __unsafe_unretained 的對象類型.
const char *class_getIvarLayout(Class cls) const char *class_getWeakIvarLayout(Class cls)
把對象(包括 Block 對象)當成節點,以強引用為關系建立有向圖,以深度優先遍歷該有向圖,尋找有向圖中的環,一個環就代表一個循環引用.
關於這兩個API的詳細使用, 可以參考Objective-C Class Ivar Layout 探索, runtime使用篇: class_getIvarLayout 和 class_getWeakIvarLayout
2.7 OOMDetector
OOMDetector是手Q自研的IOS內存監控組件, 主要有爆內存堆棧統計和內存泄漏檢測兩個功能. 主要工作原理如下:
Hook iOS系統底層內存分配的相關方法(包括malloc_zone相關的堆內存分配以及vm_allocate對應的VM內存分配方法). 跟蹤並記錄進程中每個對象內存的分配信息,包括分配堆棧、累計分配次數、累計分配內存等,這些信息也會被緩存到進程內存中.
在程序可訪問的進程內存空間中,是否有“指針變量”指向對應的內存塊,那些在整個進程內存空間都沒有指針指向的內存塊,就是我們要找的泄漏內存塊. 在iOS系統中,可能包含指針變量的內存區域有堆內存、棧內存、全局數據區和寄存器,OOMDetector 通過對這些區域遍歷掃描即可找到所有可能的“指針變量”,整個掃描流程結束后都沒有“指針變量”指向的內存塊即是泄漏內存塊.
為了避免內存訪問沖突,掃描過程需要掛起所有線程,整個過程會卡住程序1-2秒
2.8 OOMDetector優化
- hook malloc / free 等16個內存管理函數,malloc 調用是非常頻繁的,一旦 hook 后能形成非常高速的 malloc / free 流。
- 用個哈希表記錄已經分配的內存塊(key : 地址,value : (調用棧,塊大小,計數器等等))
- 在hook后的 malloc / free 方法中能拿到申請和釋放的地址。如果遇到 malloc 申請,向哈希表中插入一個key為該地址的元素。如果遇到 free 釋放,在哈希表中刪除一個key為該地址的元素。那么這個哈希表中就記錄着當前進程中所有申請的內存塊。
- 發起內存泄漏檢測的時候,遍歷內存中所有指針指向的地址,然后在哈希表中查,如果有該地址,那么對應的value的計數器加一,如果沒有則跳過。遍歷完了之后,查哈希表中所有元素的計數器,顯然計數器為 0 的內存塊就是泄漏的,沒有一個指針指向他,因為如果有指針指向他,他的計數器會被加一。
發現泄漏后,把value中的調用棧,地址等等上報到后台,程序員根據調用棧就能找到相關代碼進行泄漏修復。
3 HOOk
OC 的方法之所以可以 HOOK 是因為它的運行時特性,OC 的方法調用在底層都是 msg_send(id,SEL)的形式,這為我們提供了交換方法實現(IMP)的機會,但 C 函數在編譯鏈接時就確定了函數指針的地址偏移量(Offset),這個偏移量在編譯好的可執行文件中是固定的,而可執行文件每次被重新裝載到內存中時被系統分配的起始地址(在 lldb 中用命令image List獲取)是不斷變化的.
既然 C 函數的指針地址是相對固定且不可修改的,那么 fishhook 又是怎么實現 對 C 函數的 HOOK 呢?其實內部/自定義的 C 函數 fishhook 也 HOOK 不了,它只能HOOK Mach-O 外部(共享緩存庫中)的函數。fishhook 利用了 MachO 的動態綁定機制, 蘋果的共享緩存庫不會被編譯進我們的 MachO 文件,而是在動態鏈接時才去重新綁定.
蘋果采用了PIC(Position-independent code)技術成功讓 C 的底層也能有動態的表現:
- 編譯時在 Mach-O 文件 _DATA 段的符號表中為每一個被引用的系統 C 函數建立一個指針(8字節的數據,放的全是0),這個指針用於動態綁定時重定位到共享庫中的函數實現。
- 在運行時當系統 C 函數被第一次調用時會動態綁定一次,然后將 Mach-O 中的 _DATA 段符號表中對應的指針,指向外部函數(其在共享庫中的實際內存地址)。
fishhook 正是利用了 PIC 技術做了這么兩個操作:
- 將指向系統方法(外部函數)的指針重新進行綁定指向內部函數/自定義 C 函數。
- 將內部函數的指針在動態鏈接時指向系統方法的地址。
這樣就把系統方法與自己定義的方法進行了交換,達到 HOOK 系統 C 函數(共享庫中的)的目的
fishHook的具體原理可以參考動態修改 C 語言函數的實現 和 fishhook的實現原理淺析兩篇文章
參考資料:
1.iOS內存深入探索之Leaks
2.iOS 線上內存泄漏檢測方案與結果
3.【騰訊開源】iOS爆內存問題解決方案-OOMDetector組件
4.MLeaksFinder: 精准 iOS 內存泄露檢測工具
5.iOS內存泄漏自動檢測盤點
6.iOS內存泄漏自動檢測工具PLeakSniffer
7.FBRetainCycleDetector解析——獲取一般對象的Strong成員變量