OC三方框架05:MLeaksFinder内存泄露探索

张建 lol

前言

  • 腾讯开源了业界首创 iOS自动内存泄露检测工具 MLeaksFinderMLeaksFinderiOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。

  • 平常我们都会用 InstrumentLeaks / Allocations 或其他一些开源库进行内存泄露的排查,但它们都存在各种问题和不便,我们逐个来看这些工具的使用和存在的问题。

  • 无需修改任何业务逻辑代码,而且只在 debug 下开启,完全不影响你的 release 包。

简介

MLeaksFinderiOS 平台的自动内存泄漏检测工具,引进 MLeaksFinder 后,就可以在日常的开发,调试业务逻辑的过程中自动地发现并警告内存泄漏。开发者无需打开 instrument 等工具,也无需为了找内存泄漏而去跑额外的流程。并且,由于开发者是在修改代码之后一跑业务逻辑就能发现内存泄漏的,这使得开发者能很快地意识到是哪里的代码写得问题。这种及时的内存泄漏的发现在很大的程度上降低了修复内存泄漏的成本。

MLeaksFinder介绍

MLeaksFinder 提供了内存泄露检测更好的解决方案。只需要引入 MLeaksFinder,就可以自动在 App 运行过程检测到内存泄露的对象并立即提醒,无需打开额外的工具,也无需为了检测内存泄露而一个个场景去重复地操作。MLeaksFinder 目前能自动检测 UIViewControllerUIView 对象的内存泄露,而且也可以扩展以检测其它类型的对象。

当发生内存泄露时,MLeaksFinder 会中断言,并准确的告诉你哪个对象泄露了。这里设计为中断言而不是打日志让程序继续跑,是因为很多人不会去看日志,断言则能强制开发者注意到并去修改,而不是犯拖延症。

中断言时,控制台会有如下提示,View-ViewController stack 从上往下看,该 stack 告诉你,MyTableViewControllerUITableViewsubview UITableViewWrapperView 的subview MyTableViewCell 没被释放。而且,这里我们可以肯定的是 MyTableViewControllerUITableViewUITableViewWrapperView 这三个已经成功释放了。

1
2
3
4
5
6
7
8
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Possibly Memory Leak.
In case that MyTableViewCell should not be dealloced, override -willDealloc in MyTableViewCell by returning NO.
View-ViewController stack: (
MyTableViewController,
UITableView,
UITableViewWrapperView,
MyTableViewCell
)'

MLeaksFinder 的优点:

  • 使用简单,不侵入业务逻辑代码,不用打开 Instrument
  • 不需要额外的操作,你只需开发你的业务逻辑,在你运行调试时就能帮你检测
  • 内存泄露发现及时,更改完代码后一运行即能发现(这点很重要,你马上就能意识到哪里写错了)
  • 精准,能准确地告诉你哪个对象没被释放

MLeaksFinder流程

  1. 先通过 MLeaksFinder 找到内存泄漏的对象。

做法:MLeaksFinder 一开始从 UIViewController 入手。我们知道,当一个 UIViewControllerpopdismiss 后,该 UIViewController 包括它的 view,viewsubviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewControllerpopdismiss 一小段时间后,看看该 UIViewController

  1. 然后再过 FBRetainCycleDetector 检测该对象有没有 循环引用

做法:当传入内存中的任意一个 OC 对象,FBRetainCycleDetector 会递归遍历该对象的所有强引用的对象,以检测以该对象为根结点的强引用树有没有循环引用。

MLeaksFinder原理

原理:

为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 assertNotDealloc,而 assertNotDealloc 主要作用是直接中断言。

当一个 UIViewControllerpopdismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 willDealloc,若3秒后没被释放,就会中断言。

1
2
3
4
5
6
7
8
9
10
- (BOOL)willDealloc {
__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf assertNotDealloc];
});
return YES;
}
- (void)assertNotDealloc {
NSAssert(NO, @“”);
}

在这里,有几个问题需要解决:

  • 不入侵开发代码

这里使用了 AOP 技术,hookUIViewControllerUINavigationControllerpopdismiss 方法。

  • 遍历相关对象

