【YFMemoryLeakDetector】人人都能理解的 iOS 記憶體泄露檢測工具類

来源:http://www.cnblogs.com/ios122/archive/2017/11/23/7882383.html
-Advertisement-
Play Games

時過境遷,今天在網上搜了下 “iOS 記憶體泄露檢測”,各種討論技術文章,有點頭大。我忍不住看了下自己當時的代碼,突然感覺自己的思路好特別,好有創意。我真的就是在“創建”時把數據記錄到一個字典里,在“釋放”時,從字典里移出對象;所謂的檢測,其實就是列印那個字典,仍然在字典中的很有可能就是泄露嘍。 當... ...


背景

即使到今天,iOS 應用的記憶體泄露檢測,仍然是一個很重要的主題。我在一年前,項目中隨手寫過一個簡單的工具類,當時的確解決了大問題。視圖和控制器相關的記憶體泄露,幾乎都不存在了。後來想著一直就那個工具,寫一篇文章,不過一直沒有寫。

時過境遷,今天在網上搜了下 “iOS 記憶體泄露檢測”,各種討論技術文章,有點頭大。我忍不住看了下自己當時的代碼,突然感覺自己的思路好特別,好有創意。我真的就是在“創建”時把數據記錄到一個字典里,在“釋放”時,從字典里移出對象;所謂的檢測,其實就是列印那個字典,仍然在字典中的很有可能就是泄露嘍。

當然,還是有一些技術細節的。我把舊代碼適度拆分整理為一個開源庫了,取名為 YFMemoryLeakDetector。本篇,將著重講述簡潔之下,可能不易察覺的一些考量。

註意:這個庫,相當程度上是為當時的項目量身定製的,你可能需要適當修改,才能在自己的項目中真正發揮出它的力量。

核心技術分析

AOP 機制,藉助 Aspects 庫實現

Aspects 這個庫的基本用法,我專門說過,大家可以參考 Aspects– iOS的AOP面向切麵編程的庫。當然,用黑魔法直接操作運行時,也是很酷的。不過我當時的確是因為偷懶,才用的 Aspects。一直到現在,我依然覺得,它可能比黑魔法更可靠些。

在字典中直接存儲指針地址,而不是直接存儲對象自身

存儲指針地址的好處是,就是不會因為存儲本身影響對象的引用計數。當然,指針地址本身,在 OC 中,其實就是對象自身。而要想得到存地址,不存對象的效果,就要祭出整個工具庫的靈魂函數:

NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

將對象轉換為 NSValue,直接以 NSValue 為鍵,來標記對象。這句代碼,是整個機制的靈魂所在,也是比其他類似的記憶體泄露分析庫更簡潔的重要原因之一。我當時也是搜遍的整個網路,才知道自己要的究竟是什麼。

另外,還有一點必須提一下, NSValue 是可以在反向轉換為 oc 對象的,這有利於你在拿到工具庫提供的泄露信息後,進一步定位和分析問題:

UIViewController * vc = (UIViewController *)[key pointerValue];

對控制器和視圖,採用不同的攔截策略

  • 對象銷毀,統一攔截的是 dealloc。現在網上的很多策略,基本也是這樣。
  • 對象創建,對於視圖,攔截的是 willMoveToSuperview: ;對於控制器攔截的是 viewDidLoad 。直到現在,我依然以為,沒有調用過這兩個方法的視圖或控制器對象,本身沒有多大的攔截價值。當然,這依然因項目而異。作為一個工具類,只要它能解決大多數場景下的問題,我覺得就可以了。

load 時,自動開啟監測

所以,你只要把工具庫源碼拖拽到項目中,不需要任何修改,就可以自動監測記憶體泄露情況了。然後在需要的地方,在合適的時候,去讀取 YFMemoryLeakDetector 的單例屬性,分析結果即可。當然,這是我今天重構優化過的版本。原來是需要手動初始化的,好 Low,當時寫的!

+ (void)load
{
    [[YFMemoryLeakDetector sharedInstance] setup];
}

“見碼如晤”

