内存泄漏检测好手MLeaksFinder

2018-10-31  本文已影响0人  petyou

原理

一般而言, 当一个 UIViewController 被完全 pop 或者 dismiss 掉后, UIViewController 本身、 UIViewControllerviewUIViewControllerviewControllersviewsubViews 以及 subViewssubViews 等等都应该在短时间内被释放掉. 基于此我们构思到:

  1. 找到一个时机去检测这些对象是否还存在
  2. 如何检测这些对象是否还存在
  3. 发现该被释放的对象还存在时如何提示开发者
  4. 规避那些被设计为单例的全局对象
  5. 检测完当前对象后, 继续检测它的子view或者子控制器甚至其它属性

1. 找到一个时机去检测这些对象是否还存在

这个时机其实也就是一个 UIViewController 被完全 pop 或者 dismiss 时, 于是我们 hookUINavigationController 所有的 pop 系列方法和 UINavigationController 的一个 push 方法 (Detail VC in UISplitViewController is not dealloced until another detail VC is shown) 还有 UIViewControllerdimiss 方法, 且由于系统右滑手势返回时,可能在划到一半时hold住,虽然已被 pop,但这时还不会被释放,ViewController 要等到完全 disappear 后才释放,所以再 hookViewControllerdidDisappearviewWillAppear 这两个方法. 这时, 已经找到了检测时机.

@implementation UINavigationController (MemoryLeak)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)];
        [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:)];
    });
}

@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:)];
    });
}

2. 如何检测这些对象是否还存在

那么如何在这些 hook 掉的方法中, 检测对象是否还存在呢? 具体的方法是,为基类 NSObject 添加一个 -willDealloc 方法,并在 上述 hook 掉的方法中调用 -willDealloc 这个方法. 该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,那么如果3秒后对象成功释放,weakSelf 就会指向 nil,也就不会调用到 -assertNotDealloc 方法, 就不会有任何影响, 但如果它没被释放,-assertNotDealloc 就会被调用, 那么我们就成功将内存泄漏给找到了.

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

3. 发现该被释放的对象还存在时如何提示开发者

MLeaksFiner 0.1 版本中, 当 MLeaksFinder 发现内存泄漏时, 会直接中 assert 并打出内存泄漏的信息. assert 能迫使开发者及时地去修复内存泄漏, 并且, 如果只是打日志, 内存泄漏的日志很可能会被淹没在众多的日志中.

然而, assert 也有不好的一面. 当开发者在调试业务逻辑的过程中, 如果由于内存泄漏中 assert 而使得整个程序挂掉了, 那么开发者的思维会因此被打断, 并不得不在修复完内存泄漏之后, 从头开始调试业务逻辑. 有时候开发者更希望的是连贯地调完整个业务逻辑之后, 再回过头来修复内存泄漏.

因此, MLeaksFinder 0.2 版本中 把 assert 改成了 alert. 当发现内存泄漏之后, 开发者可以把 alert 框关掉, 并继续调试业务逻辑.

4. 规避那些被设计为单例的全局对象

可以看到在 -willDealloc 中如果当前类被添加到白名单中, 则不会去检测内存泄露. MLeaksFiner 默认添加了一些白名单, 且提供了接口给开发者添加, 在 NSObject+MemoryLeak 中有 + (void)addClassNamesToWhitelist:(NSArray *)classNames 用于添加白名单

- (BOOL)willDealloc {    
    // 这里有白名单判断    
    NSString *className = NSStringFromClass([self class]);
    if ([[NSObject classNamesWhitelist] containsObject:className])
        return NO;
    // 如果该对象被 UIApplication 对象引用这, 则也不会去检测泄露
    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;
}

+ (NSMutableSet *)classNamesWhitelist {
    static NSMutableSet *whitelist = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        whitelist = [NSMutableSet setWithObjects:
                     @"UIFieldEditor", // UIAlertControllerTextField
                     @"UINavigationBar",
                     @"_UIAlertControllerActionView",
                     @"_UIVisualEffectBackdropView",
                     nil];
        
        // System's bug since iOS 10 and not fixed yet up to this ci.
        NSString *systemVersion = [UIDevice currentDevice].systemVersion;
        if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
            [whitelist addObject:@"UISwitch"];
        }
    });
    return whitelist;
}

5. 检测完当前对象后, 继续检测它的子view或者子控制器甚至其它属性

当完成对象自身的检测后, 还需要对它的附属进行检测. 比如 UIViewController 作为容器控制器时, 当它应该被销毁时, 则它的一些子控制器或者控制器的 view 等等也应该被销毁, 故都需要检测.

@implementation UIViewController (MemoryLeak)

- (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, 那么它的 subViews 也应该被检测

@implementation UIView (MemoryLeak)

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    
    [self willReleaseChildren:self.subviews];
    
    return YES;
}

同理如果是 UINavigationController 或者 UIPageViewController 或者 UISplitViewController 或者 UITabBarController 等容器控制器, 那么它的 viewControllers 也应该被检测

@implementation UINavigationController (MemoryLeak)

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    
    [self willReleaseChildren:self.viewControllers];
    
    return YES;
}

再提一点, 对于使用 Method Swizzling
交换两个方法, 较为谨慎的写法

+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {

    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) {
        // 添加成功表明当前类添加之前并没有originalSEL这个方法(意味着你尝试替换一个并不存在的方法). 
        // 那么执行到此处.已经帮你给这个类添加了originalSEL这个方法.且实现是swizzledMethod.
        
        // 此时再将当前类的swizzledSEL的实现替换为 method_getImplementation(originalMethod)
        // 此时获取的实现已经是swizzledMethod了.
        
        // 后续无论你调用 originalSEL 还是 swizzledSEL 都是同样的实现 swizzledMethod
        class_replaceMethod(class,
                            swizzledSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
#endif
}
上一篇下一篇

猜你喜欢

热点阅读