MLeaksFinder 源码学习笔记
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
方法,既不检测内泄。构建基础白名单时,使用了单例,确保只有一个,这个方法是私有的。+ (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; }
另外,用户也可以自行添加额外的类名,方法如下:
+ (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 件事:
- (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 可以看出来,初次使用时,直接将当前类名作为第一个元素添加进去了。
- (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);
}
那么,后续这个 viewStack
和 parentPtrs
又是什么时候构建的呢?这里提供了 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:
方法发现,就做了两件事:
-
拿到当前对象的 viewStack 和 parentPtrs,然后遍历 children,为每一个 child 设置
viewStack
和parentPtrs
,而且是将自己 (child) 加进去了的。 -
执行
[child willDealloc];
,结合前边提到的willDealloc
知道,这就去检测子类了。
在这个类的最后,提供了一个交换方法的方法:
+ (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:
检测 leakedObjectPtrs
与 ptrs
之间是否有交集,即传入的 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
}
- 给传入的泄漏对象 object 关联一个代理即 proxy
- 存储 proxy.objectPtr(实际是对象地址)到集合 leakedObjectPtrs 里边
- 弹框 AlertView:若 _INTERNAL_MLF_RC_ENABLED == 1,则弹框会增加检测循环引用的选项;若 _INTERNAL_MLF_RC_ENABLED == 0,则仅展示堆栈信息。
当点击弹框中的检测循环引用按钮时,相关的操作都在下面 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 类似,这里就不啰嗦了。