iOS之深入定制基于PLeakSniffer和MLeaksFinder的內存泄漏檢測工具

      網友投稿 982 2022-05-30

      在編寫日常業務代碼時,或多或少都會引入一些導致內存泄漏的代碼,而這種行為又很難被監控,這就導致應用內存泄漏的口子越開越大,直接影響到線上應用的穩定性。

      雖然 Xcode 的 Instrucment 提供了 Leaks 和 Allocations 工具能精準地定位內存泄漏問題,但是這種方式相對比較繁瑣,需要開發人員頻繁地去操作應用界面,以觸發泄漏場景,所以 Leaks 和 Allocations 更加適合定期組織的大排查,作為監測手段,則顯得笨重。

      對于內存泄漏的監測,業內已經有了兩款成熟的開源工具,分別是 PLeakSniffer 和 MLeaksFinder。

      PLeakSniffer 使用 Ping-Pong 方式監測對象是否存活,在進入頁面時,創建控制器關聯的一系列對象代理,根據這些代理在控制器銷毀時能否響應 Ping 判斷代理對應的對象是否泄漏。

      MLeaksFinder 則是在控制器銷毀時,延遲 3s 后再向監測對象發送消息,根據監測對象能否響應消息判斷其是否泄漏。

      PLeakSniffer 和 MLeaksFinder 這兩個基本能覆蓋大部分對象泄漏或者延遲釋放的場景,考慮到性能損耗以及內存占用因素,個人更偏向于第二種方案。

      個人使用 MLeaksFinder,還存在以下問題:

      沒有處理集合對象;

      沒有處理對象持有的屬性;

      每個對象都觸發 3s 延遲機制,沒有緩存后統一處理;

      檢測結果輸出分散。

      PLeakSniffer 存在以下問題:

      沒有處理集合對象;

      處理對象持有屬性時,系統類過濾不全面;

      處理對象持有屬性時,通過 KVC 訪問屬性導致一些懶加載的觸發;

      無法處理未添加到視圖棧中的泄漏視圖;

      檢測結果輸出分散。

      對于檢測到泄漏對象的交互處理,兩者都提供了終端 log 輸出和 alert 提示功能,MLeaksFinder 甚至可以直接通過斷言中斷應用,這種提示在開發階段尚可接受,但是在提測階段,強交互會給測試人員造成困擾。至于為什么在提測階段還要集成泄漏監測工具,主要有兩個原因:

      應用功能過多的情況下,開發人員無法兼顧到老頁面,一些老頁面的泄漏場景可以通過測試人員在測試時觸發,收集之后再統一處理;

      在組件化開發環境下,開發人員可能并沒有集成泄漏監測工具,這種情況下,需要在提測階段統一收集沒有解決的泄漏問題。

      因此,對于監測輸出的訴求有兩點:

      開發時,通過終端日志提示開發者出現了內存泄漏;

      提測時,收集內存泄漏的信息并上傳至效能后臺,統一分配處理;

      和 MLeaksFinder 一樣,選擇延遲 3s 的機制來判斷對象是否泄漏,但是實現的細節略有差別。首先,監測入口變更為 viewDidDisappear: 方法,只需在控制器被父控制器中移除或者被 Dismissed 時,觸發監測動作即可:

      - (void)LeaksMonitor_viewDidDisappear:(BOOL)animated { [self LeaksMonitor_viewDidDisappear:animated]; if (![self isMovingFromParentViewController] && ![self isBeingDismissed]) { return; } [[YDWLeaksMonitor shared] detectLeaksForObject:self]; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      在應用中,還有一種監測入口出現在變更根控制器時,由于直接設置根控制器不會觸發 viewDidDisappear 方法,所以需要另外設置 :

      - (void)LeaksMonitor_setRootViewController:(UIViewController *)rootViewController { if (self.rootViewController && ![self.rootViewController isEqual:rootViewController]) { [[YDWLeaksMonitor shared] detectLeaksForObject:self.rootViewController]; } [self LeaksMonitor_setRootViewController:rootViewController]; }

      1

      2

      3

      4

      5

      6

      7

      為了能夠統一處理控制器及其持有對象,可以像 PLeakSniffer 一樣,給每個對象包裝一層代理 :

      @interface YDWLeakObjectProxy : NSObject // 持有 target 的對象弱引用 @property (weak, nonatomic) id host; // 被 host 持有的對象弱引用 @property (weak, nonatomic, readonly) id target; @end

      1

      2

      3

      4

      5

      6

      只要 host 釋放了而 target 沒釋放,則視 target 已泄漏,如果 host 未釋放,則不檢測 target,然后使用一個 collector 去收集這些對象對應的 proxy ,在收集完之后統一監測 collector 中的所有 proxy ,這樣就可以在一個控制器監測完成后,統一上傳監測出的泄漏點 :

      - (void)detectLeaksForObject:(id )object { // 收集控制器關聯的所有 proxy // 收集之后再統一處理,避免對每一個對象都進行 3s 檢測 YDWLeakObjectProxyCollector *collector = [[YDWLeakObjectProxyCollector alloc] init]; YDWLeakContext *context = [[YDWLeakContext alloc] init]; context.host = object; (void)[object LeaksMonitor_collectProxiesForCollector:collector withContext:context]; // 檢測 3s 之后,collector 中的所有 proxy 是否正常 [self detectProxyCollector:collector]; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      因為要對不同的類做特異化處理,因此先定義一個協議,通過這個協議中的 collect 方法去收集不同類實例化對象的 proxy :

      @protocol YDWLeakObjectProxyCollectable /** 收集對象及其名下的所有成員變量對應的 proxy @param collector 收集器,存儲 proxy @param ctx 上下文 */ - (void)LeaksMonitor_collectProxiesForCollector:( YDWLeakObjectProxyCollector * _Nonnull )collector withContext:( YDWLeakContext * _Nullable )ctx; @end

      1

      2

      3

      4

      5

      6

      7

      8

      9

      關鍵在于如何讓 NSObject 實現此協議,主要有四個步驟 :

      過濾系統類調用;

      向 collector 添加封裝的 proxy;

      循環遍歷對象對應的非系統類 / 父類屬性,找出 copy / strong 類型屬性,并獲取其對應的成員變量值;

      向收集的所有成員變量對象發送 collect 方法。

      NSObject 實現 collect 協議方法后,其子類就可以通過這個方法遞歸地收集名下需要監測的屬性信息。比如對于集合類型 NSArray ,實現協議方法如下,表示收集自身和每個集合元素的信息,不過由于 NSArray 是系統類,所以其實例化對象并不會被收集進 collector ,如果要收集系統類的屬性信息,只能通過讓系統類實現協議并重載 collect 方法,手動向屬性值發送 collect 消息實現,UIViewController 的 childViewControllers、presentedViewController、view 屬性也同理 :

      - (void)LeaksMonitor_collectProxiesForCollector:(YDWYDWLeakObjectProxyCollector *)collector withContext:(YDWLeakContext *)ctx { [super LeaksMonitor_collectProxiesForCollector:collector withContext:ctx]; [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { if ([obj conformsToProtocol:@protocol(YDWLeakObjectProxyCollectable)]) { [obj LeaksMonitor_collectProxiesForCollector:collector withContext:LM_CTX_D(ctx, @"contains")]; } }]; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      需要注意的是,直接調用屬性的 getter 方法獲取屬性值,可能會觸發屬性懶加載,導致出現意料之外的問題 (比如調用 UIViewController 的 view 會觸發 viewDidLoad),所以要通過 object_getIvar 去獲取屬性對應的成員變量值。當然,這種處理方式會導致無法收集某些沒有對應成員變量值的屬性,比如關聯對象、控制器的 view 等屬性,權衡利弊之后,可以選擇忽略這種屬性的監測。

      除了收集必要的對象信息之外,我還記錄了監測對象的引用路徑信息,也就是上面 LM_CTX_D 宏做的事情。有些情況下,對象的引用路徑能幫助我們發現,路徑上的哪些操作導致了對象的泄漏,特別是在網頁上瀏覽泄漏信息時,如果只有泄漏對象類和引用泄漏對象類兩個信息,脫離了對象泄漏時的上下文環境,會增加修復的難度。有了引用路徑信息后,輸出的泄漏信息如下 :

      [ O : YDWViewController.view->UIView.subviews->__NSArrayM(contains)->A.subviews->__NSArrayM(contains)->O YDWViewController : YDWViewController.childViewControllers->YDWViewController __NSCFTimer : YDWViewController.timer->__NSCFTimer ]

      1

      2

      3

      4

      5

      系統類信息并不是需要關心的,過濾掉并不會影響到最終的監測結果。目前我嘗試了兩種方式來確定一個類是否為系統類:

      通過類所在 NSBundle 的路徑;

      通過類所在地址。

      第一種的邏輯較為簡單,代碼如下:

      BOOL LMIsSystemClass(Class cls) { NSBundle *bundle = [NSBundle bundleForClass:cls]; if ([bundle isEqual:[NSBundle mainBundle]]) { return NO; } static NSString *embededDirPath; if (!embededDirPath) { embededDirPath = [[NSBundle mainBundle].bundleURL URLByAppendingPathComponent:@"Frameworks"].absoluteString; } return ![bundle.bundlePath hasPrefix:embededDirPath]; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      應用的主二進制文件,和開發者添加的 embeded frameworks 都會在固定的文件目錄下,所以直接比對路徑前綴即可。

      iOS之深入定制基于PLeakSniffer和MLeaksFinder的內存泄漏檢測工具

      第二種方式的實現步驟如下:

      遍歷所有的 image ,通過 image 的名稱判斷是否為系統 image;

      緩存所有系統 image 的起始位置,也就是 mach_header 的地址;

      判斷類是否為系統類時,使用 dladdr 函數獲取類所在 image 的信息,通過 dli_fbase 字段獲取起始地址;

      比對 image 的起始地址得知是否為系統類。

      實際嘗試下來后,發現第二種方式耗時會比第一種多,dladdr 函數占用了大部分時間(內部會遍歷所有 image 的開始結束地址,和傳入的地址進行比對),所以最終選擇了第一種方式作為判斷依據。

      過濾系統類時,針對那種會自泄漏的對象,需要進行特殊處理,不予過濾。比如 NSTimer / CADisplayLink 對象的常見內存泄漏場景,除了 target 強引用控制器造成循環引用域外,還有一種是打破了循環引用但沒有在控制器銷毀時執行 invalidate 操作,因為 NSTimer 由 RunLoop 持有,不手動停止的情況下,就會造成泄漏。

      基于延時的內存泄漏監測機制雖然適用于大部分視圖、控制器和一般屬性的泄漏場景,但是還有少部分情況,這種機制無法處理,比如單例對象和共享對象。

      首先說下單例對象,假設有 singleton 屬性,其 getter 方法返回 Singleton 單例,這時延時監測機制無法自動過濾這種情況,依然會認為 singleton 泄漏了。有一種檢測屬性返回值是否為單例的方法,就是向返回值對應類發送 init 或者 share 相關方法,通過方法返回值和屬性返回值的對比結果來判斷,但是事實上我們無法確定業務方的單例是否重寫了 init,也無法獲知具體的單例類方法,所以這種方案適用面比較局限。單例對象的處理,目前還是通過白名單的方式處理較為穩妥。

      共享對象的應用場景就比較普遍了,比如現有 A,B 頁面,A 頁面持有模型 M ,在跳轉至 B 頁面時,會將 M 傳遞給 B ,B 強引用了 M ,當 B 銷毀時, M 不會銷毀,而 M 又是 B 某個屬性的值,所以監測機制會判斷 M 泄漏了,實際上 M 只是 A 傳遞給 B 的共享對象。在一個控制器做完檢測就需要上傳至效能后臺的情況下,共享對象還沒有很好的處理方法,后期考慮結合 FBRetainCycleDetector 查找泄漏對象的循環引用信息,然后一并上傳至效能后臺,方便排查這種情況。因為每次 pop 都使用 FBRetainCycleDetector 檢測控制器會比較耗時、甚至會造成延遲釋放和卡頓,所以先用延時機制找出潛在的泄漏對象,再使用 FBRetainCycleDetector 檢測這些泄漏對象,能極大得減少需要處理的對象數量。最終網頁呈現的效果如下:

      像內存泄露這種問題,最好在應用初期就開始著手監測和解決,否則當應用功能代碼逐漸增多后,回過頭來處理這種問題費時費力,還是比較麻煩的。

      基于 PLeakSniffer 和 MLeaksFinder 監測工具的基礎上,結合團隊業務情況,進行了一些的改造,添加了集合對象的處理、引用路徑的記錄、對象的統一檢測等功能,優化了部分有問題的代碼,在一定程度上提升了延時機制的可用性。

      Image iOS

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:劍指Offer——知識點儲備-Java基礎
      下一篇:什么算是有價值的測試用例?
      相關文章
      亚洲人成中文字幕在线观看| 在线观看亚洲精品专区| 亚洲A∨午夜成人片精品网站| 亚洲无码一区二区三区| 亚洲精品国产啊女成拍色拍 | 亚洲欧洲日韩不卡| 亚洲国产精品成人久久 | 天堂亚洲国产中文在线| 亚洲av永久无码精品三区在线4| 亚洲国产精品综合久久久| 亚洲第一页在线观看| 亚洲国产高清美女在线观看 | 国产精品亚洲色图| 亚洲AV成人潮喷综合网| 亚洲不卡无码av中文字幕| 亚洲国产小视频精品久久久三级 | 亚洲av高清在线观看一区二区| 国产大陆亚洲精品国产| www.亚洲精品| 国产成人毛片亚洲精品| 亚洲日韩精品一区二区三区| 国精无码欧精品亚洲一区 | 国产亚洲成av片在线观看 | 亚洲av片在线观看| jizzjizz亚洲| 在线A亚洲老鸭窝天堂| 亚洲理论电影在线观看| 亚洲高清在线视频| 亚洲精品视频观看| 色偷偷亚洲女人天堂观看欧| 亚洲欧美黑人猛交群| 色偷偷亚洲第一综合| 亚洲精品视频久久久| 亚洲中文字幕无码永久在线| 亚洲AV综合色区无码另类小说| 亚洲无线电影官网| 亚洲精品中文字幕无乱码麻豆| 亚洲精品乱码久久久久久V| 亚洲成A∨人片天堂网无码| 在线观看亚洲av每日更新| 亚洲人成在线播放网站岛国|