1. 程式人生 > 其它 >iOS記憶體洩漏檢查&原理

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

執行後,UIViewController會很快被釋放,如果UIViewController沒有被釋放,則打個建議日誌.

這種做法比較簡單粗暴,只適合小場景,畢竟-viewDidDisappear被呼叫可能是因為有push進來一個新的ViewController,把當前的ViewController擋住了,所以存在很多錯誤的建議日誌,需要結合實際情況具體分析.

2.4 MLeaksFinder

MLeaksFinder是微信讀書團隊使用的記憶體洩漏檢測工具. 他的主要原理如下.

  1. 當一個 UIViewController 被 pop 或 dismiss 後,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放
  2. 在一個 ViewController 被 pop 或 dismiss 一小段時間後,看看該 UIViewController,它的 view,view 的 subviews 等等是否還存在

具體實現如下:

  1. 為基類 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, @“”);
     }
    
  2. 當我們認為某個物件應該要被釋放了,在釋放前呼叫這個方法,如果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優化

  1. hook malloc / free 等16個記憶體管理函式,malloc 呼叫是非常頻繁的,一旦 hook 後能形成非常高速的 malloc / free 流。
  2. 用個雜湊表記錄已經分配的記憶體塊(key : 地址,value : (呼叫棧,塊大小,計數器等等))
  3. 在hook後的 malloc / free 方法中能拿到申請和釋放的地址。如果遇到 malloc 申請,向雜湊表中插入一個key為該地址的元素。如果遇到 free 釋放,在雜湊表中刪除一個key為該地址的元素。那麼這個雜湊表中就記錄著當前程序中所有申請的記憶體塊。
  4. 發起記憶體洩漏檢測的時候,遍歷記憶體中所有指標指向的地址,然後在雜湊表中查,如果有該地址,那麼對應的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成員變數

http://hchong.net/2020/03/11/iOS記憶體洩漏檢查-原理/ ------------------越是喧囂的世界,越需要寧靜的思考------------------ 合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。 積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。