時過境遷,今天在網上搜了下 “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 實現,藉助於 Aspects 和 valueWithPointer: 代碼大大簡化。
#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);
}
}];
參考文章
- YFMemoryLeakDetector 源碼
- Aspects– iOS的AOP面向切麵編程的庫
- MLeaksFinder 新特性
- MLeaksFinder:精準 iOS 記憶體泄露檢測工具
- iOS記憶體泄漏自動檢測工具PLeakSniffer