源码解读iOS性能优化iOS 中间件开发

MLeaksFinder 源码学习笔记

2019-01-12  本文已影响30人  RiverSea
MLeaksFinder 源码学习笔记.png

0.前言

项目中集成了 MLeaksFinder 用于平时检测内存泄漏之用,它的基本工作原理也多少了解一些,最近恰好有点空闲时间,决定还是仔细看一下源码实现,毕竟自己查的才比较放心O(∩_∩)O哈哈~,于是就有了本文。

1.用法

MLeaksFinder 的使用非常人性化,直接通过 cocoaPods 导入工程中就行,当检测到泄露的时候会自动在控制台打印出相关堆栈信息。

2.原理

2.1 基本原理

一般情况下,当一个 ViewController 或 NavigationController 被 dismiss 或 pop 的时候,它自己、view、view的 subView 等都应该会很快释放掉。于是,只需要在 dismiss 或 pop 之后检测这些对象是否还存在,就可以判断是否存在内泄。

2.2 MLeaksFinder 的基本实现

为基类 NSObject 添加一个方法 -willDealloc ,它先用一个弱指针指向 self,并在一小段时间 (2秒) 后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是打印堆栈信息 (早期版本是直接中断言,不过那样会打断正常的开发工作)。

当我们认为某个对象应该要被释放了,在释放前调用 -assertNotDealloc ,如果 2 秒后它被释放成功,weakSelf 就指向 nil,-assertNotDealloc 方法就不会执行(向 nil 发送消息,实际什么也不会做),如果它没被释放,-assertNotDealloc 就会执行,从而打印出堆栈信息。

于是,当一个NavigationController 或 UIViewController 被 pop 或 dismiss 时,我们遍历它的所有 view,依次调 -willDealloc,若 2 秒后没被释放,就会打印相关堆栈信息。

3.源码

NSObject+MemoryLeak

先来看看基类 NSObject 的分类 NSObject+MemoryLeak ,这里提供了以下公开方法,详见注释,其中第二个方法只有这个宏 #define MLCheck(TARGET) [self willReleaseObject:(TARGET) relationship:@#TARGET]; 会用到,而这个宏是留给我们扩展功能使用的。

/// 入口方法
- (BOOL)willDealloc;

/// 用于扩展,即 MLCheck(TARGET) 中会用到
- (void)willReleaseObject:(id)object relationship:(NSString *)relationship;

// 用于构造堆栈信息
- (void)willReleaseChild:(id)child;
- (void)willReleaseChildren:(NSArray *)children;

/// 堆栈信息数组,元素是类名
- (NSArray *)viewStack;

/// 添加新类名到白名单
+ (void)addClassNamesToWhitelist:(NSArray *)classNames;

/// 交换方法
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL;

接着查看他的实现文件,首先是 -willDealloc 方法,实现如下,做了三件事:

- (BOOL)willDealloc {

    // 1.检测白名单
    NSString *className = NSStringFromClass([self class]);
    if ([[NSObject classNamesWhitelist] containsObject:className])
        return NO;

    // 2.fix bug
    NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
    if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
        return NO;

    // 3.核心:延迟 2 秒执行 -assertNotDealloc 方法
    __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;
}

从下面的代码可以看出来,-assertNotDealloc 做了2 件事:

- (void)assertNotDealloc {

    // 1.检测父控件体系中是否有没被释放的
    if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
        return;
    }
    [MLeakedObjectProxy addLeakedObject:self];

    // 2.打印堆栈信息
    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]);
}

我们看看 viewStack 的 setter 和 getter,这里用到了运行时机制,即利用关联对象给一个类添加属性信息。viewStack 是一个数组,存放的是类名,从 getter 可以看出来,初次使用时,直接将当前类名作为第一个元素添加进去了。

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

顺便看一下前边用到的 parentPtrs 的 setter 和 getter,从下边的源码可看出来,二者的方法实现类似,只不过后者是一个集合 set,前者是 数组 array。

- (NSSet *)parentPtrs {
    NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
    if (!parentPtrs) {
        parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
    }
    return parentPtrs;
}

- (void)setParentPtrs:(NSSet *)parentPtrs {
    objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN);
}

那么,后续这个 viewStackparentPtrs 又是什么时候构建的呢?这里提供了 2 个供外界调用的构建方法,最终是依赖后一个方法 - willReleaseChildren 实现的。

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

仔细观察上边的 willReleaseChildren: 方法发现,就做了两件事:

在这个类的最后,提供了一个交换方法的方法:

+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
#if _INTERNAL_MLF_ENABLED

#if _INTERNAL_MLF_RC_ENABLED
    // Just find a place to set up FBRetainCycleDetector.
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            [FBAssociationManager hook];
        });
    });
#endif

    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);

    // YES if the method was added successfully, otherwise NO (for example, the class already contains a method implementation with that name).
    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
}

交换方法代码就做介绍了,下边主要讲一下用到的两个宏:_INTERNAL_MLF_ENABLED_INTERNAL_MLF_RC_ENABLED

MLeaksFinder.h

为了弄清楚上边提到的两个宏,我们来看看 MLeaksFinder.h 这个文件,分了两部分:

_INTERNAL_MLF_ENABLED

下边的条件编译语句用于确定 _INTERNAL_MLF_ENABLED 的值,即决定是否需要开启内存泄漏的检测,默认是在 DEBUG 模式下检测,当然,也可以自己修改这个值。

//#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 // DEBUG 环境的话,_INTERNAL_MLF_ENABLED == 1

#endif