YFMemoryLeakDetector.h 頭文件部分,主要簡化為暴露了存儲可能有記憶體泄露情況的視圖和控制器的字典屬性;同時提供了一個單例方法,以便於具體分析和操作記憶體分析情況。

#import <Foundation/Foundation.h>

/**
 *  分析頁面和頁面內視圖是否有記憶體泄露的情況.
 */
@interface  YFMemoryLeakDetector: NSObject

#pragma mark - 屬性.

/*
  已載入,但尚未正確釋放,有記憶體風險的控制器對象.
 
 以指針地址為key,以對象字元串為值.所以不用擔心因為記錄本身而引起的記憶體泄露問題.
 
 必要時,可以使用類似 (UIViewController *)[key pointerValue] 的語法來獲取原始的 OC對象來進一步做些過濾操作.
 */
@property (strong, atomic) NSMutableDictionary * loadedViewControllers;

/*
 已載入,但尚未正確釋放,有記憶體風險的視圖對象.
 
 以指針地址為key,以對象字元串為值.所以不用擔心因為記錄本身而引起的記憶體泄露問題.
 
 必要時,可以使用類似 (UIView *)[key pointerValue] 的語法來獲取原始的 OC對象來進一步做些過濾操作.
 */
@property (strong, atomic) NSMutableDictionary * loadedViews; //!< 已載入的視圖.



#pragma mark - 單例方法.
+(YFMemoryLeakDetector *) sharedInstance;
@end

YFMemoryLeakDetector.m 實現,藉助於 AspectsvalueWithPointer: 代碼大大簡化。

#import <objc/runtime.h>
#import <UIKit/UIKit.h>

#import "YFMemoryLeakDetector.h"
#import "Aspects.h"

@interface  YFMemoryLeakDetector()
@end

@implementation  YFMemoryLeakDetector

static YFMemoryLeakDetector * sharedLocalSession = nil;

+ (void)load
{
    [[YFMemoryLeakDetector sharedInstance] setup];
}

+(YFMemoryLeakDetector *) sharedInstance{
    @synchronized(self){
        if (sharedLocalSession == nil) {
            sharedLocalSession = [[self alloc] init];
        }
    }
    return  sharedLocalSession;
}


- (void)setup
{
    self.loadedViewControllers = [NSMutableDictionary dictionaryWithCapacity: 42];
    self.loadedViews = [NSMutableDictionary dictionaryWithCapacity:42];
    
    /* 控制器迴圈引用的檢測. */
    [UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info) {
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

        [self.loadedViewControllers setObject:[NSString stringWithFormat:@"%@", info.instance] forKey:key];
    }error:NULL];
    
    [UIViewController aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info) {
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];

        [self.loadedViewControllers removeObjectForKey: key];
    }error:NULL];
    
    /* 視圖迴圈引用的檢測. */
    /* 只捕捉已經從父視圖移除,卻未釋放的視圖.以指針區分. */
    [UIView aspect_hookSelector:@selector(willMoveToSuperview:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info, UIView * superview){
        /* 過濾以 _ 開頭的私有類. */
        NSString * viewClassname = NSStringFromClass(object_getClass(info.instance));
        if ([viewClassname hasPrefix:@"_"]) {
            return;
        }
        
        /* 相容處理使用了KVO機制監測 delloc 方法的庫,如 RAC. */
        if ([viewClassname hasPrefix:@"NSKVONotifying_"]) {
            return;
        }
        
        NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];
        
        /* 從父視圖移除時,就直接判定為已釋放.
         這樣做的合理性在於:當視圖從父視圖移除後,一般是很難再出發迴圈引用的條件了,所以可適度忽略.
         */
        if (!superview) {
            [self.loadedViews removeObjectForKey: key];
        }
        
        NSMutableDictionary * obj = [self.loadedViews objectForKey: key];
        
        if (obj) { /* 一個 UIView 視圖,只記錄一次即可.因為一個UIView,最多只被 delloc 一次. */
            return;
        }
        
        [self.loadedViews setObject: [NSString stringWithFormat:@"%@", info.instance] forKey:key];
        
        /* 僅對有效實例進行捕捉.直接捕捉類對象,會引起未知崩潰,尤其涉及到和其他有KVO機制的類庫配合使用時. */
        [info.instance aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){
            [self.loadedViews removeObjectForKey: key];
        }error:NULL];
    }error:NULL];
}
@end

