Skip to content

Latest commit

 

History

History
311 lines (246 loc) · 10 KB

检测内存泄漏.md

File metadata and controls

311 lines (246 loc) · 10 KB

检测内存泄漏

1 检测内存泄漏

1.1 检测内存泄漏的几种方式

检测内存泄漏一般有以下几种方式。

  • 通过dealloc判断
  • Instruments -> leak
  • Debug Memory Graph
  • MLeaksFinder

通过打印dealloc的方式,需要每个页面都仔细检查,并且许多泄漏是在很多处理的前提才会出现的,所以该方式需要大量时间去排查。

Instruments -> leak的方式适合大范围,查找全部的泄漏点,数据量大。

Debug Memory Graph是会通过图形化的方式显示引用的对象。

MLeaksFinder是通过hook来实现无代码入侵的自动检测对象是否正常释放。

1.2 MLeaksFinder原理及实现

MLeaksFinder最主要的实现是通过对象将要释放时,延迟执行一个block。block内部通过self去调用一个方法。如果还能调用,说明该对象还未释放,并且记录视图的栈通过alert显示出来提醒开发者。

MLeaksFinder通过实现了几个重要类的分类(UINavigationController、UITabBarController、UIViewController、UIView、NSObject),替换部视图的生命周期方法,以便检测和处理。

通过几个主要的类看内部实现

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:)];
    });
}
// 离开页面
- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];
    // 当nav pop出时才去执行willDealloc才有意义
    // 配合UINavigationController+MemoryLeak使用
    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);
}

// dismiss时的释放检测
- (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];
}

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    // 递归排查所有的子控制器
    [self willReleaseChildren:self.childViewControllers];
    // 检查present的控制器
    [self willReleaseChild:self.presentedViewController];
    
    // 是否正在显示 递归view的全部子view
    if (self.isViewLoaded) {
        [self willReleaseChild:self.view];
    }
    
    return YES;
}

UIView+MemoryLeak

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }
    // 递归检查所有子view
    [self willReleaseChildren:self.subviews];
    
    return YES;
}

NSObject+MemoryLeak

- (BOOL)willDealloc {
    NSString *className = NSStringFromClass([self class]);
    // 是否在白名单内
    if ([[NSObject classNamesWhitelist] containsObject:className])
        return NO;
    
    // 检查最后一个发送action的是不是当前对象 是的话就不处理,因为willDealloc会先调用
    NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
    if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        return NO;
    
    // 检测内存泄漏的核心实现
    // 延迟2秒后执行self的方法 如果不为nil则会执行 说明未释放
    __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 {
    // 判断当前泄漏的Set是不是已经在泄漏名单里面了
    if ([LeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    // 加入名单内
    [LeakedObjectProxy 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]);
}
// 检查泄漏
- (void)willReleaseChild:(id)child {
    if (!child) {
        return;
    }
    
    [self willReleaseChildren:@[ child ]];
}

// 检查所有数组内的对象的泄漏
- (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 ];
}

// 设置view的路径栈
- (void)setViewStack:(NSArray *)viewStack {
    objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN);
}

// 获取路径上的所有对象
- (NSSet *)parentPtrs {
    NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
    if (!parentPtrs) {
        parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
    }
    return parentPtrs;
}

// 设置路径上的对象 用set保存就可以 用于判断
- (void)setParentPtrs:(NSSet *)parentPtrs {
    objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}

// 白名单
+ (NSMutableSet *)classNamesWhitelist {
    static NSMutableSet *whitelist = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        whitelist = [NSMutableSet setWithObjects:
                     @"UIFieldEditor",
                     @"UINavigationBar",
                     @"_UIAlertControllerActionView",
                     @"_UIVisualEffectBackdropView",
                     nil];
    });
    return whitelist;
}
// 动态添加至白名单
+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
    [[self classNamesWhitelist] addObjectsFromArray:classNames];
}

// 交换方法
+ (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) {
        class_replaceMethod(class,
                            swizzledSEL,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

LeakedObjectProxy

// 通过交集判断是否已经存在
+ (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.");
    
    LeakedObjectProxy *proxy = [[LeakedObjectProxy alloc] init];
    proxy.object = object;
    proxy.objectPtr = @((uintptr_t)object);
    proxy.viewStack = [object viewStack];
    static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
    
    [leakedObjectPtrs addObject:proxy.objectPtr];
    
    // 弹窗提醒
    [self alertWithTitle:@"内存泄漏" message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
    
}

// 弹窗显示
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message {
    dispatch_block_t block = ^() {
        UIAlertController *temp = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
        [temp addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]];
        [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:temp animated:YES completion:nil];
        alertController = temp;
    };
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"程序捕捉到内存泄漏警告: title=%@, message=%@", title, message);
        if (alertController) {
            [alertController dismissViewControllerAnimated:NO completion:block];
        }
        else {
            block();
        }
    });
}

简单的一个控制器在dismiss后,检查路径如下

  1. swizzled_dismissViewControllerAnimated中执行willDealloc
  2. 递归当前对象的所有子控制器、persent控制器
  3. 递归检查self.view
  4. UIView的willDealloc会检查所有子View
  5. NSObject中willDealloc 判断检查的对象是否在白名单中
  6. 添加延时2秒的方法
  7. 2秒后,如果存在泄漏的对象将其添加到泄漏名单中
  8. 将泄漏对象的视图栈中所有的类名显示在弹窗