記憶體洩漏的高效檢測方法 - MLeaksFinder
對於iOS開發者而言,記憶體洩漏是一個老生常談的問題,包括日常開發和麵試過程中,都會涉及到這方面的知識。
當然Xcode提供了Instrument
工具,我們一般都會用Leaks / Allocations
來排查記憶體洩漏,網上也有一些開源庫來進行記憶體洩漏的排查
MLeaksFinder - 簡介
MLeaksFinder
是 WeRead 團隊開源的iOS記憶體洩漏檢測工具,wereadteam部落格,GitHub。
MLeaksFinder
提供了記憶體洩露檢測更好的解決方案。引進 MLeaksFinder
後,就可以在日常的開發,除錯業務邏輯的過程中自動地發現並警告記憶體洩漏。開發者無需開啟 Instrument
當發生記憶體洩漏時,MLeaksFinder
會用彈窗alert
的形式告訴開發者記憶體洩漏的物件,開發者可以把alert
關掉,並繼續除錯業務邏輯。
MLeaksFinder - 使用方式
把 MLeaksFinder
目錄下的檔案新增到你的專案中,就可以在執行時(debug 模式下)幫助你檢測專案裡的記憶體洩露了,無需修改任何業務邏輯程式碼,而且只在 debug 下開啟,完全不影響你的 release 包。
引入MLeaksFinder
可選擇用CocoaPods
安裝,安裝時注意有沒有warnings
,特別是 OTHER_LDFLAGS
相關的 warnings
。如果有 warnings
,可以在主工程的 Build Settings -> Other Linker Flags
加上 -ObjC
。
亦可手動引入,直接把 MLeaksFinder
的程式碼放到專案裡即生效。如果把 MLeaksFinder
做為子工程,需要在主工程的 Build Settings -> Other Linker Flags
加上 -ObjC
。
引入後,先驗證引入是否成功,在UIViewController+MemoryLeak.m
+ (void)load
方法中新增斷點,app啟用時進入該方法便引入成功。
引進 MLeaksFinder
的程式碼後即可檢測記憶體洩漏,但查詢迴圈引用的功能還未生效。可以再手動加入 FBRetainCycleDetector
程式碼,然後把 MLeaksFinder.h
裡的 //#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1
開啟。
MLeaksFinder
預設只在 debug 下生效,當然也可以通過 MLeaksFinder.h
裡的 //#define MEMORY_LEAKS_FINDER_ENABLED 0
來手動控制開關。
MLeaksFinder - 原理
從UIViewController
入手,當一個UIViewController
被pop或者dismiss後,該VC包括它的子View,或者子view的子view等等都會很快的被釋放(除非設計成單例,或者持有它的強引用,但一般很少這樣做)。於是,我們只需在一個ViewController
被pop或者dismiss一小段時間後,看看該VC,它的subViews等是否還存在。
通過UIViewController+MemoryLeak.h
的load
方法可以看出,交換了viewDidDisappear:、viewWillAppear:、dismissViewControllerAnimated:completion:
三個方法。
下面分析一下方法viewDidDisappear:
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self,kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
複製程式碼
呼叫了當前類的-willDealloc
方法,
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
複製程式碼
通過super呼叫父類的-willDealloc
,重點說明一下該方法
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication],kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
__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;
}
複製程式碼
- 第一步:首先通過
classNamesWhitelist
檢測白名單,如果物件在白名單之中,便return NO
,即不是記憶體洩漏。
構建基礎白名單時,使用了單例,確保只有一個,這個方法是私有的。
+ (NSMutableSet *)classNamesWhitelist {
static NSMutableSet *whitelist = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
whitelist = [NSMutableSet setWithObjects:
@"UIFieldEditor",// UIAlertControllerTextField
@"UINavigationBar",@"_UIAlertControllerActionView",@"_UIVisualEffectBackdropView",nil];
// System's bug since iOS 10 and not fixed yet up to this ci.
NSString *systemVersion = [UIDevice currentDevice].systemVersion;
if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
[whitelist addObject:@"UISwitch"];
}
});
return whitelist;
}
複製程式碼
同時,在NSObject+MemoryLeak.h
檔案中提供了一個方法,使得我們可以自定義白名單
+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
[[self classNamesWhitelist] addObjectsFromArray:classNames];
}
複製程式碼
- 第二步:判斷該物件是否是上一次傳送action的物件,是的話,不進行記憶體檢測
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication],kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
複製程式碼
- 第三步:弱指標指向self,2s延遲,然後通過這個弱指標呼叫
-assertNotDealloc
,若被釋放,給nil發訊息直接返回,不觸發-assertNotDealloc
方法,認為已經釋放;如果它沒有被釋放(洩漏了),-assertNotDealloc
就會被呼叫
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,^{
__strong id strongSelf = weakSelf;
[strongSelf assertNotDealloc];
});
複製程式碼
assertNotDealloc
方法放在最後再談
接著會呼叫-willReleaseChildren、-willReleaseChild
遍歷該物件的子物件,判斷是否釋放
- (void)willReleaseChild:(id)child {
if (!child) {
return;
}
[self willReleaseChildren:@[ child ]];
}
- (void)willReleaseChildren:(NSArray *)children {
NSArray *viewStack = [self viewStack];
NSSet *parentPtrs = [self parentPtrs];
for (id child in children) {
NSString *className = NSStringFromClass([child class]);
[child setViewStack:[viewStack arrayByAddingObject:className]];
[child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
[child willDealloc];
}
}
複製程式碼
通過程式碼可以看出,-willReleaseChildren
拿到當前物件的viewStack
和parentPtrs
,然後遍歷children
,為每個子物件設定viewStack
和parentPtrs
。
然後會執行[child willDealloc]
,去檢測子類。
這裡結合原始碼看下viewStack
與parentPtrs
的get和set實現方法
- (NSArray *)viewStack {
NSArray *viewStack = objc_getAssociatedObject(self,kViewStackKey);
if (viewStack) {
return viewStack;
}
NSString *className = NSStringFromClass([self class]);
return @[ className ];
}
- (void)setViewStack:(NSArray *)viewStack {
objc_setAssociatedObject(self,kViewStackKey,viewStack,OBJC_ASSOCIATION_RETAIN);
}
- (NSSet *)parentPtrs {
NSSet *parentPtrs = objc_getAssociatedObject(self,kParentPtrsKey);
if (!parentPtrs) {
parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self),nil];
}
return parentPtrs;
}
- (void)setParentPtrs:(NSSet *)parentPtrs {
objc_setAssociatedObject(self,kParentPtrsKey,parentPtrs,OBJC_ASSOCIATION_RETAIN);
}
複製程式碼
兩者實現方法類似,通過執行時機制,即利用關聯物件給一個類新增屬性資訊,只不過前者是一個數組,後者是一個集合。
關聯物件parentPtrs
,會在-assertNotDealloc
中,會判斷當前物件是否與父節點集合有交集。下面仔細看下-assertNotDealloc
方法
- (void)assertNotDealloc {
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[MLeakedObjectProxy addLeakedObject:self];
NSString *className = NSStringFromClass([self class]);
NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced,override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@",className,[self viewStack]);
}
複製程式碼
這裡呼叫了MLeakedObjectProxy
類中的+isAnyObjectLeakedAtPtrs
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
NSAssert([NSThread isMainThread],@"Must be in main thread.");
static dispatch_once_t onceToken;
dispatch_once(&onceToken,^{
leakedObjectPtrs = [[NSMutableSet alloc] init];
});
if (!ptrs.count) {
return NO;
}
if ([leakedObjectPtrs intersectsSet:ptrs]) {
return YES;
} else {
return NO;
}
}
複製程式碼
該方法中初始化了一個單例物件leakedObjectPtrs
,通過leakedObjectPtrs
與傳入的引數[self parentPtrs]
檢測他們的交集,傳入的 ptrs 中是否是洩露的物件。
如果上述方法返回的是NO,則繼續呼叫下面方法+addLeakedObject
+ (void)addLeakedObject:(id)object {
NSAssert([NSThread isMainThread],@"Must be in main thread.");
MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init];
proxy.object = object;
proxy.objectPtr = @((uintptr_t)object);
proxy.viewStack = [object viewStack];
static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
objc_setAssociatedObject(object,kLeakedObjectProxyKey,proxy,OBJC_ASSOCIATION_RETAIN);
[leakedObjectPtrs addObject:proxy.objectPtr];
#if _INTERNAL_MLF_RC_ENABLED
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@",proxy.viewStack]
delegate:proxy
additionalButtonTitle:@"Retain Cycle"];
#else
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@",proxy.viewStack]];
#endif
}
複製程式碼
第一步:構造MLeakedObjectProxy
物件,給傳入的洩漏物件 object
關聯一個代理即 proxy
第二步:通過objc_setAssociatedObject(object,OBJC_ASSOCIATION_RETAIN)
方法,object
強持有proxy
, proxy
若持有object
,如果object
釋放,proxy
也會釋放
第三步:儲存 proxy.objectPtr
(實際是物件地址)到集合 leakedObjectPtrs
裡邊
第四步:彈框 AlertView
若 _INTERNAL_MLF_RC_ENABLED == 1
,則彈框會增加檢測迴圈引用的選項;若 _INTERNAL_MLF_RC_ENABLED == 0
,則僅展示堆疊資訊。
對於MLeakedObjectProxy
類而言,是檢測到記憶體洩漏才產生的,作為洩漏物件的屬性存在的,如果洩漏的物件被釋放,那麼MLeakedObjectProxy
也會被釋放,則呼叫-dealloc
函式
集合leakedObjectPtrs
中移除該物件地址,同時再次彈窗,提示該物件已經釋放了
- (void)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(),^{
[leakedObjectPtrs removeObject:objectPtr];
[MLeaksMessenger alertWithTitle:@"Object Deallocated"
message:[NSString stringWithFormat:@"%@",viewStack]];
});
}
複製程式碼
當點選彈框中的檢測迴圈引用按鈕時,相關的操作都在下面 AlertView
的代理方法裡邊,即非同步地通過 FBRetainCycleDetector
檢測迴圈引用,然後回到主執行緒,利用彈框提示使用者檢測結果。
同時控制檯會有相關輸出
可以快速定位到記憶體洩漏的位置。另外,針對一些特殊情況:
- 有時候即使調了pop,dismiss,也不會被釋放,比如單例。如果某個特別的物件不會被釋放,開發者可以重寫
willDealloc
,return NO
- 部分系統的view是不會被釋放的,需要建立白名單
-
MLeaksFinder
支援手動擴充套件,通過MLCheck()
巨集來檢測其他型別的物件的記憶體洩露,為傳進來的物件建立View-ViewController stack資訊 - 結合
FBRetainCycleDetector
一起使用時:- 記憶體洩漏不一定是迴圈引用造成的
- 有的迴圈引用
FBRetainCycleDetector
不一定能找出
參考資料: