背景
即使到今天,iOS 應(yīng)用的內(nèi)存泄露檢測(cè),仍然是一個(gè)很重要的主題。我在一年前,項(xiàng)目中隨手寫過一個(gè)簡(jiǎn)單的工具類,當(dāng)時(shí)的確解決了大問題。視圖和控制器相關(guān)的內(nèi)存泄露,幾乎都不存在了。后來想著一直就那個(gè)工具,寫一篇文章,不過一直沒有寫。
時(shí)過境遷,今天在網(wǎng)上搜了下 “iOS 內(nèi)存泄露檢測(cè)”,各種討論技術(shù)文章,有點(diǎn)頭大。我忍不住看了下自己當(dāng)時(shí)的代碼,突然感覺自己的思路好特別,好有創(chuàng)意。我真的就是在“創(chuàng)建”時(shí)把數(shù)據(jù)記錄到一個(gè)字典里,在“釋放”時(shí),從字典里移出對(duì)象;所謂的檢測(cè),其實(shí)就是打印那個(gè)字典,仍然在字典中的很有可能就是泄露嘍。
當(dāng)然,還是有一些技術(shù)細(xì)節(jié)的。我把舊代碼適度拆分整理為一個(gè)開源庫了,取名為 YFMemoryLeakDetector。本篇,將著重講述簡(jiǎn)潔之下,可能不易察覺的一些考量。
注意:這個(gè)庫,相當(dāng)程度上是為當(dāng)時(shí)的項(xiàng)目量身定制的,你可能需要適當(dāng)修改,才能在自己的項(xiàng)目中真正發(fā)揮出它的力量。
核心技術(shù)分析
AOP 機(jī)制,借助 Aspects 庫實(shí)現(xiàn)
Aspects 這個(gè)庫的基本用法,我專門說過,大家可以參考 Aspects– iOS的AOP面向切面編程的庫。當(dāng)然,用黑魔法直接操作運(yùn)行時(shí),也是很酷的。不過我當(dāng)時(shí)的確是因?yàn)橥祽?,才用?Aspects。一直到現(xiàn)在,我依然覺得,它可能比黑魔法更可靠些。
在字典中直接存儲(chǔ)指針地址,而不是直接存儲(chǔ)對(duì)象自身
存儲(chǔ)指針地址的好處是,就是不會(huì)因?yàn)榇鎯?chǔ)本身影響對(duì)象的引用計(jì)數(shù)。當(dāng)然,指針地址本身,在 OC 中,其實(shí)就是對(duì)象自身。而要想得到存地址,不存對(duì)象的效果,就要祭出整個(gè)工具庫的靈魂函數(shù):
NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];
將對(duì)象轉(zhuǎn)換為 NSValue,直接以 NSValue 為鍵,來標(biāo)記對(duì)象。這句代碼,是整個(gè)機(jī)制的靈魂所在,也是比其他類似的內(nèi)存泄露分析庫更簡(jiǎn)潔的重要原因之一。我當(dāng)時(shí)也是搜遍的整個(gè)網(wǎng)絡(luò),才知道自己要的究竟是什么。
另外,還有一點(diǎn)必須提一下, NSValue 是可以在反向轉(zhuǎn)換為 oc 對(duì)象的,這有利于你在拿到工具庫提供的泄露信息后,進(jìn)一步定位和分析問題:
UIViewController * vc = (UIViewController *)[key pointerValue];
對(duì)控制器和視圖,采用不同的攔截策略
- 對(duì)象銷毀,統(tǒng)一攔截的是 dealloc?,F(xiàn)在網(wǎng)上的很多策略,基本也是這樣。
- 對(duì)象創(chuàng)建,對(duì)于視圖,攔截的是 willMoveToSuperview: ;對(duì)于控制器攔截的是 viewDidLoad 。直到現(xiàn)在,我依然以為,沒有調(diào)用過這兩個(gè)方法的視圖或控制器對(duì)象,本身沒有多大的攔截價(jià)值。當(dāng)然,這依然因項(xiàng)目而異。作為一個(gè)工具類,只要它能解決大多數(shù)場(chǎng)景下的問題,我覺得就可以了。
在 load 時(shí),自動(dòng)開啟監(jiān)測(cè)
所以,你只要把工具庫源碼拖拽到項(xiàng)目中,不需要任何修改,就可以自動(dòng)監(jiān)測(cè)內(nèi)存泄露情況了。然后在需要的地方,在合適的時(shí)候,去讀取 YFMemoryLeakDetector 的單例屬性,分析結(jié)果即可。當(dāng)然,這是我今天重構(gòu)優(yōu)化過的版本。原來是需要手動(dòng)初始化的,好 Low,當(dāng)時(shí)寫的!
+ (void)load
{
[[YFMemoryLeakDetector sharedInstance] setup];
}
“見碼如晤”
YFMemoryLeakDetector.h 頭文件部分,主要簡(jiǎn)化為暴露了存儲(chǔ)可能有內(nèi)存泄露情況的視圖和控制器的字典屬性;同時(shí)提供了一個(gè)單例方法,以便于具體分析和操作內(nèi)存分析情況。
#import <Foundation/Foundation.h>
/**
* 分析頁面和頁面內(nèi)視圖是否有內(nèi)存泄露的情況.
*/
@interface YFMemoryLeakDetector: NSObject
#pragma mark - 屬性.
/*
已加載,但尚未正確釋放,有內(nèi)存風(fēng)險(xiǎn)的控制器對(duì)象.
以指針地址為key,以對(duì)象字符串為值.所以不用擔(dān)心因?yàn)橛涗洷旧矶鸬膬?nèi)存泄露問題.
必要時(shí),可以使用類似 (UIViewController *)[key pointerValue] 的語法來獲取原始的 OC對(duì)象來進(jìn)一步做些過濾操作.
*/
@property (strong, atomic) NSMutableDictionary * loadedViewControllers;
/*
已加載,但尚未正確釋放,有內(nèi)存風(fēng)險(xiǎn)的視圖對(duì)象.
以指針地址為key,以對(duì)象字符串為值.所以不用擔(dān)心因?yàn)橛涗洷旧矶鸬膬?nèi)存泄露問題.
必要時(shí),可以使用類似 (UIView *)[key pointerValue] 的語法來獲取原始的 OC對(duì)象來進(jìn)一步做些過濾操作.
*/
@property (strong, atomic) NSMutableDictionary * loadedViews; //!< 已加載的視圖.
#pragma mark - 單例方法.
+(YFMemoryLeakDetector *) sharedInstance;
@end
YFMemoryLeakDetector.m 實(shí)現(xiàn),借助于 Aspects 和 valueWithPointer: 代碼大大簡(jiǎn)化。
#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];
/* 控制器循環(huán)引用的檢測(cè). */
[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];
/* 視圖循環(huán)引用的檢測(cè). */
/* 只捕捉已經(jīng)從父視圖移除,卻未釋放的視圖.以指針區(qū)分. */
[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機(jī)制監(jiān)測(cè) delloc 方法的庫,如 RAC. */
if ([viewClassname hasPrefix:@"NSKVONotifying_"]) {
return;
}
NSValue * key = [NSValue valueWithPointer: (__bridge const void * _Nullable)(info.instance)];
/* 從父視圖移除時(shí),就直接判定為已釋放.
這樣做的合理性在于:當(dāng)視圖從父視圖移除后,一般是很難再出發(fā)循環(huán)引用的條件了,所以可適度忽略.
*/
if (!superview) {
[self.loadedViews removeObjectForKey: key];
}
NSMutableDictionary * obj = [self.loadedViews objectForKey: key];
if (obj) { /* 一個(gè) UIView 視圖,只記錄一次即可.因?yàn)橐粋€(gè)UIView,最多只被 delloc 一次. */
return;
}
[self.loadedViews setObject: [NSString stringWithFormat:@"%@", info.instance] forKey:key];
/* 僅對(duì)有效實(shí)例進(jìn)行捕捉.直接捕捉類對(duì)象,會(huì)引起未知崩潰,尤其涉及到和其他有KVO機(jī)制的類庫配合使用時(shí). */
[info.instance aspect_hookSelector:NSSelectorFromString(@"dealloc") withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){
[self.loadedViews removeObjectForKey: key];
}error:NULL];
}error:NULL];
}
@end
使用示例:
這里展示一個(gè)基于工具類,二次分析的示例:
YFMemoryLeakDetector * memoryLeakDetector = [YFMemoryLeakDetector sharedInstance];
/* 控制器檢測(cè)結(jié)果的輸出. */
[memoryLeakDetector.loadedViewControllers enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
UIViewController * vc = (UIViewController *)[key pointerValue];
if (!vc.parentViewController) { /* 進(jìn)一步過濾掉有父控制器的控制器. */
NSLog(@"有內(nèi)存泄露風(fēng)險(xiǎn)的控制器: %@", obj);
}
}];
/* 視圖檢測(cè)結(jié)果的輸出. */
[memoryLeakDetector.loadedViews enumerateKeysAndObjectsUsingBlock:^(NSValue * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
UIView * view = (UIView *)[key pointerValue];
if (!view.superview) { /* 進(jìn)一步過濾掉有父視圖的視圖,即只輸出一組視圖的根節(jié)點(diǎn),這樣便于更進(jìn)一步定位問題. */
NSLog(@"有內(nèi)存泄露風(fēng)險(xiǎn)的視圖: %@", obj);
}
}];