在实际项目中,我们发现有时候一个 UIViewController 被释放了,但它的 view 没被释放,或者一个 UIView 被释放了,但它的某个 subview 没被释放。这种内存泄露的情况很常见,因此,我们有必要遍历基于 UIViewController 的整棵 View-ViewController 树。我们通过 UIViewControllerpresentedViewControllerview 属性,UIViewsubviews 属性等递归遍历。对于某些 ViewController,如 UINavigationControllerUISplitViewController 等,我们还需要遍历 viewControllers 属性。

  • 构建堆栈信息

需要构建 View-ViewController stack 信息以告诉开发者是哪个对象没被释放。在递归遍历 View-ViewController 树时,子节点的 stack 信息由父节点的 stack 信息加上子结点信息即可。

  • 例外机制

对于有些 ViewController,在被 popdismiss 后,不会被释放(比如单例),因此需要提供机制让开发者指定哪个对象不会被释放,这里可以通过重载上面的 -willDealloc 方法,直接 return NO 即可。

  • 特殊情况

对于某些特殊情况,释放的时机不大一样(比如系统手势返回时,在划到一半时 hold 住,虽然已被 pop,但这时还不会被释放,ViewController 要等到完全 disappear 后才释放),需要做特殊处理,具体的特殊处理视具体情况而定。

  • 系统View

某些系统的私有 View,不会被释放(可能是系统 bug 或者是系统出于某些原因故意这样做的,这里就不去深究了),因此需要建立白名单

  • 手动扩展

MLeaksFinder 目前只检测 ViewControllerView 对象。为此,MLeaksFinder 提供了一个手动扩展的机制,你可以从 UIViewControllerUIView 出发,去检测其它类型的对象的内存泄露。如下所示,我们可以检测 UIViewController 底下的 View Model

1
2
3
4
5
6
7
8
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
MLCheck(self.viewModel);
return YES;
}

这里的原理跟上面的是一样的,宏 MLCheck() 做的事就是为传进来的对象建立 View-ViewController stack 信息,并对传进来的对象调用 -willDealloc 方法。

MLeaksFinder源码

  1. MLeaksFinder.h

MLeaksFinder.h 定义了 MLeaksFinder 中使用的宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifdef MEMORY_LEAKS_FINDER_ENABLED

//_INTERNAL_MLF_ENABLED 宏用来控制 MLLeaksFinder库
//什么时候开启检测,可以自定义这个时机,默认则是在DEBUG模式下会启动,RELEASE模式下不启动
//它是通过预编译来实现的
#define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED

#else

#define _INTERNAL_MLF_ENABLED DEBUG

#endif

//_INTERNAL_MLF_RC_ENABLED 宏用来控制 是否开启循环引用的检测
#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED

//COCOAPODS 因为MLLeaksFinder引用了第三库用来检查循环引用,所以必须是当前项目中使用了COCOAPODS,才能使用这个功能。
#elif COCOAPODS

#define _INTERNAL_MLF_RC_ENABLED COCOAPODS

#endif
  1. MLeakedObjectProxy

MLeakedObjectProxy用来对泄漏对象检查循环引用

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//用来检查当前泄漏对象是否已经添加到泄漏对象集合中,如果是,就不再添加也不再提示开发者
+ (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;
}
//NSSet求交集
if ([leakedObjectPtrs intersectsSet:ptrs]) {
return YES;
} else {
return NO;
}
}
+ (void)addLeakedObject:(id)object {

NSAssert([NSThread isMainThread], @"Must be in main thread.");


//创建用于检查循环引用的objectProxy对象
MLeakedObjectProxy *proxy = [[MLeakedObjectProxy 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];



#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

}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)
buttonIndex {

#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;

//retainCycles中是找到的所有循环引用的链
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

}
  1. NSObject+MemoryLeak

