简介

MLeaksFinder是微信开源的能够在iOS开发阶段用来检测内存走漏的库,能够主动的在UIView和UIViewController中发现走漏,经过弹出框显示在View-ViewController中的走漏目标。除了发现UIView和UIViewController的走漏目标,开发者也能够扩展到其他类型的目标。

并且能够经过FBRetainCycleDetector找到引证环。

内存分类

  • Leaked memory 不被应用引证的内存,无法再次运用或者开释(能够运用Leaks Instrument东西检测到)
  • Abandoned memory 没有用途的,仍被应用引证的内存
  • Cached memory 仍然被应用引证,能够被再次运用然后得到更好的功能

原理

除了单例及其持有它的强引证,当一个UIViewController被pop或dismiss后,对应的View,View的subViews等目标将很快被开释。

Hook

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

Hook所有页面退出的状况作为检测机遇

  • UIViewController

    • -dismiss
    • -viewDidDisappear
  • UINavigationController

    • -pop
    • -popToRoot
    • -popToViewController

视图层级

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

页面生命周期

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 内存走漏检测东西