MLeaksFinder是WeRead团队开源的一款检测iOS内存泄漏的框架,其使用只需将文件加入项目中,如果有内存泄漏,数秒后将弹出AlertView来提示,代码无侵入性,但是只能寻找到泄漏的UIView和UIViewController,如果需要检测循环引用,则需要引入FBRetainCycleDetector。本文分析的是MLeaksFinder0.2的源码
实现原理
内存泄漏检测
内存泄漏检测主要的代码在NSObject的分类中,它的willDealloc方法如下:
- (BOOL)willDealloc {
//如果该对象的类在白名单内,则不检测
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;
//如果该对象在sendAction,则不检测
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 assertNotDealloc];
});
return YES;
}
后面会讲到willDealloc的调用时机为ViewController消失的时候,但是某些情况下控制器是被缓存起来或者作为单例常驻在内存中,这个时候,需要开发者手动将它们加入白名单中(也可以重写willDealloc方法返回NO)。
如果该对象为上一次发送action的对象,也不会进行内存检测,MemoryLeak会在UIApplication的分类中记录上一次发送action的地址,这里的逻辑应该是有问题的,会导致有些case检测不出来,这个也放到后面讲。
然后再来看assertNotDealloc的实现:
- (void)assertNotDealloc {
//这个方法判断leakedObjectPtrs跟对象的父节点集合有交集,则说明父节点已经弹过,无需再处理
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
//在这个方法里面弹AlertView
[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]);
}
//这个方法在MLeakedObjectProxy里面
+ (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];
static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);
//将该对象的地址添加到leakedObjectPtrs中
[leakedObjectPtrs addObject:proxy.objectPtr];
//如果接入了FBRetainCycleDetector,则弹出的alertView具有显示循环引用的功能,否则只会显示视图栈
#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的帮助。
如果泄漏的对象在某个时间点重新释放了,也会弹alertView提示,具体实现如下:
//这是MLeakedObjectProxy的析构函数
//MLeakedObjectProxy是检测到泄漏才产生的,作为泄漏对象的属性存在着
//如果泄漏对象被释放,对应的MLeakedObjectProxy也会被释放,就会调用该析构函数
- (void)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(), ^{
[leakedObjectPtrs removeObject:objectPtr];
[MLeaksMessenger alertWithTitle:@"Object Deallocated"
message:[NSString stringWithFormat:@"%@", viewStack]];
});
}
willDealloc执行时机
目前,MLeaksFinder只能检测ViewController相关的泄漏,主要的核心代码在ViewController里面。
MLeaksFinder判断满足以下3种情况之一的时候,认为ViewController应该被释放:
作为模态弹出的ViewController消失
实现这个比较简单,只需hook UIViewController的dismissViewControllerAnimated:completion:方法即可
- (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];
}
这里区分了dismissViewControllerAnimated:completion:这个方法是presentedViewController还是presentingViewController调用的,详情可以参考dismissViewControllerAnimated:completion:你用对了吗?
ViewControler在NavigationController中被pop出来
UINavigationController有3中方法可以使得ViewController pop出来,分别是:
- popViewControllerAnimated:
- popToViewController:animated:
- popToRootViewControllerAnimated:
后两种方法都返回pop出来的ViewController数组,只需hook这两个方法,遍历该数组里的ViewController依次调用WillDealloc即可。
对弈第一种方法,因为用户左滑的时候同样会触发,如果用户左滑时间超过2秒(滑到一半停下),这时ViewController不会被释放掉,因此需要做特殊处理,下面来看其实现代码:
- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];
if (!poppedViewController) {
return nil;
}
//为了方便起见,这里去掉SplitViewController相关的代码
// 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;
}
当ViewController被出栈的时候,给它关联了一个kHasBeenPoppedKey的属性并置为YES,然后,就在ViewController消失的时机调用WillDealloc,当然,如果滑到一半又滑回来,还需要把kHasBeenPoppedKey置为NO。
- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
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);
}
父控制器执行WillDealloc
当父控制器执行WillDealloc时,其所有子控制器和Presented出来的控制器都应执行WillDealloc:
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
//所有子控制器都被将会释放
[self willReleaseChildren:self.childViewControllers];
//模态出来的控制器将会释放
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
对应的View将会释放
[self willReleaseChild:self.view];
}
return YES;
}
除此之外,UINavigationController,UITabbarController,UIPageViewController和UISplitViewController在执行willDealloc时,其子控制器都应该调用willDealloc
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
同理,在UIView执行willDealloc时,其子视图都要调用willDealloc
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.subviews];
return YES;
}
记录viewStack和parentPtrs
willReleaseChildren是NSObject分类中的方法,除了调用对象的WillDealloc外,还记录了viewStack和parentPtrs:
- (void)willReleaseChildren:(NSArray *)children {
NSArray *viewStack = [self viewStack];
NSSet *parentPtrs = [self parentPtrs];
for (id child in children) {
NSString *className = NSStringFromClass([child class]);
//设置子对象的ViewStack,为该对象的ViewStack加上子对象的类名
[child setViewStack:[viewStack arrayByAddingObject:className]];
//设置子对象的parentPtrs,为该对象的parentPtrs加上子对象的地址
[child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
//子对象将会释放
[child willDealloc];
}
}
//如果出现内存泄漏,则会在alertView中显示viewStack
- (NSArray *)viewStack {
NSArray *viewStack = objc_getAssociatedObject(self, kViewStackKey);
if (viewStack) {
return viewStack;
}
NSString *className = NSStringFromClass([self class]);
return @[ className ];
}
//父节点集合,如果泄漏对象的集合跟parentPtrs有交集,则不再弹出alertView,用于防止alertView重复弹出
- (NSSet *)parentPtrs {
NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey);
if (!parentPtrs) {
parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil];
}
return parentPtrs;
}
看起来,只要出现了内存泄漏的alertView,其显示的viewStack顶层会发生泄漏,而viewStack的其他对象并不会发生泄漏
存在问题
在NSObject调用willDealloc的时候,判断该对象是否是上一个sendAction的控件,否则直接返回NO,而从上面的代码可知,在一个UIView执行willDealloc返回NO之后,则不会再遍历子视图调用willDealloc。在左滑返回时,控制器的View会被判定为上一个sendAction的控件,这个时候,如果在该View上的子视图发生了泄漏,是检测不出来的。以下是模拟这种情况的实验代码。
//确保retainView无法被释放,模拟subview内存泄漏
SecondSubView* retainView = nil;
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
SecondSubView *subview = [[SecondSubView alloc] initWithFrame:CGRectMake(10, 10, 200, 200)];
subview.backgroundColor = [UIColor redColor];
retainView = subview;
[self.view addSubview:subview];
}
如果左滑移除SecondViewController,subview不会被释放掉(但是打印其地址发现还存在),如果是点击返回按钮返回,则会检测出内存泄漏,视图栈为[SecondViewController view SecondSubView]。
实验代码在这里。
总结
总得来说 MLeaksFinder是一个非常不错的库,引进 MLeaksFinder 后,就可以在日常的开发和的过程中发现大部分的内存泄漏。开发者无需打开 instrument 等工具并且,由于开发者是在修改代码之后一跑业务逻辑就能发现内存泄漏的,这使得开发者能很快地意识到是哪里的代码写得问题。这种及时的内存泄漏的发现在很大的程度上降低了修复内存泄漏的成本,还是值得推荐的。