APM - iOS 内存泄漏监控 MLeaksFinder代码解析

2,690 阅读6分钟

简介

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;
}

引用

MLeaksFinder Github

MLeaksFinder 新特性

MLeaksFinder:精准 iOS 内存泄漏检测工具