前言
本文主要記錄在iOS開發中發現的一個系統級別內存泄露的過程。測試iOS系統11.2.1,設備iPhoneX。
如何復現
下面是復現泄漏的測試代碼,LeakObject
是一個沒有任何多余代碼的類,繼承自NSObject
。
LeakObject *leakObject = [LeakObject new];
for(int i = 0; i < 10000 * 1000; ++i) {
id value = @"hello";
[leakObject validateValue:&value forKey:@"notexistkey" error:nil];
}
當對一個沒有實現校驗方法的key進行validateValue
時,就會有少量內存泄漏。如果執行很多次,結果還是很可觀的。上面的代碼會讓內存飆到160M。
定位泄漏源
這個泄漏使用Instruments的Leaks模版可以很快的發現,但是代碼卻不好定位。下面是Leaks報告的泄漏截圖。
可以看出,泄漏發生在validateValue:forKey:error:
,再細看右邊的調用棧,可以看到這塊內存是由malloc
分配的。所以很有可能是這個系統方法內部發生了泄漏。
使用符號斷點深入觀察系統方法
首先使用符號斷點,讓程序在validateValue:forKey:error:
處停下。
運行程序,命中斷點后,我們就可以觀察
validateValue:forKey:error:
的匯編代碼了。尋找Leak的內存來源
在匯編代碼中,我發現了一個malloc調用和一個free調用。
0x1048272ef <+63>: callq 0x10498b1da ; symbol stub for: malloc
...
0x104827389 <+217>: callq 0x10498b066 ; symbol stub for: free
通過單步調試發現,malloc出來的內存主要用來存儲key,并且把首字母變成大寫,應該是為了方便構成validate<Key>:error:
的selector name。不過如果對象上沒有校驗這個key的方法,那么代碼會直接jump到free調用的下二行。這樣這個內存塊就永遠不會被釋放了。
0x104827390 <+224>: movb $0x1, %r14b
當我們給LeakObject加上notexistkey的校驗方法后,單步可以發現free被調用。下面是增加的校驗方法。
- (BOOL)validateNotexistkey:(id *)value error:(NSError **)error {
return YES;
}
總結
這次泄漏的尋找過程,大致可以分為
- 使用Instruments Leak模版初步定位
- 使用符號斷點深入泄漏方法,如果泄漏的方法不是系統或者第三方靜態(動態)庫的方法,就不用這么麻煩了。
- 關注泄漏內存塊的分配釋放方式,在源碼或者匯編代碼中尋找匹配的內存塊。
由于這次泄漏的僅僅是malloc內存塊,所以OC的引用計數記錄并不能起什么作用。