MLeaksFinder 源码学习笔记

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]; 会用到,而这个宏是留给我们扩展功能使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// 入口方法
- (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 方法,实现如下,做了三件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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 方法,既不检测内泄。构建基础白名单时,使用了单例,确保只有一个,这个方法是私有的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    + (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;
    }

    另外,用户也可以自行添加额外的类名,方法如下:

    1
    2
    3
    + (void)addClassNamesToWhitelist:(NSArray *)classNames {
    [[self classNamesWhitelist] addObjectsFromArray:classNames];
    }
  • 修复一个 bug

    此处处理一个了bug,即 在button 的点击事件或者UITableview 的点击Cell的事件中调用self.navigationController popViewControllerAnimated:YES 时就报没有释放 ,详见文末的参考。

  • 核心:延迟 2 秒执行 -assertNotDealloc 方法

    用弱引用 weakSelf 指向 self,延迟 2 秒执行 weakSelf 的 -assertNotDealloc 方法,这个方法如果能够执行,则说明当前对象泄露了。

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

1
2
3
4
5
6
7
8
9
10
11
12
- (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]);
}
  • 判断当前对象父控件的层级体系中是否有没被释放的对象,如果有就不往下执行了,否则把自己加进去,并打印堆栈信息。

    因为父对象的 -willDealloc 会先执行,所以如果父对象一定会销毁的话,那么也应该是先销毁,即先从 MLeakedObjectProxy 中移除,加了这个判断之后,就不会出现一个堆栈中出现多个未释放对象的情况。

    这里用到了 2 个 MLeakedObjectProxy 中的方法 +isAnyObjectLeakedAtPtrs:+addLeakedObject:,后边会讲到。

  • 打印 viewStack 这个数组,数组里存放的是从父对象到子对象,一直到当前对象的类名。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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。

1
2
3
4
5
6
7
8
9
10
11
- (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 实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (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: 方法发现,就做了两件事:

  • 拿到当前对象的 viewStack 和 parentPtrs,然后遍历 children,为每一个 child 设置 viewStackparentPtrs ,而且是将自己 (child) 加进去了的。

  • 执行 [child willDealloc]; ,结合前边提到的 willDealloc 知道,这就去检测子类了。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+ (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 模式下检测,当然,也可以自己修改这个值。

1
2
3
4
5
6
7
8
9
10
11
//#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 一并导入,对于后者的工作原理,会单独分一篇介绍的。

1
2
3
4
5
6
7
8
9
10
11
//#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 文件发现,对外只提供了两个类方法:

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

先看第一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ (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: ,它只要做了这么几件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+ (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
}
  • 给传入的泄漏对象 object 关联一个代理即 proxy
  • 存储 proxy.objectPtr(实际是对象地址)到集合 leakedObjectPtrs 里边
  • 弹框 AlertView:若 _INTERNAL_MLF_RC_ENABLED == 1,则弹框会增加检测循环引用的选项;若 _INTERNAL_MLF_RC_ENABLED == 0,则仅展示堆栈信息。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (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 方法,就是将几个系统方法和自定义方法交换,以便给系统方法增加新的操作。

1
2
3
4
5
6
7
8
+ (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 方法。

1
2
3
4
5
6
7
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];

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

- (void)swizzled_viewWillAppear:

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

1
2
3
4
5
- (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 方法检测内泄。

1
2
3
4
5
6
7
8
9
10
11
12
- (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: 方法,由于构建堆栈信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (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.参考

0%