使用示例:

這裡展示一個基於工具類,二次分析的示例:

YFMemoryLeakDetector * memoryLeakDetector = [YFMemoryLeakDetector sharedInstance];
        
/* 控制器檢測結果的輸出. */
[memoryLeakDetector.loadedViewControllers enumerateKeysAndObjectsUsingBlock:^(NSValue *  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    UIViewController * vc = (UIViewController *)[key pointerValue];
    if (!vc.parentViewController) { /* 進一步過濾掉有父控制器的控制器. */
        NSLog(@"有記憶體泄露風險的控制器: %@", obj);
    }
}];
    
/* 視圖檢測結果的輸出. */
[memoryLeakDetector.loadedViews enumerateKeysAndObjectsUsingBlock:^(NSValue *  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
    UIView * view = (UIView *)[key pointerValue];
    if (!view.superview) { /* 進一步過濾掉有父視圖的視圖,即只輸出一組視圖的根節點,這樣便於更進一步定位問題. */
        NSLog(@"有記憶體泄露風險的視圖: %@", obj);
    }
}];

參考文章


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 為什麼要使用cdn 雅虎軍規有一條規則建議我們是用cdn。隨便在網上搜索,可以找到使用的cdn的好處。 再次強調第一條黃金定律,減少網頁內容的下載時間。提高下載速度還可以通過CDN(內容分髮網絡)來提升。CDN通過部署在不同地區的伺服器來提高客戶的下載速度。如果你的網站上有大量的靜態內容,世界各地的 ...
  • 1、JS中的||符號: 運算方法: 只要“||”前面為false,不管“||”後面是true還是false,都返回“||”後面的值。 只要“||”前面為true,不管“||”後面是true還是false,都返回“||”前面的值。 總結:真前假後 2、JS中的&&符號: 運算方法: 只要“&&”前面是 ...
  • 由於最近剛換了一個工作,所以決定重新申請一個blog,把工作當中遇到的一些問題記錄下來,方便自己下次忘記,也希望能與一起需要的小伙伴一起共勉。 如果有不同的觀點或者是不同的看法,大家都可以暢談,我一直認為討論、實踐才是提升自己能力的真正‘路徑’。。。(好啦,進入今天的主題) 在這份工作開始之前,我一 ...
  • 一、DOM API也在不斷升級 web前端標準一直在不斷升級,比方說,說了很多年的HTML5、CSS3,以及天天見的ES6。 然後,似乎就沒有然後了。實際上,除了HTML5/CSS3/ES6+,關於DOM標準也是在不斷升級進步,而且瀏覽器也在悄悄地進行跟進與支持。 然而,這種跟進與支持呢非常的低調與 ...
  • 一、text-decoration:underline下劃線的問題 CSS text-decoration:underline可以給內聯文本增加下劃線,但是,如果對細節要求較高,就會發現,下劃線經常會和中文文字的下邊緣搞在一起,英文的話甚至直接穿越,看起來就比較香菇藍瘦。 上圖幾個中文下邊緣正好都是 ...
  • 第3章 盒模型,定位,浮動,清理 1.盒模型用到的屬性width,height,padding,border,margin 普通文檔流的上下垂直margin會疊加 2.塊級框 與 行內框, 利用display屬性來改變特性,display:block,display:inlne-block 3. 3 ...
  • 第2章 選擇器,註釋 1.要知道常用選擇器(id選擇器,類選擇器,類型選擇器,後代選擇器,偽類選擇器(文檔結構之外)) 通用選擇器(*{ }) 高級選擇器(子選擇器,相鄰同胞選擇器,屬性選擇器) 2.選擇器特殊性分級 3.層疊和繼承的區別 4.設計代碼的結構,使用風格統一的大註釋塊分割每個部分 一般 ...
  • 一,效果圖。 二,工程圖。 三,代碼。 ViewController.h ViewController.m photoCollectionViewCell.h photoCollectionViewCell.m ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...