iOS-記憶體洩漏檢測

語言: CN / TW / HK

為什麼大家在開發中,一直比較注重記憶體的問題。因為雖然現在是 ARC 機制,但是如果我們處理不好堆上面的記憶體問題還是會出現記憶體洩漏的,如果一直洩漏多少記憶體也是不夠用的,最終就會導致程式崩潰。

檢測方案:

  • 手動檢測,實現 dealloc 方法,離開當前類是否會呼叫;

  • 工具檢測,使用 Xcode 自帶的工具進行檢測;

  • 自動化檢測,自動檢測出發生記憶體洩漏的地方,並打印出對應的資訊;

一、介紹手動檢測

可以實現 ViewController 的分類,在分類裡面實現 -(void)dealloc , 在離開頁面是看是否有資訊列印,如果列印則證明當前類沒有記憶體洩漏問題

- (void)dealloc {     NSLog(@"%s", __func__); }

二、介紹工具檢測

靜態記憶體洩漏分析方法

主要分析以下四種問題:

1、邏輯錯誤:訪問空指標或未初始化的變數等;

2、記憶體管理錯誤:如記憶體洩漏等;

3、宣告錯誤:從未使用過的變數;

4、Api呼叫錯誤:未包含使用的庫和框架。

如何進行靜態分析

在選單欄找到 Product ,點選 Analyze

截圖2021-11-21 下午8.23.48.png

根據執行出來的結果,一個個去具體分析。這都是工具分析出來了,需要自己甄別是不是可以忽略

截圖2021-11-22 上午9.02.52.png

截圖2021-11-22 上午8.58.59.png

動態記憶體洩漏分析方法

靜態記憶體洩漏分析不能把所有的記憶體洩漏排查出來,因為有的記憶體洩漏發生在執行時,當用戶做某些操作時才發生記憶體洩漏。Instruments 是 Xcode 自帶的檢測除錯工具,Instruments 提供了很多功能,主要包含以下這些功能:

  1. Time Profiler:CPU 分析工具分析程式碼的執行時間。
  2. Core Animation:離屏渲染,圖層混合等GPU耗時。
  3. Leaks:記憶體檢測,記憶體洩漏檢測工具。
  4. Energy Log:耗電檢測工具。
  5. Network:流量檢測工具。

工具的使用

這裡我只是介紹 Leaks 記憶體洩漏檢測工具,選擇 Xcode -> Product -> Profile,選擇 Leaks

截圖2021-11-21 下午2.48.23.png

  • 選中 Leaks,在 Leaks 所在欄中選擇 CallTree;
  • Call Tree 會給我們大概的位置,這個時候需要縮小範圍、篩選資料;
  • 點選下方的 CallTree ,發現有這幾個篩選項:
  • Separate by Thread :按執行緒分開做分析,這樣更容易揪出那些吃資源的問題執行緒。
  • Invert Call Tree :反向輸出呼叫樹。把呼叫層級最深的方法顯示在最上面,更容易找到最耗時的操作。
  • Hide System Libraries:隱藏系統庫檔案。過濾掉各種系統呼叫,只顯示自己的程式碼呼叫。
  • Flattern Recursion:拼合遞迴。將同一遞迴函式產生的多條堆疊(因為遞迴函式會呼叫自己)合併為一條。

截圖2021-11-21 下午8.21.08.png

將以上勾選之後便可以看到對應的具體程式碼,此時選中一行耗時操作雙擊便可以進入到對應的程式碼中並且顯示詳細的消耗時間。這樣就找到了問題。

三、介紹自動化檢測

MLeaksFinder

MLeaksFinder 是 WeRea d團隊開源的一款檢測 iOS 記憶體洩漏的框架,對程式碼沒有侵入性,而且其使用非常簡單,只需要引入專案中,如果有記憶體洩漏,3秒後自動彈出 alert 來顯示捕捉的資訊。它預設只檢測應用裡 UIViewController 和 UIView 物件的洩漏情況。因為一般應用裡記憶體洩漏影響最嚴重的就是這兩種記憶體佔用比較高的物件,它也可以在程式碼裡設定擴充套件以檢測其他型別的物件洩漏情況

MLeaksFinder的實現原理

一般情況下,當一個 UIViewController 被 pop 或者 dismiss 掉後,它的 view 和 view 的subview等也會很快地被釋放掉,除非我們把它設定為單例或者還有強引用指向它。MLeaksFinder 的做法就是根據這種基本情況,在一個 UIViewController 被 pop 或者 dismiss 掉3秒後,看看它的 view 和 view 的 subview 等是否還存在,如果還存在,就意味著有可能有記憶體洩漏發生,彈框提醒使用者。

``` - (BOOL)willDealloc { __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf assertNotDealloc]; }); return YES; }

  • (void)assertNotDealloc { NSAssert(NO, @“”); }

```

MLeaksFinder的特性

  • 不侵入程式碼
  • 白名單機制
  • 可以構建洩漏堆疊
  • 擴充套件性和其他特殊處理

MLeaksFinder 雖然幫我們找到了記憶體洩漏的物件,但是我們具體不知道引起迴圈引用的鏈條,使用者還要自己去看程式碼進行排查,這是很浪費時間的。因為記憶體洩漏一般都是迴圈引用導致的。

FBRetainCycleDetector介紹

FBRetainCycleDetectorFaceBook開源的用於檢測強引用迴圈的工具。預設是在DEBUG環境中啟用,當然你也可以通過設定RETAIN_CYCLE_DETECTOR_ENABLED以始終開啟。使用這個工具可以傳入應用記憶體裡的任意一個 Objective-C 物件,FBRetainCycleDetector 會查詢以該物件為根節點的強引用樹中有沒有迴圈引用。

