【YFMemoryLeakDetector】人人都能理解的 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);
    }
}];

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章