MLeaksFinder源码学习

927 阅读6分钟

为了方便理解, 跟源码有出入.

核心原理

- (BOOL)willDealloc {
    __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;
}

在你认为某对象将要dealloc的地方,调用一下该对象willDealloc, 如果2秒钟后该对象依旧存在,说明该对象可能释放不及时,存在内存泄漏

所以重要的是什么时候应该调用willDealloc呢?

具体操作

MLeaksFinder针对viewvc,帮我们在合适的时机自动调用了willDealloc

针对vc

vc什么时候应该调用willdealloc?

  1. vc执行dismissViewControllerAnimated:completion:

背景知识:

当我们vcA执行presentViewController:animated:completion:弹出vcB

此时,

vcB.presentingViewController == vcA
vcA.presentedViewController == vcB

当我们的vc执行dismissViewControllerAnimated:completion:

如果该vcpresentedViewController, 那么将自己的presentedViewController控制器dismiss掉

如果该vc没有presentedViewController, 那么就相当于让自己的presentingViewController执行dismissViewControllerAnimated:completion:

也就是说:

某vc执行dismissViewControllerAnimated:completion:

如果该vcpresentedViewController, 那么该vcpresentedViewController需要调用willdealloc

如果该vc没有presentedViewController, 那么该vc需要调用willdealloc

// UIViewController (MemoryLeak)
- (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];
}
  1. vcnavigationController执行了popToRootViewControllerAnimated:

image.png

当所属的navigationController调用popToRootViewControllerAnimated:时,返回值是所有需要出栈的vc数组, vc出栈正常应该销毁,所以调用willdealloc

// UINavigationController (MemoryLeak)
- (NSArray<UIViewController *> *)swizzled_popToRootViewControllerAnimated:(BOOL)animated {
    NSArray<UIViewController *> *poppedViewControllers = [self swizzled_popToRootViewControllerAnimated:animated];
    
    for (UIViewController *viewController in poppedViewControllers) {
        [viewController willDealloc];
    }
    
    return poppedViewControllers;
}
  1. vcnavigationController执行了popToViewController:animated:

同2

// UINavigationController (MemoryLeak)
- (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;
}
  1. vcnavigationController执行了popViewControllerAnimated: 这个稍微有点不同, 这个是因为有侧滑手势触发时, vc的navigationController 就会触发popViewControllerAnimated:, 但是侧滑手势触发后并不是一定会把该vc出栈, 所以该vc不能在popViewControllerAnimated:时调用willDealloc

MLeaksFinder是这么做的,在pop时, 设置一个标志位来标志是否被pop过, 在viewDidDisAppear判断是否被pop过, 如果被该vcpop过,并且调用了viewDidDisAppear,说明这个vc是真实出栈了, 当然下一次重新appear的时候, 需要将标志位恢复

// UIViewController (MemoryLeak)
- (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);
}

// UINavigationController (MemoryLeak)
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
    UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
    
    if (!poppedViewController) {
        return nil;
    }
    
    // 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;
}

vcwilldealloc

vc调用willdealloc

  1. childViewControllers中的所有子vcpresentedViewController都需要调用willdealloc, 如果该vcviewControllers属性, 例如UINavigationController, UIPageViewController, UITabBarController等, 其viewControllers中的vc也需要调用willdealloc
  2. view需要调用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;
}

针对view

  1. 每一个vcwillDealloc时需要将自己的view调用willdealloc
  2. 每一个的viewwillDealloc时需要将自己的subviews调用willdealloc
// UIView (MemoryLeak)
- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    
    [self willReleaseChildren:self.subviews];
    
    return YES;
}

视图栈(定位页面)

视图栈的作用是为更准确的知道发生内存泄露的vcview所处的位置

对象A触发了对象BwillDealloc, 那么

应令对象B视图栈 = 对象A的视图栈 + 对象B的类

这样对象B就能顺着视图栈找到最顶层的调用willDeallocvc来准确判断位置

// NSObject (MemoryLeak)
- (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];
    }
}

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

