iOS記憶體洩漏檢查&原理
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
這種做法比較簡單粗暴,只適合小場景,畢竟-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成員變數