前言
提到iOS的內(nèi)存泄漏檢測竿屹,第一個想到的應(yīng)該就是Instruments的Leaks檢測模版。不過使用過的人一般都會覺得這個檢測不準(zhǔn)確秉溉,有時候明明泄露了坚嗜,但是它卻檢測不出來诗充。本文將帶大家深入了解Leaks模版檢測泄漏的原理,知道原理之后碟绑,你就會明白哪些類型的內(nèi)存泄漏可以被檢測茎匠,哪些無法被檢測了诵冒。
常規(guī)的內(nèi)存泄漏情景
iOS中我們使用引用計數(shù)來管理OC對象,但是對于CoreFoundation中的對象侮东,我們只能手動管理或者橋接到OC對象,通過引用計數(shù)來管理驱敲。對于手動malloc或者vm_allocate的內(nèi)存宽闲,則只能手動或者自定義一套內(nèi)存管理系統(tǒng)去管理了。所以對于一個iOS開發(fā)人員來說娩梨,發(fā)生泄漏的主要情景有
- OC對象循環(huán)引用
- OC對象被全局變量直接或者間接持有姚建,忘記斷開
- CF對象或者malloc的內(nèi)存忘記手動釋放
那么Leaks檢測模版能檢測出哪些泄漏呢吱殉?我們先介紹Leaks模版檢測泄漏的原理再一一分析。
Leaks如何檢測內(nèi)存泄漏
Instruments對Leaks的介紹僅僅是Examines a process' heap for leaked memory;
稿湿,檢測一個進(jìn)程堆里的泄露內(nèi)存饺藤。翻看一下官方的Find Memory Leaks介紹流礁,里面對于Leak Memory的介紹稍微的詳細(xì)一些,check for leaks—memory that has been allocated to objects that are no longer referenced and reachable.
再姑,所以一塊內(nèi)存是否泄露元镀,主要取決于它是否referenced and reachable
,那么怎么定義一塊內(nèi)存是否被引用呢栖疑?當(dāng)然不是通過引用計數(shù)遇革,因為還得檢測malloc出來的內(nèi)存,只有OC對象有引用計數(shù)概念比原。通過搜索杠巡,我找到了Leaks模版的命令行版本氢拥,也是蘋果官方提供的锨侯。打開你的命令行,輸入
man leaks
一份詳細(xì)的介紹文檔就出來了叁怪。
NAME
leaks -- Search a process's memory for unreferenced malloc buffers
這里對于leaks工具的簡介更加的清晰奕谭,unreferenced malloc buffers
表明這個工具的基本原理就是檢測malloc的內(nèi)存塊是否被依然被引用痴荐。非malloc出來的內(nèi)存塊則無能為力,比如vm_allocate出來的內(nèi)存难捌。想要了解vm_allocate相關(guān)的知識根吁,可以去看這篇文章合蔽。因為OC對象也都是通過malloc分配內(nèi)存的,所以自然也可以檢測愚争。下面的文檔則更清晰的告訴我們什么是unreferenced malloc buffers
挤聘。
Specifically, leaks examines a specified process's memory for values that may be pointers to malloc-allocated buffers. Any buffer reachable from a pointer in writable
global memory (e.g., __DATA segments), a register, or on the stack is
assumed to be memory in use. Any buffer reachable from a pointer in a
reachable malloc-allocated buffer is also assumed to be in use. The
buffers which are not reachable are leaks;
大致意思就是leaks搜索所有可能包含指向malloc內(nèi)存塊指針的內(nèi)存區(qū)域组去,比如全局?jǐn)?shù)據(jù)內(nèi)存塊,寄存器和所有的棧诚撵。如果malloc內(nèi)存塊的地址被直接或者間接引用寿烟,則是reachable
的,反之缝其,則是leaks徘六。
泄漏檢測情景分析
OC對象循環(huán)引用
我們可以通過一個小例子來還原這個情景待锈。
@interface LeakObject : NSObject
@property LeakObject *cycleRef;
@end
// 構(gòu)造循環(huán)引用
LeakObject *leakObj1 = [LeakObject new];
LeakObject *leakObj2 = [LeakObject new];
leakObj1.cycleRef = leakObj2;
leakObj2.cycleRef = leakObj1;
接下來我們使用Instruments Leaks或者leaks命令行來檢測泄漏。
下面是我用leaks工具檢測出來的泄漏和屎。使用命令leaks PID
眶俩,PID是進(jìn)程ID快鱼,在模擬器運(yùn)行App,然后通過Activity Monitor找到對應(yīng)的PID抹竹。
...
Analysis Tool Version: iOS Simulator 11.2 (15C107)
----
leaks Report Version: 2.0
leaks[9676]: Process 9648 is not debuggable.
Due to security restrictions, leaks cannot show memory contents of restricted processes.
Process 9648: 32174 nodes malloced for 7490 KB
Process 9648: 2 leaks for 9216 total leaked bytes.
Leak: 0x7fe4c9044e00 size=4608 zone: MallocHelperZone_0x123380000 LeakObject ObjC LeaksExample
Leak: 0x7fe4c9046000 size=4608 zone: MallocHelperZone_0x123380000 LeakObject ObjC LeaksExample
命令行工具也很好的為我們檢測出來了泄漏窃判。由于兩個LeakObject互相引用,而且未被全局?jǐn)?shù)據(jù)內(nèi)存塊询件,寄存器或者任何棧持有引用唆樊,所以被判定為unreachable的leak對象。
OC對象被全局變量直接或者間接持有
這種情況其實是Leaks無法檢測的嘿辟,因為被全局對象直接或者間接引用的malloc內(nèi)存塊在Leaks看來還是reachable的红伦。最簡單的例子就是被static的指針變量引用,在上面的基礎(chǔ)上舉個例子召调。
static void *leakObj = NULL;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 構(gòu)造循環(huán)引用
LeakObject *leakObj1 = [LeakObject new];
LeakObject *leakObj2 = [LeakObject new];
leakObj1.cycleRef = leakObj2;
leakObj2.cycleRef = leakObj1;
leakObj = (__bridge void *)leakObj1;
}
@end
注意箕戳,我用的static變量只是一個void *
類型的指針陵吸,不會對leakObj1
的引用計數(shù)造成任何實質(zhì)性的影響壮虫,但卻對Leaks的檢測結(jié)果造成了影響环础。
因為static變量leakObj
處于全局?jǐn)?shù)據(jù)內(nèi)存區(qū)线得,Leaks檢測到這個變量指向leakObj1
的內(nèi)存區(qū)域,所以認(rèn)為leakObj1
是reachable的募狂,并無泄漏發(fā)生角雷。這就是static變量對Leaks檢測的影響。這個例子屬于展示的比較直接雷滚,下面再看一個隱藏比較深的例子祈远。
為LeakObject增加一個block屬性商源。
typedef void(^LeakCallback)(void);
@interface LeakObject : NSObject
@property LeakObject *cycleRef;
@property (copy) LeakCallback callback;
@end
利用這個block構(gòu)造循環(huán)引用。下面是一個標(biāo)準(zhǔn)的由block引起的循環(huán)引用躬充。
@interface ViewController () {
LeakObject *_testLeak;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LeakObject *leakObj = [LeakObject new];
leakObj.callback = ^ {
NSLog(@"%@", self);
};
_testLeak = leakObj;
}
@end
最后在AppDelegate中創(chuàng)建ViewController然后再釋放掉它充甚。
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UINavigationController *navVC = [[UINavigationController alloc] initWithRootViewController:[UIViewController new]];
window.rootViewController = navVC;
[window makeKeyAndVisible];
ViewController *vc = [ViewController new];
[navVC.topViewController presentViewController:vc animated:YES completion:^{
}];
[vc dismissViewControllerAnimated:YES completion:nil];
使用Leaks進(jìn)行檢測,你會發(fā)現(xiàn)并無泄漏盈蛮。
為什么呢抖誉?我們再次運(yùn)行App衰倦,使用Debug Memory Graph來看看內(nèi)存中對象的引用關(guān)系圖。
我們可以發(fā)現(xiàn)我磁,ViewController被一個
malloc(16)
引用夺艰,很明顯這只是一個弱引用沉衣,否則ViewController永遠(yuǎn)不會被釋放,這和我在上面使用void *引用LeakObject屬于同一種方式存谎。你可以使用Debug>Debug Workflow>View Memory
來查看這個malloc(16)
的內(nèi)存區(qū)域愕贡,可以看到ViewController的內(nèi)存地址巷屿。0x60000001dc90
是malloc(16)
的起始地址嘱巾,0x7fe312608780
是ViewController的內(nèi)存地址。ViewController通過這個malloc(16)
被reachable的內(nèi)存塊引用篙螟,所以Leaks認(rèn)為ViewController并沒有泄漏问拘。不過目前我還沒有弄清楚這個malloc(16)
來自哪里惧所,有什么作用下愈,如果你感興趣蕾久,可以深入研究一下。
上面的2個例子解釋了為什么有時候Leaks無法檢測出來某些內(nèi)存泄露履因,它們還僅僅是弱引用盹愚,如果你不小心使用全局變量強(qiáng)引用了OC對象,那么你只能靠Allocations的引用計數(shù)Recorder來一一排查了霞篡,Leaks工具完全無法給你提供任何幫助。
CF對象或者malloc的內(nèi)存忘記手動釋放
這兩種情況還是很好檢測的污淋,不過它們同樣會受全局變量引用的影響。讀者可以自己嘗試全局變量引用對于malloc和CF對象Leaks檢測的影響礁鲁。
總結(jié)
實際開發(fā)過程中赁豆,遇到的情況會復(fù)雜的多,不過當(dāng)我們掌握了Leaks檢測的原理后析二,就能夠更有目標(biāo)性的解決內(nèi)存泄露叶摄。當(dāng)Leaks檢測失效安拟,可以在Allocations列表中觀察當(dāng)前存活的對象,是否有應(yīng)該已經(jīng)被釋放卻依然存活的会傲,如果有就應(yīng)該開始思考系統(tǒng)或者自身的代碼是否在全局?jǐn)?shù)據(jù)區(qū)對它有任何形式的引用,還可以借助Debug Memory Graph來觀察存疑對象的引用關(guān)系圖淌山。結(jié)合多方工具,大部分的內(nèi)存泄漏還是很好解決的顺少,不過有些泄漏可能存在于第三方庫甚至系統(tǒng)庫中王浴,這些就要費很多功夫了,或者你也可以直接換其他庫秒裕。