_INTERNAL_MLF_RC_ENABLED

下边的条件编译语句用于确定 _INTERNAL_MLF_RC_ENABLED 的值,即决定是否需要开启循环引用的检测,默认是 如果项目中使用了 CocoaPods,则会通过 FBRetainCycleDetector 进行检测。实际使用 CocoaPods 导入 MLeaksFinder 的时候,会将 FBRetainCycleDetector 一并导入,对于后者的工作原理,会单独分一篇介绍的。

//#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 1

#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

#elif COCOAPODS

#define _INTERNAL_MLF_RC_ENABLED COCOAPODS

#endif

MLeakedObjectProxy

现在解决一个遗留问题,就是前边 -willDealloc 方法中用到的 MLeakedObjectProxy 这个类,查看其 .h 文件发现,对外只提供了两个类方法:

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs;
+ (void)addLeakedObject:(id)object;

先看第一个方法:

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
    NSAssert([NSThread isMainThread], @"Must be in main thread.");

    // 1.初始化 leakedObjectPtrs
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        leakedObjectPtrs = [[NSMutableSet alloc] init];
    });

    if (!ptrs.count) {
        return NO;
    }

    // 2.检测 `leakedObjectPtrs` 与 `ptrs` 之间是否有交集
    // 当 leakedObjectPtrs 中 至少有一个对象也出现在 ptrs 中时,返回 YES。
    if ([leakedObjectPtrs intersectsSet:ptrs]) {
        return YES;
    } else {
        return NO;
    }
}

这里,首先初始化了 leakedObjectPtrs(为了保证唯一性,使用了单例),他是用来存储发生内泄的对象地址 (已经转成了数值,即 uintptr_t)。然后通过 -intersectsSet: 检测 leakedObjectPtrsptrs 之间是否有交集,即传入的 ptrs 中是否是泄露的对象。

下面看看第二个重要方法 + addLeakedObject: ,它只要做了这么几件事:

+ (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];

    // 1.给每一个 object 关联一个代理即proxy
    static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
    objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);

    // 2.存储 proxy.objectPtr 到集合 leakedObjectPtrs 里边
    [leakedObjectPtrs addObject:proxy.objectPtr];

    // 3.弹框
#if _INTERNAL_MLF_RC_ENABLED
    [MLeaksMessenger alertWithTitle:@"Memory Leak"
                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]
                           delegate:proxy
              additionalButtonTitle:@"Retain Cycle"];
#else
    [MLeaksMessenger alertWithTitle:@"Memory Leak"
                            message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
#endif
}

当点击弹框中的检测循环引用按钮时,相关的操作都在下面 AlertView 的代理方法里边,即异步地通过 FBRetainCycleDetector 检测循环引用,然后回到主线程,利用弹框提示用户检测结果。

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (!buttonIndex) {
        return;
    }

    id object = self.object;
    if (!object) {
        return;
    }

#if _INTERNAL_MLF_RC_ENABLED
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
        [detector addCandidate:self.object];
        NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20];

        BOOL hasFound = NO;
        for (NSArray *retainCycle in retainCycles) {
            NSInteger index = 0;
            for (FBObjectiveCGraphElement *element in retainCycle) {
                if (element.object == object) {
                    NSArray *shiftedRetainCycle = [self shiftArray:retainCycle toIndex:index];

                    dispatch_async(dispatch_get_main_queue(), ^{
                        [MLeaksMessenger alertWithTitle:@"Retain Cycle"
                                                message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]];
                    });
                    hasFound = YES;
                    break;
                }

                ++index;
            }
            if (hasFound) {
                break;
            }
        }
        if (!hasFound) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [MLeaksMessenger alertWithTitle:@"Retain Cycle"
                                        message:@"Fail to find a retain cycle"];
            });
        }
    });
#endif
  }
}

UIViewController+MemoryLeak

说了这么多,最后,以 UIViewController 为例,查看一下检测内泄的入口,即如何实现调用 -willdeallloc 方法,即如何开始构建内泄的堆栈信息的。

下面是 +load 方法,就是将几个系统方法和自定义方法交换,以便给系统方法增加新的操作。

+ (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:

先取出了 kHasBeenPoppedKey 对应的值,这个值是在右滑返回上个页面并触发 pop 时,设置为 YES 的,说明当前 ViewController 要销毁了,所以在这个时候调用了 -willDealloc 方法。

- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];

    if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
        [self willDealloc];
    }
}

- (void)swizzled_viewWillAppear:

与上边对应,这里是在当前 ViewController 的视图展示出来的时候,将 kHasBeenPoppedKey 关联的值设为 NO,即当前 ViewController 没有通过右滑返回。

- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];

    objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}

- swizzled_dismissViewControllerAnimated:

前边两个方法是针对滑动返回做的处理,这里是针对通过 present 的对象 dismiss 时的操作,即如果当前 ViewController 没有 presentedViewController,就直接调用当前 ViewController 的 -willDealloc 方法检测内泄。

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

最后重写了 -willDealloc 方法,调用了 -willReleaseChildren: 方法,由于构建堆栈信息。

- (BOOL)willDealloc {
    if (![super willDealloc]) {
        return NO;
    }

    // viewController
    [self willReleaseChildren:self.childViewControllers];
    [self willReleaseChild:self.presentedViewController];

    // view
    if (self.isViewLoaded) {
        [self willReleaseChild:self.view];
    }

    return YES;
}

其它 UI 相关类的内泄检测与 ViewController 类似,这里就不啰嗦了。

4.参考

上一篇下一篇

猜你喜欢

热点阅读