地址栈(防止无用上报和重报)

举个例子:

对象A已经被发现有内存泄漏

而由对象A触发了对象BwillDealloc, 这时候对象B的内存泄漏需要提示吗?

这种情况是不需要的, 对象A发生内存泄漏后, 由它触发willDealloc的对象大概率也会因为它的内存泄漏而导致内存泄漏

所以, 我们当发现对象A内存泄漏后, 另一个内存泄漏的对象需要判断一下其地址栈上是否存在对象A, 如果存在, 则不需要报告

地址栈, 其生成逻辑跟视图栈是一样的, 视图栈上存的是类名, 地址栈中存的是对象地址

// NSObject (MemoryLeak)
- (void)assertNotDealloc {
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    [MLeakedObjectProxy addLeakedObject:self];
}


// MLeakedObjectProxy
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        leakedObjectPtrs = [[NSMutableSet alloc] init];
    });
    
    if (!ptrs.count) {
        return NO;
    }
    // 代表地址栈中存在内存泄漏的对象
    if ([leakedObjectPtrs intersectsSet:ptrs]) {
        return YES;
    } else {
        return NO;
    }
}

+ (void)addLeakedObject:(id)object {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");
    [leakedObjectPtrs addObject:@((uintptr_t)object)];
    
    // 可在此处报告内存泄漏
  }
    

其他小技巧

  1. 支持某些类加入白名单, 代表这些类下的对象不需要内存监测
// NSObject (MemoryLeak)
+ (void)addClassNamesToWhitelist:(NSArray *)classNames;
  1. 可以自动手动调用willDealloc, 解决类似这样的情况, 一个单例对象, 但是其某些属性对象需要在合适的时机释放, 这时候可以让这个单例对象手动调用willReleaseObject:relationship:来触发这些属性对象的willDealloc, 这个方法有个宏MLCheck
// NSObject (MemoryLeak)
- (void)willReleaseObject:(id)object relationship:(NSString *)relationship {
    if ([relationship hasPrefix:@"self"]) {
        relationship = [relationship stringByReplacingCharactersInRange:NSMakeRange(0, 4) withString:@""];
    }
    NSString *className = NSStringFromClass([object class]);
    className = [NSString stringWithFormat:@"%@(%@), ", relationship, className];
    
    [object setViewStack:[[self viewStack] arrayByAddingObject:className]];
    [object setParentPtrs:[[self parentPtrs] setByAddingObject:@((uintptr_t)object)]];
    [object willDealloc];
}

#define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET];
  1. 当然某些情况下,可能某些对象在3秒钟后才释放, 并不存在内存泄漏, 但是MLeaksFinder在2秒钟后就会报告内存泄漏, 这时候可能存在误报, 针对这种情况, MLeaksFinder在对象正式释放后, 也会有dealloced提示.

3.1 在地址栈中,MLeakedObjectProxyaddLeakedObject的时候,新建了一个MLeakedObjectProxy对象跟泄漏的object用强引用策略绑定起来.这样只有object释放的时候,MLeakedObjectProxy对象才会释放

3.2 这个MLeakedObjectProxy对象调用dealloc的时候, 代表object已经释放了, 这时候需要提示可能误报了, 这个对象是能正常释放的

// MLeakedObjectProxy
+ (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;
    // 绑定, 强引用, 只有`object`释放的时候, proxy才会释放
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
    
    [leakedObjectPtrs addObject:proxy.objectPtr];
}

- (void)dealloc {
    NSNumber *objectPtr = _objectPtr;
    NSArray *viewStack = _viewStack;
    dispatch_async(dispatch_get_main_queue(), ^{
        [leakedObjectPtrs removeObject:objectPtr];
        // 可在此处报告有误报情况, 可以把视图栈带上方便定位位置  
    });
}

小优化

  1. 针对同一个类下, 部分对象可能不需要监测内存泄漏的情况支持的不好, 可以通过加个关联属性解决
  2. NavigationControllersetViewControlls:方法没有做处理