``` #import

_handlerBlock = ^{ NSLog(@"%@", self); };

FBRetainCycleDetector detector = [FBRetainCycleDetector new]; [detector addCandidate:self]; NSSet retainCycles = [detector findRetainCycles]; NSLog(@"%@", retainCycles);

```

這兩個工具一起搭配使用真是如虎添翼,很容易排查出記憶體的問題。所以查詢記憶體洩漏現在一般是兩個工具一起用,先用 MLeaksFinder 找出洩漏的物件,然後再用 FBRetainCycleDetector 檢測該物件有沒有迴圈引用,如果有,根據找出來的迴圈引用鏈條去檢視修改程式碼就方便很多了。

具體使用

引入 MLeaksFinder 庫的時候,也預設引入了 FBRetainCycleDetector 庫,預設在 Debug 環境下起作用

pod 'MLeaksFinder'

但是拉取後執行會報出下面這個錯誤

Cannot initialize a parameter of type 'id<NSCopying> _Nonnull' with an rvalue of type 'Class'

有兩種辦法可以解決:

  1. 把報錯的這句程式碼改成

layoutCache[(id<NSCopying>)currentClass] = ivars 2. 在 Podfile 檔案里加上這些配置,官方臨時解決方案

``` post_install do |installer|

Fix for XCode 12.5

find_and_replace("Pods/FBRetainCycleDetector/FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.mm", "layoutCache[currentClass] = ivars;", "layoutCache[(id)currentClass] = ivars;") end

def find_and_replace(dir, findstr, replacestr)   Dir[dir].each do |name|       text = File.read(name)       replace = text.gsub(findstr,replacestr)       if text != replace           puts "Fix: " + name           File.open(name, "w") { |file| file.puts replace }           STDOUT.flush       end   end   Dir[dir + '/'].each(&method(:find_and_replace)) end ``` 配置完是這樣的

截圖2021-11-22 上午10.36.21.png

常見的記憶體洩露場景:

目前,在 ARC 環境下,導致記憶體洩漏的根本原因是程式碼中存在迴圈引用,從而導致一些記憶體無法釋放,最終導致 dealloc() 方法無法被呼叫。主要原因大概有一下幾種型別:

  1. Retain Cycle,Block 強引用
  2. NSTimer使用不當
  3. 第三方提供方法造成的記憶體洩漏
  4. CoreFoundation 方式申請的記憶體,忘記釋放

下面簡單舉個例子看看是否可以檢測到記憶體洩露:

  • block 使用不當導致的迴圈引用

截圖2021-11-21 下午5.21.15.png

當離開這個介面是,過秒鐘就會彈出這個彈框,點選 Retain Cycle 查詢迴圈引用,這時清晰的分析中因為 VC 裡 testView 使用 block 導致的,上面圖中發現是因為這句導致[self test];

截圖2021-11-21 下午5.22.03.png

截圖2021-11-21 下午5.22.14.png

  • delegate 導致的迴圈引用

這個是因為在宣告 delegate 屬性時使用了 strong 修飾了

截圖2021-11-21 下午5.23.13.png

截圖2021-11-21 下午5.22.59.png

注意

存在有些情況下點選查詢迴圈引用,但是找不到的情況,這個時候就要自己在類裡面去排查了

截圖2021-11-21 下午5.24.48.png

``` 是否開啟迴圈引用檢測,預設是不開啟的,被註釋掉了, 如果不需要開啟只需要把這個設定成0

define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1

```

存在的問題

  1. MLeaksFinder 中的彈窗還是 UIAlertView 實現的,這個控制元件在 iOS9.0 已經被遺棄了,需要使用 UIAlertController 替代;
  2. 使用 UITextField 後一直檢測到異常,這個可以加到白名單裡面;

新增白名單,新增完成後就不會去檢測這個類了。需要匯入標頭檔案

```

import

[NSObject addClassNamesToWhitelist:@[NSStringFromClass([SecondViewController class])]];

``` 有些輔助開發的庫,是可以設定新增引入的,比如設定 Debug 環境下引入

pod 'MLeaksFinder', :configurations => ['Debug'] 總得來說 MLeaksFinder + FBRetainCycleDetector 是一個質量很高的庫,在實用性和便利性上做到了完美結合。

擴充套件

如果要將其作為日常自動化的工具的話,希望 MLeaksFinder 本身可以提供回撥介面,以便在記憶體洩漏發生時可以通過後臺上報的方式提交,像埋點資料上報那樣。

Facebook 的工程師們早就已經將 iOS 的記憶體洩漏排查自動化了,併發布了一篇非常不錯的文章來介紹其原理,以及開源了他們的三套件。

三個開源工具:

1.主要用於檢測迴圈引用 - FBRetainCycleDetector

2.主要用於快速檢測潛在的記憶體洩漏物件,並提供給 FBRetainCycleDetector 進行檢測 -FBAllocationTracker

3.視覺化工具,直接嵌入到App中,可以起到在App中直接檢視記憶體使用情況,並篩選潛在洩漏物件的作用 - FBMemoryProfiler

  • Facebook 的自動化:客戶端自動監測 -> 上報服務端 -> 歸類/篩選 -> 分發給指定人員 -> 處理記憶體洩漏;
  • 未開源的部分在於服務端如何對上報的迴圈引用鏈進行歸類與篩選,不過 Facebook 的工程師們也給出了策略;

FBmemoryProfiler 是 FB 開源的一款用於分析 iOS 記憶體使用和檢測迴圈引用的工具庫,也可以在專案裡面引入這個庫進行檢測。

參考文章:

iOS記憶體洩漏監測自動化

在iOS上自動檢測記憶體洩露