
0.前言
項目中集成了 MLeaksFinder 用于平時檢測內(nèi)存泄漏之用,它的基本工作原理也多少了解一些,最近恰好有點空閑時間,決定還是仔細看一下源碼實現(xiàn),畢竟自己查的才比較放心O(∩_∩)O哈哈~,于是就有了本文。
1.用法
MLeaksFinder 的使用非常人性化,直接通過 cocoaPods 導入工程中就行,當檢測到泄露的時候會自動在控制臺打印出相關堆棧信息。
2.原理
2.1 基本原理
一般情況下,當一個 ViewController 或 NavigationController 被 dismiss 或 pop 的時候,它自己、view、view的 subView 等都應該會很快釋放掉。于是,只需要在 dismiss 或 pop 之后檢測這些對象是否還存在,就可以判斷是否存在內(nèi)泄。
2.2 MLeaksFinder 的基本實現(xiàn)
為基類 NSObject 添加一個方法 -willDealloc ,它先用一個弱指針指向 self,并在一小段時間 (2秒) 后,通過這個弱指針調(diào)用 -assertNotDealloc,而 -assertNotDealloc 主要作用是打印堆棧信息 (早期版本是直接中斷言,不過那樣會打斷正常的開發(fā)工作)。
當我們認為某個對象應該要被釋放了,在釋放前調(diào)用 -assertNotDealloc ,如果 2 秒后它被釋放成功,weakSelf 就指向 nil,-assertNotDealloc 方法就不會執(zhí)行(向 nil 發(fā)送消息,實際什么也不會做),如果它沒被釋放,-assertNotDealloc 就會執(zhí)行,從而打印出堆棧信息。
于是,當一個NavigationController 或 UIViewController 被 pop 或 dismiss 時,我們遍歷它的所有 view,依次調(diào) -willDealloc(對 -willDealloc 的調(diào)用是通過 method-swizzle 追加到 pop/dismiss 方法中的),若 2 秒后沒被釋放,就會打印相關堆棧信息。
3.源碼
NSObject+MemoryLeak
先來看看基類 NSObject 的分類 NSObject+MemoryLeak ,這里提供了以下公開方法,詳見注釋,其中第二個方法只有這個宏 #define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET]; 會用到,而這個宏是留給我們擴展功能使用的。
/// 入口方法
- (BOOL)willDealloc;
/// 用于擴展,即 MLCheck(TARGET) 中會用到
- (void)willReleaseObject:(id)object relationship:(NSString *)relationship;
// 用于構造堆棧信息
- (void)willReleaseChild:(id)child;
- (void)willReleaseChildren:(NSArray *)children;
/// 堆棧信息數(shù)組,元素是類名
- (NSArray *)viewStack;
/// 添加新類名到白名單
+ (void)addClassNamesToWhitelist:(NSArray *)classNames;
/// 交換方法
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL;
接著查看他的實現(xiàn)文件,首先是 -willDealloc 方法,實現(xiàn)如下,做了三件事:
- (BOOL)willDealloc {
// 1.檢測白名單
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
// 2.fix bug
NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;
// 3.核心:延遲 2 秒執(zhí)行 -assertNotDealloc 方法
__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;
}
-
檢測白名單
檢測當前對象是否在白名單中,如果在,就不調(diào)用
-assertNotDealloc方法,既不檢測內(nèi)泄。構建基礎白名單時,使用了單例,確保只有一個,這個方法是私有的。+ (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; }另外,用戶也可以自行添加額外的類名,方法如下:
+ (void)addClassNamesToWhitelist:(NSArray *)classNames { [[self classNamesWhitelist] addObjectsFromArray:classNames]; } -
修復一個 bug
此處處理一個了bug,即
在button 的點擊事件或者UITableview 的點擊Cell的事件中調(diào)用self.navigationController popViewControllerAnimated:YES 時就報沒有釋放,詳見文末的參考。 -
核心:延遲 2 秒執(zhí)行
-assertNotDealloc方法用弱引用 weakSelf 指向 self,延遲 2 秒執(zhí)行 weakSelf 的
-assertNotDealloc方法,這個方法如果能夠執(zhí)行,則說明當前對象泄露了。
從下面的代碼可以看出來,-assertNotDealloc 做了2 件事:
- (void)assertNotDealloc {
// 1.檢測父控件體系中是否有沒被釋放的
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[MLeakedObjectProxy addLeakedObject:self];
// 2.打印堆棧信息
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, className, [self viewStack]);
}
-
判斷當前對象父控件的層級體系中是否有沒被釋放的對象,如果有就不往下執(zhí)行了,否則把自己加進去,并打印堆棧信息。
因為父對象的
-willDealloc會先執(zhí)行,所以如果父對象一定會銷毀的話,那么也應該是先銷毀,即先從MLeakedObjectProxy中移除,加了這個判斷之后,就不會出現(xiàn)一個堆棧中出現(xiàn)多個未釋放對象的情況。這里用到了 2 個
MLeakedObjectProxy中的方法+isAnyObjectLeakedAtPtrs:和+addLeakedObject:,后邊會講到。 打印
viewStack這個數(shù)組,數(shù)組里存放的是從父對象到子對象,一直到當前對象的類名。
我們看看 viewStack 的 setter 和 getter,這里用到了運行時機制,即利用關聯(lián)對象給一個類添加屬性信息。viewStack 是一個數(shù)組,存放的是類名,從 getter 可以看出來,初次使用時,直接將當前類名作為第一個元素添加進去了。
- (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);
}
順便看一下前邊用到的 parentPtrs 的 setter 和 getter,從下邊的源碼可看出來,二者的方法實現(xiàn)類似,只不過后者是一個集合 set,前者是 數(shù)組 array。
- (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);
}
那么,后續(xù)這個 viewStack 和 parentPtrs 又是什么時候構建的呢?這里提供了 2 個供外界調(diào)用的構建方法,最終是依賴后一個方法 - willReleaseChildren 實現(xiàn)的。
- (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: 方法發(fā)現(xiàn),就做了兩件事:
拿到當前對象的 viewStack 和 parentPtrs,然后遍歷 children,為每一個 child 設置
viewStack和parentPtrs,而且是將自己 (child) 加進去了的。執(zhí)行
[child willDealloc];,結合前邊提到的willDealloc知道,這就去檢測子類了。
在這個類的最后,提供了一個交換方法的方法:
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED
#if _INTERNAL_MLF_RC_ENABLED
// Just find a place to set up FBRetainCycleDetector.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_async(dispatch_get_main_queue(), ^{
[FBAssociationManager hook];
});
});
#endif
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
// YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name).
BOOL didAddMethod =
class_addMethod(class,
originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
#endif
}
交換方法代碼就做介紹了,下邊主要講一下用到的兩個宏:_INTERNAL_MLF_ENABLED 和_INTERNAL_MLF_RC_ENABLED
MLeaksFinder.h
為了弄清楚上邊提到的兩個宏,我們來看看 MLeaksFinder.h 這個文件,分了兩部分:
_INTERNAL_MLF_ENABLED
下邊的條件編譯語句用于確定 _INTERNAL_MLF_ENABLED 的值,即決定是否需要開啟內(nèi)存泄漏的檢測,默認是在 DEBUG 模式下檢測,當然,也可以自己修改這個值。
//#define MEMORY_LEAKS_FINDER_ENABLED 0
#ifdef MEMORY_LEAKS_FINDER_ENABLED
#define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED
#else
#define _INTERNAL_MLF_ENABLED DEBUG // DEBUG 環(huán)境的話,_INTERNAL_MLF_ENABLED == 1
#endif
_INTERNAL_MLF_RC_ENABLED
下邊的條件編譯語句用于確定 _INTERNAL_MLF_RC_ENABLED 的值,即決定是否需要開啟循環(huán)引用的檢測,默認是 如果項目中使用了 CocoaPods,則會通過 FBRetainCycleDetector 進行檢測。實際使用 CocoaPods 導入 MLeaksFinder 的時候,會將 FBRetainCycleDetector 一并導入,對于后者的工作原理,會單獨分一篇介紹的。
//#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1
#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
#elif COCOAPODS
#define _INTERNAL_MLF_RC_ENABLED COCOAPODS
#endif
MLeakedObjectProxy
現(xiàn)在解決一個遺留問題,就是前邊 -willDealloc 方法中用到的 MLeakedObjectProxy 這個類,查看其 .h 文件發(fā)現(xiàn),對外只提供了兩個類方法:
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs;
+ (void)addLeakedObject:(id)object;
先看第一個方法:
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
NSAssert([NSThread isMainThread], @"Must be in main thread.");
// 1.初始化 leakedObjectPtrs
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
leakedObjectPtrs = [[NSMutableSet alloc] init];
});
if (!ptrs.count) {
return NO;
}
// 2.檢測 `leakedObjectPtrs` 與 `ptrs` 之間是否有交集
// 當 leakedObjectPtrs 中 至少有一個對象也出現(xiàn)在 ptrs 中時,返回 YES。
if ([leakedObjectPtrs intersectsSet:ptrs]) {
return YES;
} else {
return NO;
}
}
這里,首先初始化了 leakedObjectPtrs(為了保證唯一性,使用了單例),他是用來存儲發(fā)生內(nèi)泄的對象地址 (已經(jīng)轉成了數(shù)值,即 uintptr_t)。然后通過 -intersectsSet: 檢測 leakedObjectPtrs 與 ptrs 之間是否有交集,即傳入的 ptrs 中是否是泄露的對象。
下面看看第二個重要方法 + 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];
// 1.給每一個 object 關聯(lián)一個代理即proxy
static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
// 2.存儲 proxy.objectPtr 到集合 leakedObjectPtrs 里邊
[leakedObjectPtrs addObject:proxy.objectPtr];
// 3.彈框
#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
}
- 給傳入的泄漏對象 object 關聯(lián)一個代理即 proxy
- 存儲 proxy.objectPtr(實際是對象地址)到集合 leakedObjectPtrs 里邊
- 彈框 AlertView:若 _INTERNAL_MLF_RC_ENABLED == 1,則彈框會增加檢測循環(huán)引用的選項;若 _INTERNAL_MLF_RC_ENABLED == 0,則僅展示堆棧信息。
當點擊彈框中的檢測循環(huán)引用按鈕時,相關的操作都在下面 AlertView 的代理方法里邊,即異步地通過 FBRetainCycleDetector 檢測循環(huán)引用,然后回到主線程,利用彈框提示用戶檢測結果。
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (!buttonIndex) {
return;
}
id object = self.object;
if (!object) {
return;
}
#if _INTERNAL_MLF_RC_ENABLED
dispatch_async(dispatch_get_global_queue(0, 0), ^{
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self.object];
NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20];
BOOL hasFound = NO;
for (NSArray *retainCycle in retainCycles) {
NSInteger index = 0;
for (FBObjectiveCGraphElement *element in retainCycle) {
if (element.object == object) {
NSArray *shiftedRetainCycle = [self shiftArray:retainCycle toIndex:index];
dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]];
});
hasFound = YES;
break;
}
++index;
}
if (hasFound) {
break;
}
}
if (!hasFound) {
dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:@"Fail to find a retain cycle"];
});
}
});
#endif
}
}
UIViewController+MemoryLeak
說了這么多,最后,以 UIViewController 為例,查看一下檢測內(nèi)泄的入口,即如何實現(xiàn)調(diào)用 -willdeallloc 方法,即如何開始構建內(nèi)泄的堆棧信息的。
下面是 +load 方法,就是將幾個系統(tǒng)方法和自定義方法交換,以便給系統(tǒng)方法增加新的操作。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
[self swizzleSEL:@selector(viewWillAppear:) withSEL:@selector(swizzled_viewWillAppear:)];
[self swizzleSEL:@selector(dismissViewControllerAnimated:completion:) withSEL:@selector(swizzled_dismissViewControllerAnimated:completion:)];
});
}
然后,我們看看這三個自定義方法都做了些什么:
- (void)swizzled_viewDidDisappear:
先取出了 kHasBeenPoppedKey 對應的值,這個值是在右滑返回上個頁面并觸發(fā) pop 時,設置為 YES 的,說明當前 ViewController 要銷毀了,所以在這個時候調(diào)用了 -willDealloc 方法。
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
- (void)swizzled_viewWillAppear:
與上邊對應,這里是在當前 ViewController 的視圖展示出來的時候,將 kHasBeenPoppedKey 關聯(lián)的值設為 NO,即當前 ViewController 沒有通過右滑返回。
- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
- swizzled_dismissViewControllerAnimated:
前邊兩個方法是針對滑動返回做的處理,這里是針對通過 present 的對象 dismiss 時的操作,即如果當前 ViewController 沒有 presentedViewController,就直接調(diào)用當前 ViewController 的 -willDealloc 方法檢測內(nèi)泄。
- (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;
[dismissedViewController willDealloc];
}
最后重寫了 -willDealloc 方法,調(diào)用了 -willReleaseChildren: 方法,由于構建堆棧信息。
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
// viewController
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
// view
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
其它 UI 相關類的內(nèi)泄檢測與 ViewController 類似,這里就不啰嗦了。