简介
MLeaksFinder是微信开源的可以在iOS开发阶段用来检测内存泄漏的库,可以自动的在UIView和UIViewController中发现泄漏,通过弹出框显示在View-ViewController中的泄漏对象。除了发现UIView和UIViewController的泄漏对象,开发者也可以扩展到其他类型的对象。
并且可以通过FBRetainCycleDetector找到引用环。
内存分类
- Leaked memory 不被应用引用的内存,无法再次使用或者释放(可以使用Leaks Instrument工具检测到)
- Abandoned memory 没有用处的,仍被应用引用的内存
- Cached memory 仍然被应用引用,可以被再次使用从而得到更好的性能
原理
除了单例及其持有它的强引用,当一个UIViewController被pop或dismiss后,对应的View,View的subViews等对象将很快被释放。
Hook
Hook所有页面退出的情况作为检测时机
-
UIViewController
- -dismiss
- -viewDidDisappear
-
UINavigationController
- -pop
- -popToRoot
- -popToViewController
视图层级
页面生命周期
UINavigationController -push/pop
-[MyUINavigationController pushViewController:animated:]
-[ParentViewController viewWillDisappear:]
-[ChildViewController viewWillAppear:]
-[ParentViewController viewDidDisappear:]
-[ChildViewController viewDidAppear:]
-[MyUINavigationController popViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ChildViewController viewDidDisappear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController dealloc]
从上述的页面生命周期可以看到,viewDidDisappear在页面做push的时候也会触发,所以不能单独以viewDidDisappear作为检测时机,需要以先有popViewControllerAnimated,后有viewDidDisappear为特征,才能作为页面出栈的判断。
Left-edge swipe
UINavigationController -pop有一种特殊情况是左滑手势
先左滑-后松开
-[MyUINavigationController popViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ParentViewController viewWillDisappear:]
-[ParentViewController viewDidDisappear:]
-[ChildViewController viewWillAppear:]
-[ChildViewController viewDidAppear:]
先左滑-后完成
-[MyUINavigationController popViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ChildViewController viewDidDisappear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController dealloc]
由于左滑开始的时候,popViewControllerAnimated已经触发,而且时长由用户决定。由于检测需要使用延迟执行,查看页面等对象是否释放,所以不能单独以
popViewControllerAnimated为检测时机,需要以先有popViewControllerAnimated,后有viewDidDisappear为特征,才能作为页面出栈的判断。
popToRoot/popToViewController
-[MyUINavigationController pushViewController:animated:]
-[MyUINavigationController pushViewController:animated:]
-[ParentViewController viewWillDisappear:]
-[OtherViewController viewWillAppear:]
-[ParentViewController viewDidDisappear:]
-[OtherViewController viewDidAppear:]
-[OtherViewController viewWillDisappear:]
-[ChildViewController viewWillAppear:]
-[OtherViewController viewDidDisappear:]
-[ChildViewController viewDidAppear:]
-[MyUINavigationController popToRootViewControllerAnimated:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[OtherViewController dealloc]
-[ChildViewController viewDidDisappear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController dealloc]
从页面生命周期可以看到,除了TopViewController和RootViewController/ToViewController,其他处于中间的ViewController在弹出页面栈的时候不会有生命周期的调用,可以获取中间的ViewController开始检测。
UIViewController -present/dismiss
Modal
Modal是指模态,模式状态,模态是人机交互过程中的一种状态,表现为用户相同的操作下可以产生不同的结果。例如使打开文件夹时,点击和右击会触发不同的效果。弹框也有对应的对话框,弹出框,全屏框等效果。
根据Modal的不同,ParentViewController的Appear状态会不同,当ParentViewController完全被遮挡的时候,会有Appear相关的页面生命周期变化。
不同模态下的页面生命周期不同,可以分为3类。
半覆盖
- UIModalPresentationPageSheet
- UIModalPresentationFormSheet
- UIModalPresentationAutomatic
- UIModalPresentationCustom
- UIModalPresentationPopover
-[ParentViewController presentViewController:animated:completion:]
-[ChildViewController viewWillAppear:]
-[ChildViewController viewDidAppear:]
-[ChildViewController dismissViewControllerAnimated:completion:]
-[ChildViewController viewWillDisappear:]
-[ChildViewController viewDidDisappear:]
-[ChildViewController dealloc]
全覆盖
- UIModalPresentationCurrentContext
- UIModalPresentationFullScreen
-[ParentViewController presentViewController:animated:completion:]
-[ParentViewController viewWillDisappear:]
-[ChildViewController viewWillAppear:]
-[ChildViewController viewDidAppear:]
-[ParentViewController viewDidDisappear:]
-[ChildViewController dismissViewControllerAnimated:completion:]
-[ChildViewController viewWillDisappear:]
-[ParentViewController viewWillAppear:]
-[ParentViewController viewDidAppear:]
-[ChildViewController viewDidDisappear:]
-[ChildViewController dealloc]
报错
- UIModalPresentationOverFullScreen
- UIModalPresentationNone
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'The specified modal presentation style doesn't have a corresponding presentation controller.'
这两种模态,会导致报OC语言层面异常,模态展示方式和对应controller不匹配
代码解析
使用了Method Swizzle对页面的生命周期进行了Hook
Method Swizzle
NSObject (MemoryLeak)中对swizzle做了封装
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);
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
}
使用宏作为功能开关,DEBUG的状态下打开,或者手动设置MEMORY_LEAKS_FINDER_ENABLED为1打开
//#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
#endif
UINavigationController (MemoryLeak)
popViewControllerAnimated
在该生命周期中标记kHasBeenPoppedKey为YES
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
if (!poppedViewController) {
return nil;
}
// Detail VC in UISplitViewController is not dealloced until another detail VC is shown
if (self.splitViewController &&
self.splitViewController.viewControllers.firstObject == self &&
self.splitViewController == poppedViewController.splitViewController) {
objc_setAssociatedObject(self, kPoppedDetailVCKey, poppedViewController, OBJC_ASSOCIATION_RETAIN);
return poppedViewController;
}
// VC is not dealloced until disappear when popped using a left-edge swipe gesture
extern const void *const kHasBeenPoppedKey;
objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);
return poppedViewController;
}
在UIViewController的viewWillAppear中,标记kHasBeenPoppedKey为NO
- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
popToViewController
获取中间的ViewController开始检测
- (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;
}
popToRootViewControllerAnimated
获取中间的ViewController开始检测
- (NSArray<UIViewController *> *)swizzled_popToRootViewControllerAnimated:(BOOL)animated {
NSArray<UIViewController *> *poppedViewControllers = [self swizzled_popToRootViewControllerAnimated:animated];
for (UIViewController *viewController in poppedViewControllers) {
[viewController willDealloc];
}
return poppedViewControllers;
}
UIViewController (MemoryLeak)
viewDidDisappear
判断kHasBeenPoppedKey为YES,然后开始检测
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
dismissViewControllerAnimated
- (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];
}
对应的presentedViewController,presentingViewController,self对应如下,获取到dismissedViewController并执行检测。由于dismissedViewController并没有边缘手势的影响,不需要使用dimiss和viewDidDisappear叠加的方式来判断检测的时机。
-[ChildViewController dismissViewControllerAnimated:completion:]
self.presentedViewController (null)
self.presentingViewController <MyUINavigationController: 0x10181d600>
self <ChildViewController: 0x102d043e0>
-[ChildViewController viewWillDisappear:]
-[ChildViewController viewDidDisappear:]
-[ChildViewController dealloc]
Action
获取对应类的weakSelf,延迟2秒在主队列执行,调用weakSelf中的assertNotDealloc方法,方法还能响应的话就记录相关信息
类结构
NSObject
-
UIViewController
- UINavigationController
- UIPageViewController
- UISplitViewController
- UITabBarController
-
UIView
检测对象中存在继承关系,检测方法也使用了继承关系
代码解析
NSObject (MemoryLeak)
willDealloc
- 如果在白名单中,不进行检测
- 如果泄漏对象是最近一次UIControl事件的触发者,不进行检测(按照修改记录来看,当使用Button来pop ViewController的时候,VC不能及时释放)
- (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;
}
UIApplication (MemoryLeak)
- 获取和更新LatestSender
- (BOOL)swizzled_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event {
objc_setAssociatedObject(self, kLatestSenderKey, @((uintptr_t)sender), OBJC_ASSOCIATION_RETAIN);
return [self swizzled_sendAction:action to:target from:sender forEvent:event];
}
UITouch (MemoryLeak)
- 获取和更新LatestSender
- (void)swizzled_setView:(UIView *)view {
[self swizzled_setView:view];
if (view) {
objc_setAssociatedObject([UIApplication sharedApplication],
kLatestSenderKey,
@((uintptr_t)view),
OBJC_ASSOCIATION_RETAIN);
}
}
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, className, [self viewStack]);
}
willReleaseChildren
- 遍历视图树,构建视图堆栈ViewStack
- 记录父节点
- (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];
}
}
UINavigationController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UIPageViewController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UISplitViewController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UITabBarController (MemoryLeak)
willDealloc
- 检测self.viewControllers
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
UIViewController (MemoryLeak)
willDealloc
- 检测self.childViewControllers
- 检测self.presentedViewController
- viewDidLoad的情况下从self.view开始遍历subviews
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}
UIView (MemoryLeak)
willDealloc
- 遍历view的subviews
UIView (MemoryLeak)
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}