主要分析MLeaksFinder的原理和具體實現(xiàn)
Leaks
從蘋果官方文檔可知,一個app的內(nèi)存主要分3類
Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
Abandoned memory: Memory still referenced by your application that has no useful purpose.
Cached memory: Memory still referenced by your application that might be used again for better performance.
其中 Leaked memory 和 Abandoned memory 都屬于應(yīng)該釋放而沒釋放的內(nèi)存拭嫁,都是內(nèi)存泄露,而 Leaks 工具只負責檢測 Leaked memory缺脉,而不管 Abandoned memory靶瘸。在 MRC 時代 Leaked memory 很常見,因為很容易忘了調(diào)用 release俗冻,但在 ARC 時代更常見的內(nèi)存泄露是循環(huán)引用導(dǎo)致的 Abandoned memory堆巧,Leaks 工具查不出這類內(nèi)存泄露妄荔,應(yīng)用有限舰涌。對于 Abandoned memory迁杨,可以用 Instrument 的 Allocations 檢測出來。檢測方法是用 Mark Generation 的方式讥蔽,當你每次點擊 Mark Generation 時荒揣,Allocations 會生成當前 App 的內(nèi)存快照篷角,而且 Allocations 會記錄從上回內(nèi)存快照到這次內(nèi)存快照這個時間段內(nèi),新分配的內(nèi)存信息系任。缺點是需要重復(fù)操作恳蹲,其無法及時得知泄漏
對于 Leaked memory,可以使用Leaks 工具檢測俩滥,適用于運行時的檢測
介紹
MLeaksFinder 提供了內(nèi)存泄露檢測更好的解決方案嘉蕾。只需要引入 MLeaksFinder,就可以自動在 App 運行過程檢測到內(nèi)存泄露的對象并立即提醒霜旧,無需打開額外的工具错忱,也無需為了檢測內(nèi)存泄露而一個個場景去重復(fù)地操作。MLeaksFinder 目前能自動檢測 UIViewController 和 UIView 對象的內(nèi)存泄露颁糟,而且也可以擴展以檢測其它類型的對象航背。
當發(fā)生內(nèi)存泄露時,MLeaksFinder 會中斷言棱貌,并準確的告訴你哪個對象泄露了。這里設(shè)計為中斷言而不是打日志讓程序繼續(xù)跑箕肃,是因為很多人不會去看日志婚脱,斷言則能強制開發(fā)者注意到并去修改,而不是犯拖延癥勺像。
優(yōu)勢
從 MLeaksFinder 的使用方法可以看出障贸,MLeaksFinder 具備以下優(yōu)點:
- 使用簡單,不侵入業(yè)務(wù)邏輯代碼吟宦,不用打開 Instrument
- 不需要額外的操作篮洁,你只需開發(fā)你的業(yè)務(wù)邏輯,在你運行調(diào)試時就能幫你檢測
- 內(nèi)存泄露發(fā)現(xiàn)及時殃姓,更改完代碼后一運行即能發(fā)現(xiàn)(這點很重要袁波,你馬上就能意識到哪里寫錯了)
- 精準瓦阐,能準確地告訴你哪個對象沒被釋放
原理
- 其本質(zhì)也是采用 category + runtime 實現(xiàn),主要是通過AOP切面編程思想來添加額外的功能篷牌,實現(xiàn)代碼無侵入的內(nèi)存泄漏檢測
-
其類的結(jié)構(gòu)圖示如下所示
具體實現(xiàn)
-
1睡蟋、尋找釋放點
這里主要是通過AOP思想,通過method swizzling替換了幾個釋放方法
- 2枷颊、追蹤泄漏
從上面Hook的方法的具體實現(xiàn)戳杀,我們發(fā)現(xiàn)一個共通點,最后都會調(diào)用 willDealloc 方法夭苗,說明當追蹤到頁面需要釋放時信卡,會調(diào)用此方法,以 UIViewController+MemoryLeak 分類中的 dismissViewControllerAnimated:completion: 方法為例题造,查看其具體實現(xiàn)
//load方法中hook dissmiss方法
[self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
//swizzled_dismissViewControllerAnimated:completion:的具體實現(xiàn)
- (void)swizzled_dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
[self swizzled_dismissViewControllerAnimated:flag completion:completion];
UIViewController *dismissedViewController = self.presentedViewController;
if (!dismissedViewController && self.presentingViewController) {
dismissedViewController = self;
}
if (!dismissedViewController) return;
//視圖控制器即將被釋放時調(diào)用 willDealloc 方法
[dismissedViewController willDealloc];
}
查看 willDealloc 的具體實現(xiàn)
- (BOOL)willDealloc {
//調(diào)用父類的 willDealloc 方法
if (![super willDealloc]) {
return NO;
}
//構(gòu)建堆棧信息
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
- 3傍菇、報告泄漏
上述調(diào)用父類的 willDealloc,其實就是調(diào)用 NSObject分類中的 willDealloc 方法晌梨,其中主要做了以下幾件事- 首先判斷這個class是否在白名單中桥嗤,如果在則忽略 ==> 白名單機制
- 如果當前對象在發(fā)送action則忽略它(因為 willDealloc)總會先于他們調(diào)用)
- 設(shè)置一個weak指針,在調(diào)用 dispatch_after 在2秒后調(diào)用 assertNotDealloc 方法仔蝌,如果還沒釋放泛领,那么會進入這個方法,如果已經(jīng)釋放了敛惊,那么這個方法是進不去的渊鞋。
- (BOOL)willDealloc {
//獲取類名
NSString *className = NSStringFromClass([self class]);
//判斷這個class是否在白名單中,如果是瞧挤,則忽略 -- 白名單機制
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
//UIControl的target-action機制锡宋,如果當前對象在發(fā)送action,則忽略(因為willDealloc會先于action調(diào)用)
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
//設(shè)置一個weak指針特恬,指向self
__weak id weakSelf = self;
//在2s后执俩,如果當前對象還沒有釋放,則會執(zhí)行到assertNotDealloc方法癌刽。如果已經(jīng)釋放役首,那么weakSelf則會是nil,不會走到assertNotDealloc方法
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong id strongSelf = weakSelf;
//直接中斷言显拜,即報告內(nèi)存泄漏
[strongSelf assertNotDealloc];
});
return YES;
}
- 4衡奥、構(gòu)建堆棧信息
調(diào)用了父類方法的實現(xiàn)后,在顯示堆棧信息之前远荠,還需要進行堆棧信息的構(gòu)造矮固,主要是通過willReleaseChild、willReleaseChildren兩個方法譬淳,其作用是向這個對象之中的子對象調(diào)用willDealloc 釋放的方法档址,如 view 的 subviews盹兢, UINavigationController 的 viewcontrollers 等等的子對象。
//向這個對象中的子對象調(diào)用釋放的方法
- (void)willReleaseChild:(id)child {
if (!child) {
return;
}
[self willReleaseChildren:@[ child ]];
}
//遍歷子對象辰晕,然后將父對象的class name加上子對象的class name蛤迎,一步步構(gòu)造出一個 視圖堆棧,如果出現(xiàn)內(nèi)存泄漏含友,直接打印此對象的 視圖堆棧即可
- (void)willReleaseChildren:(NSArray *)children {
NSArray *viewStack = [self viewStack]; //通過關(guān)聯(lián)對象獲取值 子視圖的 class name
NSSet *parentPtrs = [self parentPtrs]; //通過關(guān)聯(lián)對象獲取 父對象的 class name
for (id child in children) {
NSString *className = NSStringFromClass([child class]);
[child setViewStack:[viewStack arrayByAddingObject:className]];
[child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
[child willDealloc];
}
}
其整體的流程圖示如下