一、概述 MLeaksFinder 是腾讯 WeRead 团队开源的 iOS 内存泄露检测工具。 MLeaksFinder 的使用非常简单,使用 pod 添加了 MLeaksFinder 依赖之后,如果页面出现了内存泄露,APP 进行弹窗提示:
接下来,通过 MLeaksFinder 源码,看下 MLeaksFinder 的实现原理。
二、实现原理 1、willDealloc 方法 MLeaksFinder 在 NSObject (MemoryLeak)
新增了一个 willDealloc
方法,其实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 - (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 ; } - (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, className, [self viewStack]); }
willDealloc
方法主要逻辑如下:
白名单校验,过滤掉一些系统 UI 组件。
某些系统的私有 View
,由于系统机制(也可能是 BUG)原因不会被释放,所以增加白名单过滤掉这些 View
。
判断 self
是否是 LatestSender
,如果是则不检测。
某些情况下,刚刚触发的事件(点击、操作)可能会暂时保留对象,可能会导致短期内无法释放。此处判断是为了防止出现误报。
UIApplication+MemoryLeak
中 hook 了 sendAction:to:from:forEvent:
方法,以 kLatestSenderKey
作为关联对象的 Key 将 LatestSender
保存到了 UIApplication
中。
延迟 2 秒,如果 2 秒后 self
还没有销毁,则判定发生内存泄露。
对象销毁后,当前的 GCD 延迟任务仍然会执行,但 self
变成了 nil
,assertNotDealloc
就执行不到了。
对象被销毁后,dispatch_after
提交的任务仍会执行,这是因为任务的调度与对象的生命周期无关。调用 dispatch_after
时,GCD 会将 block
注册到一个定时器中。当指定时间到达时,定时器会将该 block
提交到目标队列,无论对象是否已经被销毁。
assertNotDealloc
方法主要逻辑如下:
判断 self
的父指针(parentPtrs
)是否有内存泄露,如果有,直接 return
。
其父指针集合中有内存泄露,那么当前对象很有可能也会发生泄漏。父对象的泄漏弹窗提示已经包含了关于父子关系链的相关信息,子对象不再重复提示,避免过多弹窗。
调用 MLeakedObjectProxy
的 addLeakedObject:
方法。
接下来看下 addLeakedObject:
方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + (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 }
可以看到,该方法中主要逻辑就是将发生内存泄露的 self
保存到 leakedObjectPtrs
中,并 alert 弹窗提示开发者发生了内存泄露。
2、willDealloc 调用时机 既然通过 NSObject+MemoryLeak
新增的 willDealloc
方法中检测内存泄露并弹窗提示,那么 willDealloc
方法调用时机是什么呢?
(1)UINavigationController (MemoryLeak) 忽略其中 iPad 适配逻辑,精简后的 UINavigationController (MemoryLeak)
主要逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 static const void *const kPoppedDetailVCKey = &kPoppedDetailVCKey;@implementation UINavigationController (MemoryLeak )+ (void )load { static dispatch_once_t onceToken; dispatch_once (&onceToken, ^{ [self swizzleSEL:@selector (popViewControllerAnimated:) withSEL:@selector (swizzled_popViewControllerAnimated:)]; [self swizzleSEL:@selector (popToViewController:animated:) withSEL:@selector (swizzled_popToViewController:animated:)]; [self swizzleSEL:@selector (popToRootViewControllerAnimated:) withSEL:@selector (swizzled_popToRootViewControllerAnimated:)]; }); } - (UIViewController *)swizzled_popViewControllerAnimated:(BOOL )animated { UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated]; if (!poppedViewController) { return nil ; } extern const void *const kHasBeenPoppedKey; objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES ), OBJC_ASSOCIATION_RETAIN); return poppedViewController; } - (NSArray <UIViewController *> *)swizzled_popToViewController:(UIViewController *)viewController animated:(BOOL )animated { NSArray <UIViewController *> *poppedViewControllers = [self swizzled_popToViewController:viewController animated:animated]; for (UIViewController *viewController in poppedViewControllers) { [viewController willDealloc]; } return poppedViewControllers; } - (NSArray <UIViewController *> *)swizzled_popToRootViewControllerAnimated:(BOOL )animated { NSArray <UIViewController *> *poppedViewControllers = [self swizzled_popToRootViewControllerAnimated:animated]; for (UIViewController *viewController in poppedViewControllers) { [viewController willDealloc]; } return poppedViewControllers; } - (BOOL )willDealloc { if (![super willDealloc]) { return NO ; } [self willReleaseChildren:self .viewControllers]; return YES ; } @end
可以看到,上面主要 hook 了 UINavigationController
的下面这几个方法:
popViewControllerAnimated:
当前 UIViewController
被 pop 后,将该 UIViewController
标记成 BeenPopped
。
popToViewController:animated:
该方法的返回值是包含所有被 pop 的 UIViewController
数组。
遍历该数组,逐个调用内部 UIViewController
的 willDealloc
。
popToRootViewControllerAnimated:
该方法的返回值也是包含所有被 pop 的 UIViewController
数组。
遍历该数组,逐个调用内部 UIViewController
的 willDealloc
。
可以看到,在 UIViewController
被 pop 后,该 UIViewController
的 willDealloc
方法会被调用。
(2)UIViewController (MemoryLeak) UIViewController (MemoryLeak)
中逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 const void *const kHasBeenPoppedKey = &kHasBeenPoppedKey;@implementation UIViewController (MemoryLeak )+ (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:(BOOL )animated { [self swizzled_viewDidDisappear:animated]; if ([objc_getAssociatedObject(self , kHasBeenPoppedKey) boolValue]) { [self willDealloc]; } } - (void )swizzled_viewWillAppear:(BOOL )animated { [self swizzled_viewWillAppear:animated]; objc_setAssociatedObject(self , kHasBeenPoppedKey, @(NO ), OBJC_ASSOCIATION_RETAIN); } - (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]; } - (BOOL )willDealloc { if (![super willDealloc]) { return NO ; } [self willReleaseChildren:self .childViewControllers]; [self willReleaseChild:self .presentedViewController]; if (self .isViewLoaded) { [self willReleaseChild:self .view]; } return YES ; } @end
上面主要 hook 了 UIViewController
的下面这几个方法:
viewDidDisappear:
如果当前 UIViewController
被标记为已弹出(kHasBeenPoppedKey
),则会调用 willDealloc
方法进行内存泄漏检测
viewWillAppear:
将 UIViewController
的已弹出标记重置为 NO。
dismissViewControllerAnimated:completion:
获取被 dismiss 的 UIViewController
并调用 willDealloc
方法进行内存泄漏检测。
由于 UIViewController
的这个 category 中也实现了 willDealloc
方法,所以上一步 UINavigationController (MemoryLeak)
中,调用的 willDealloc
实际上这里的 willDealloc
方法。
UIViewController(MemoryLeak)
的 willDealloc
中又调用了 [super willDealloc]
,此处会走进前面的 NSObject (MemoryLeak)
中的 willDealloc
中,执行真正的内存泄露检测逻辑。
(3)总结 总结前述主要逻辑如下:
NSObject (MemoryLeak)
的 willDealloc
是内存泄露检测核心逻辑。
使用 GCD 创建 2s 的延迟任务,如果 2s 后对象还没释放,则弹窗提示发生内存泄露。
对象销毁后,当前的 GCD 延迟任务仍然会执行,但 self 变成了 nil
,所以后续弹窗提示的方法就执行不到了。
willDealloc
执行时机如下:
调用了 UINavigationController
如下 pop 方法后,遍历方法返回的被 pop 的 UIViewController
数组,逐个调用内部 UIViewController
的 willDealloc:
popToViewController:animated:
popToRootViewControllerAnimated:
UIViewController
的 viewDidDisappear
方法被调用,且 UIViewController
已经被标记成 HasBeenPopped
时,调用 willDealloc
。
UINavigationController (MemoryLeak)
hook popViewControllerAnimated:
方法,当 UIViewController
被 pop 时将其标记成 HasBeenPopped
。
UIViewController
被 dismiss 时,调用 willDealloc
。