NSObject+MemoryLeak主要功能存储对象的父子节点的树形结构,method swizzle逻辑 ,白名单以及判断对象是否发生内存泄漏

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
- (BOOL)willDealloc {

NSString *className = NSStringFromClass([self class]);
//通过白名单可以配置哪些对象不纳入检查,例如一些单例
if ([[NSObject classNamesInWhiteList] 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为nil 调用该方法什么也不发生
[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]);

}

//主要通过递归来记录每个节点在树形结构中的位置,以及父子节点的指针
- (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];
}
}
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {


//通过预编译控制是否hook方法
#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);

BOOL didAddMethod =
//class_addMethod主要是用来给某个类添加一个方法,originalSEL相当于是方法名称,method_getIm
//plementtation是方法实现, 它返回一个BOOL类型的值
//在当前class中没有叫originalSEL的方法(
//具体不是看interface里没有没有声明,而是看implementaion文件里有没有方法实现),
// 并且有swizzledMethod方法的实现
//这个时候该函数会返回true,其他情况均返回false
class_addMethod(class,

originalSEL,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
//didAddMethod为true 说明swizzledMethod之前不存在,通过class_addMethod函数添加了一个名字叫origninalSEL,实现是swizzledMoethod函数。
class_replaceMethod(class,

swizzledSEL,

method_getImplementation(originalMethod),

method_getTypeEncoding(originalMethod));

} else {
//didAddMethod为false 说明swizzledMethod方法已经存在,直接交换二者实现
method_exchangeImplementations(originalMethod, swizzledMethod);

}

#endif

}
  1. UINavigationController + MemoryLeak

UINavigationController + MemoryLeak 主要是通过UINavigationController的方法去检测子UIViewController页面的生命周期,UIViewController的生命周期由UINavigationController的方法和UIViewController自身的一些方法共同决定的。

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91

//现在在具体的类型中添加方法hook,加载load中并且调用dspatch_once来保证只初始化一次,load是必然会调用的,并且category的load方法调用和类自身的load方法调用是分开的,互不干扰。
+ (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:)];

});
}

- (void)swizzled_pushViewController:(UIViewController *)viewController
animated:(BOOL)animated {

if (self.splitViewController) {
//这里主要是考虑到app中有使用splitViewController的情况的时候,下一个根页面push之后,
//之前被pop的根页面才会回收
id detailViewController = objc_getAssociatedObject(self,
kPoppedDetailVCKey);

if ([detailViewController isKindOfClass:[UIViewController class]]) {
//回收之前被pop的根页面
[detailViewController willDealloc];

objc_setAssociatedObject(self, kPoppedDetailVCKey, nil,
OBJC_ASSOCIATION_RETAIN);

}
}
[self swizzled_pushViewController:viewController animated:animated];

}
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {

UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];

if (!poppedViewController) {
return nil;
}

//当前页面是spliteViewController根页面
if (self.splitViewController &&

self.splitViewController.viewControllers.firstObject == self &&

self.splitViewController == poppedViewController.splitViewController) {

objc_setAssociatedObject(self, kPoppedDetailVCKey, poppedViewController
, OBJC_ASSOCIATION_RETAIN);

return poppedViewController;
}


// 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;

}

- (NSArray<UIViewController *> *)swizzled_popToViewController:(
UIViewController *)viewController animated:(BOOL)animated {

NSArray<UIViewController *> *poppedViewControllers = [self
swizzled_popToViewController:viewController animated:animated];

//一次性pop多个页面的时候,这些页面的viewDidDisappear估计都没有被调用,直接回收了
for (UIViewController *viewController in poppedViewControllers) {

[viewController willDealloc];

}

return poppedViewControllers;

}

  1. UIViewController + MemoryLeak
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
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
//仅仅当是pop引起viewDidDisappear的时候才释放(当被挡住之后也会调用ViewDidDisappear)
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {

[self willDealloc];

}

}

- (void)swizzled_dismissViewControllerAnimated:(BOOL)flag
completion:(void (^)(void))completion {

[self swizzled_dismissViewControllerAnimated:flag completion:completion];

//dismiss掉presentedViewController,释放它 (但是什么时候当前viewController被释放呢)
UIViewController *dismissedViewController = self.presentedViewController;

if (!dismissedViewController && self.presentingViewController) {
//释放自己
dismissedViewController = self;
}

if (!dismissedViewController) return;
//以present出来的viewcontroller,不通过DidDisappear去判断是否释放了
[dismissedViewController willDealloc];

}

实际项目中使用

  • cocoapods集成,只需要在 podfile 文件中加入相关库即可。

pod 'MLeaksFinder'

  • Post title:OC三方框架05:MLeaksFinder内存泄露探索
  • Post author:张建
  • Create time:2023-02-08 16:34:17
  • Post link:https://redefine.ohevan.com/2023/02/08/OC性能优化/性能优化01.3:MLeaksFinder内存泄露检测/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.