小總結(jié)
MLeaksFinder 是 WeRead 團(tuán)隊(duì)開源的iOS內(nèi)存泄漏檢測工具固惯,wereadteam博客芯侥,GitHub躏将。
MLeaksFinder 個(gè)人認(rèn)為優(yōu)勢是在開發(fā)過程中自動(dòng)檢測 UIViewController 和 UIView 等UI對(duì)象內(nèi)存泄露陌知,及自定義是否檢測 UIViewController 中強(qiáng)引用的 NSObject 類對(duì)象內(nèi)存泄露京闰。這樣在開發(fā)調(diào)試過程中就能實(shí)際避免大部分內(nèi)存泄漏并及時(shí)修正編碼習(xí)慣颜及。
小學(xué)習(xí)
以下均轉(zhuǎn)自wereadteam博客
1. Leaks -> 從蘋果的開發(fā)者文檔里可以看到,一個(gè) app 的內(nèi)存分三類:
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument). 泄漏內(nèi)存應(yīng)用程序沒有引用的內(nèi)存蹂楣,不能再次使用或釋放(也可以通過使用泄漏工具檢測到)俏站。
Abandoned memory: Memory still referenced by your application that has no useful purpose.廢棄內(nèi)存:內(nèi)存仍然被應(yīng)用程序引用,沒有任何有用的用途痊土。
Cached memory: Memory still referenced by your application that might be used again for better performance.緩存內(nèi)存:您的應(yīng)用程序仍然引用的內(nèi)存乾翔,這些內(nèi)存可能會(huì)再次用于更好的性能。
以上
Leaked memory
和Abandoned memory
都屬于應(yīng)該釋放而沒釋放的內(nèi)存施戴,都是內(nèi)存泄露
2.說說 Instrument 的 Leaks / Allocations
Leaks 工具只負(fù)責(zé)檢測
Leaked memory
反浓,而不管Abandoned memory
。在 MRC 時(shí)代Leaked memory
很常見赞哗,因?yàn)楹苋菀淄苏{(diào)用release
雷则,但在 ARC 時(shí)代更常見的內(nèi)存泄露是循環(huán)引用導(dǎo)致的Abandoned memory
,Leaks 工具查不出這類內(nèi)存泄露肪笋,應(yīng)用有限月劈。-
用 Instrument 的 Allocations 檢測Abandoned memory 。
檢測方法是用 Mark Generation 的方式藤乙,當(dāng)你每次點(diǎn)擊 Mark Generation 時(shí)猜揪,Allocations 會(huì)生成當(dāng)前 App 的內(nèi)存快照,而且 Allocations 會(huì)記錄從上回內(nèi)存快照到這次內(nèi)存快照這個(gè)時(shí)間段內(nèi)坛梁,新分配的內(nèi)存信息而姐。舉一個(gè)最簡單的例子:我們可以不斷重復(fù) push 和 pop 同一個(gè) UIViewController,理論上來說划咐,push 之前跟 pop 之后拴念,app 會(huì)回到相同的狀態(tài)。因此褐缠,在 push 過程中新分配的內(nèi)存政鼠,在 pop 之后應(yīng)該被 dealloc 掉,除了前幾次 push 可能有預(yù)熱數(shù)據(jù)和 cache 數(shù)據(jù)的情況队魏。如果在數(shù)次 push 跟 pop 之后公般,內(nèi)存還不斷增長,則有內(nèi)存泄露。因此官帘,我們在每回 push 之前跟 pop 之后瞬雹,都 Mark Generation 一下,以此觀察內(nèi)存是不是無限制增長遏佣。
缺點(diǎn) 用這種方法來發(fā)現(xiàn)內(nèi)存泄露還是很不方便的:
首先挖炬,你得打開 Allocations
其次,你得一個(gè)個(gè)場景去重復(fù)的操作
無法及時(shí)得知泄露状婶,得專門做一遍上述操作意敛,十分繁瑣
3. 原理學(xué)習(xí)
MLeaksFinder 一開始從 UIViewController 入手。我們知道膛虫,當(dāng)一個(gè) UIViewController 被 pop 或 dismiss 后草姻,該 UIViewController 包括它的 view,view 的 subviews 等等將很快被釋放(除非你把它設(shè)計(jì)成單例稍刀,或者持有它的強(qiáng)引用撩独,但一般很少這樣做)。于是账月,我們只需在一個(gè) ViewController 被 pop 或 dismiss 一小段時(shí)間后综膀,看看該 UIViewController,它的 view局齿,view 的 subviews 等等是否還存在剧劝。
具體的方法是,為基類 NSObject 添加一個(gè)方法 -willDealloc 方法抓歼,該方法的作用是讥此,先用一個(gè)弱指針指向 self,并在一小段時(shí)間(3秒)后谣妻,通過這個(gè)弱指針調(diào)用 -assertNotDealloc萄喳,而 -assertNotDealloc 主要作用是直接中斷言。
- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong id strongSelf = weakSelf;
[strongSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”);
}
這樣蹋半,當(dāng)我們認(rèn)為某個(gè)對(duì)象應(yīng)該要被釋放了他巨,在釋放前調(diào)用這個(gè)方法,如果3秒后它被釋放成功湃窍,weakSelf 就指向 nil闻蛀,不會(huì)調(diào)用到 -assertNotDealloc
方法,也就不會(huì)中斷言您市,如果它沒被釋放(泄露了),-assertNotDealloc
就會(huì)被調(diào)用中斷言役衡。這樣茵休,當(dāng)一個(gè) UIViewController 被 pop 或 dismiss 時(shí)(我們認(rèn)為它應(yīng)該要被釋放了),我們遍歷該 UIViewController 上的所有 view,依次調(diào) -willDealloc
榕莺,若2秒后沒被釋放俐芯,就會(huì)斷言。
4. 代碼中解決的問題
不入侵開發(fā)代碼
這里使用了 AOP 技術(shù)钉鸯,hook 掉 UIViewController 和 UINavigationController 的 pop 跟 dismiss 方法吧史,關(guān)于如何 hook,請(qǐng)參考 Method Swizzling唠雕。遍歷相關(guān)對(duì)象
在實(shí)際項(xiàng)目中贸营,我們發(fā)現(xiàn)有時(shí)候一個(gè) UIViewController 被釋放了,但它的 view 沒被釋放岩睁,或者一個(gè) UIView 被釋放了钞脂,但它的某個(gè) subview 沒被釋放。這種內(nèi)存泄露的情況很常見捕儒,因此冰啃,我們有必要遍歷基于 UIViewController 的整棵 View-ViewController 樹。我們通過 UIViewController 的 presentedViewController 和 view 屬性刘莹,UIView 的 subviews 屬性等遞歸遍歷阎毅。對(duì)于某些 ViewController,如 UINavigationController点弯,UISplitViewController 等扇调,我們還需要遍歷 viewControllers 屬性。構(gòu)建堆棧信息
需要構(gòu)建 View-ViewController stack 信息以告訴開發(fā)者是哪個(gè)對(duì)象沒被釋放蒲拉。在遞歸遍歷 View-ViewController 樹時(shí)肃拜,子節(jié)點(diǎn)的 stack 信息由父節(jié)點(diǎn)的 stack 信息加上子結(jié)點(diǎn)信息即可。例外機(jī)制
對(duì)于有些 ViewController雌团,在被 pop 或 dismiss 后燃领,不會(huì)被釋放(比如單例),因此需要提供機(jī)制讓開發(fā)者指定哪個(gè)對(duì)象不會(huì)被釋放锦援,這里可以通過重載上面的 -willDealloc 方法猛蔽,直接 return NO 即可。特殊情況
對(duì)于某些特殊情況灵寺,釋放的時(shí)機(jī)不大一樣(比如系統(tǒng)手勢返回時(shí)曼库,在劃到一半時(shí) hold 住,雖然已被 pop略板,但這時(shí)還不會(huì)被釋放毁枯,ViewController 要等到完全 disappear 后才釋放),需要做特殊處理叮称,具體的特殊處理視具體情況而定种玛。系統(tǒng)View
某些系統(tǒng)的私有 View藐鹤,不會(huì)被釋放(可能是系統(tǒng) bug 或者是系統(tǒng)出于某些原因故意這樣做的,這里就不去深究了)赂韵,因此需要建立白名單手動(dòng)擴(kuò)展
MLeaksFinder目前只檢測 ViewController 跟 View 對(duì)象娱节。為此,MLeaksFinder 提供了一個(gè)手動(dòng)擴(kuò)展的機(jī)制祭示,你可以從 UIViewController 跟 UIView 出發(fā)肄满,去檢測其它類型的對(duì)象的內(nèi)存泄露。如下所示质涛,我們可以檢測 UIViewController 底下的 View Model:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}
這里的原理跟上面的是一樣的稠歉,宏 MLCheck() 做的事就是為傳進(jìn)來的對(duì)象建立 View-ViewController stack 信息,并對(duì)傳進(jìn)來的對(duì)象調(diào)用 -willDealloc 方法蹂窖。
- 查找循環(huán)引用鏈
Facebook 開源了的循環(huán)引用檢測工具 FBRetainCycleDetector轧抗。當(dāng)傳入內(nèi)存中的任意一個(gè) OC 對(duì)象,F(xiàn)BRetainCycleDetector 會(huì)遞歸遍歷該對(duì)象的所有強(qiáng)引用的對(duì)象瞬测,以檢測以該對(duì)象為根結(jié)點(diǎn)的強(qiáng)引用樹有沒有循環(huán)引用横媚。
我們知道,很多循環(huán)引用是 block 的使用不當(dāng)造成的月趟。而 FBRetainCycleDetector 最大的技術(shù)亮點(diǎn)灯蝴,正在于如何找出一個(gè) block 的所有強(qiáng)引用對(duì)象。
然而孝宗,F(xiàn)BRetainCycleDetector 的使用存在兩個(gè)問題:
需要找到候選的檢測對(duì)象
檢測循環(huán)引用比較耗時(shí)
正是由于這兩個(gè)問題穷躁,F(xiàn)BRetainCycleDetector 通常是結(jié)合其它工具一起使用,通過其它工具先找出候選的檢測對(duì)象因妇,然后進(jìn)行有選擇的檢測问潭。當(dāng) MLeaksFinder 與 FBRetainCycleDetector 結(jié)合使用時(shí),正好能達(dá)到很好的效果婚被。我們先通過 MLeaksFinder 找到內(nèi)存泄漏的對(duì)象狡忙,然后再過 FBRetainCycleDetector 檢測該對(duì)象有沒有循環(huán)引用即可。