1.MLeaksFinder.h
#import "NSObject+MemoryLeak.h"
//#define MEMORY_LEAKS_FINDER_ENABLED 0
#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 宏用来控制 是否开启循环引用的检测
#define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 0
#ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
#define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED
//COCOAPODS 因为MLeaksFinder引用了第三库(FBRetainCycleDetector)用来检查循环引用,所以必须是当前项目中使用了COCOAPODS,才能使用这个功能。
#elif COCOAPODS
#define _INTERNAL_MLF_RC_ENABLED COCOAPODS
#endif
_INTERNAL_MLF_ENABLED 作为条件编译的表达式判断条件,用于控制MLeaksFinder的其他文件是否参与编译,在发布环境下,_INTERNAL_MLF_ENABLED为0,那么相当于该库的功能关闭。如果需要无论是调试环境还是发布环境都关闭代码,可以解注释#define MEMORY_LEAKS_FINDER_ENABLED 0. _INTERNAL_MLF_RC_ENABLED表示是否导入FBAssociationManager来监测循环引用。默认不开启
2.MLeaksMessenger
这个文件主要负责展示内存泄露。 MLeaksMessenger.h中有两个方法
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message;
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle;
我们查看.m文件可以发现,后一个方法实际上是第一个方法的Designated Initializer,我们可以称之为全能初始化方法
#import "MLeaksMessenger.h"
static __weak UIAlertView *alertView;
@implementation MLeaksMessenger
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message {
[self alertWithTitle:title message:message delegate:nil additionalButtonTitle:nil];
}
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle {
[alertView dismissWithClickedButtonIndex:0 animated:NO];
UIAlertView *alertViewTemp = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:@"OK"
otherButtonTitles:additionalButtonTitle, nil];
[alertViewTemp show];
alertView = alertViewTemp;
NSLog(@"%@: %@", title, message);
}
@end
这里运用了一个技巧:使用静态全局变量用__weak修饰变量。 static __weak UIAlertView alertView; 第一次调用[alertView dismissWithClickedButtonIndex:0 animated:NO];这个方法的时候,alertView为nil, [alertView dismissWithClickedButtonIndex:0 animated:NO]不产生任何操作,只是一个弹框。 再次调用这个方法(即点击查看retain cycle),会通过alertView来dimiss现有的弹框,再显示新的弹框。所以alertView是记录当前显示的内存泄漏的弹框。同时设置__weak修饰让这个全局变量弱引用。一旦弹框消失,自动设置为nil.
3.MLeakedObjectProxy
这个文件是检测内存泄露的核心文件 对外提供了两个方法:
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs;
+ (void)addLeakedObject:(id)object;
第一个方法用来判断ptrs(NSSet类型)中是否有泄漏的对象,如果有返回True 第二个方法是将对象加入泄漏对象的集合,同时调用MLeaksMessenger的弹窗方法 无论是判断还是比较,始终需要一个集合来保存所有泄漏对象。自然而然检查MLeakedObjectProxy。 全局static变量static NSMutableSet * leakedObjectPtrs;*就是用来做比较的对象。 上面两个方法都只在 NSObject的category的 assertNotDealloc 中调用。 让我们看一下.m文件中方法的实现:
//用来检查当前泄漏对象是否已经添加到泄漏对象集合中,如果是,就不再添加也不再提示开发者
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
NSAssert([NSThread isMainThread], @"Must be in main thread.");
/*
#define NSAssert(condition, desc)
condition是一个表达式,如果表达式为false,那么就抛出一个异常,并且在日志中输出desc内容。
desc可以忽略不写。
表达式为true时,不执行任何操作。
*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
leakedObjectPtrs = [[NSMutableSet alloc] init];
});
if (!ptrs.count) {
return NO;
}
//intersectsSet判断两个集合的交集是否至少存在一个元素
//防止 addLeakedObject多次被调用
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);
//将自己封装成 MLeakedObjectProxy对象加入leakedObjectPtrs中
[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)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(), ^{
[leakedObjectPtrs removeObject:objectPtr];
[MLeaksMessenger alertWithTitle:@"Object Deallocated"
message:[NSString stringWithFormat:@"%@", viewStack]];
});
}
}
在上述两个方法的实现中,我们发现了几个要点
- 使用这两个方法必须要在主线程中使用
- 待检查的对象,必须要检查是否已经被记录,以防止重复添加,造成循环
展示循环引用的核心代码在下面:
#pragma mark - UIAlertViewDelegate
- (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;
//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
}
//把当前对象调整到第一个的位置,方便查看
- (NSArray *)shiftArray:(NSArray *)array toIndex:(NSInteger)index {
if (index == 0) {
return array;
}
NSRange range = NSMakeRange(index, array.count - index);
NSMutableArray *result = [[array subarrayWithRange:range] mutableCopy];
[result addObjectsFromArray:[array subarrayWithRange:NSMakeRange(0, index)]];
return result;
}
我们发现,MLeaksFinder在展示循环引用的时候,使用的是Facebook开源的 FBRetainCycleDetector 工具。 我们先通过 MLeaksFinder 找到内存泄漏的对象,然后再过 FBRetainCycleDetector 检测该对象有没有循环引用。 有关FBRetainCycleDetector,我们可以查阅这篇文章(需要科学上网)。 我们实际上可以了解,FBRetainCycleDetector是将一个对象,一个ViewController,或者一个block当成一个节点,相关的强引用关系则是线。他们实际上会形成有向无环图(DAG 图),我们则需要在其中寻找可能存在的环,这里使用了深度优先搜索算法来遍历它,并找到循环节点。
4.NSObject+MemoryLeak
这个文件主要用来存储对象的父子节点的树形结构,method swizzle逻辑 ,白名单以及实施判断对象是否发生内存泄漏。
- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] 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 assertNotDealloc];
});
return YES;
}
这个方法对外的时候主要是用来提供给各个方法退出的时候调用的。而内部,则是先筛选不会报告的若干情况,以下是三个判断条件:
- [[NSObject classNamesInWhiteList] containsObject:className]为True. 显而易见,这个方法是判断是否加入白名单
- [senderPtr isEqualToNumber:@((uintptr_t)self)]为True. 介绍在下方
- __strong id strongSelf = weakSelf;中的strongify为nil. 这里先设置 __weak id weakSelf = self;,然后在进行__strong id strongSelf = weakSelf,假如对象已经被释放,strongSelf为nil 调用该方法什么也不发生。
这里我说明下第二条: 正在执行target-Action的target对象不监测内存泄漏。当用户触发执行Target-Action方法的时候,实际上在执行action方法前,是sender对象先执行sendAction:to:forEvent方法,然后UIApplicatoin执行 sendAction:to:from:forEvent:方法,其中from就是sender对象. 这里使用方法交换截获sendAction:to:from:forEvent:,然后截获了当前sender对象保存在kLatestSenderKey中。判断两者是否相同。 这里的原因涉及到了target-action原理,当前实际上会形成一个循环引用,这里推荐这篇文章,我们可以得出结论
对于_target成员变量,在UIControlTargetAction的初始化方法中调用了objc_storeWeak,即这个成员变量对外部传进来的target对象是以weak的方式引用的。
而如果发生恩泄露,则会调用以下方法:
- (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)willReleaseObject:(id)object relationship:(NSString )relationship;这个方法我没有查阅到相关引用,可能是已经废弃。如果有知道的,请不吝赐教。
接下来分析下面三个关键的方法:
- (void)willReleaseChild:(id)child;
- (void)willReleaseChildren:(NSArray *)children;
- (NSArray *)viewStack;
- (void)willReleaseChild:(id)child 其实只是将child对象添加到一个数组中执行 - (void)willReleaseChildren:(NSArray *)children方法 ```
-
(void)willReleaseChild:(id)child { if (!child) { return; }
[self willReleaseChildren:@[ child ]]; }
第一个方法只在UIViewController+MemoryLeak中执行,
-
(BOOL)willDealloc { if (![super willDealloc]) { return NO; }
[self willReleaseChildren:self.childViewControllers]; [self willReleaseChild:self.presentedViewController]; if (self.isViewLoaded) { //判断一个UIViewController的view是否已经被加载 [self willReleaseChild:self.view]; }
return YES; }
而后一个方法则在很多地方都有执行,其内部实现如下
- (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]; } } ``` 注释掉无关的代码,我们实际上发现,这里循环调用willDealloc方法。而注释掉的方法则是递归self.view,写入一个栈viewStack当中,最后在Alertview中展示出来。 构造堆栈信息的原理就是,递归遍历子对象,然后将父对象 class name 加上子对象 class name,一步步构造出一个 view stack。出现泄漏则直接打印此对象的 view stack 即可。
+(void)addClassNamesToWhitelist:(NSArray )classNames;方法则一目了然,用于添加白名单。 最后一个方法
+ (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_getImplementtation是方法实现, 它返回一个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
}
中间那一段BOOL类型的使用原因 “ 周全起见,有两种情况要考虑一下。第一种情况是要复写的方法(overridden)并没有在目标类中实现(notimplemented),而是在其父类中实现了。第二种情况是这个方法已经存在于目标类中(does existin the class itself)。这两种情况要区别对待。 (译注: 这个地方有点要明确一下,它的目的是为了使用一个重写的方法替换掉原来的方法。但重写的方法可能是在父类中重写的,也可能是在子类中重写的。) 对于第一种情况,应当先在目标类增加一个新的实现方法(override),然后将复写的方法替换为原先(的实现(original one)。 对于第二情况(在目标类重写的方法)。这时可以通过method_exchangeImplementations来完成交换.”
这个方法为其他分类统一给出了一个Method Swizzling的方法。
其他类
关于其他类,基本上都是实现了交换方法。不细说。
总结
MleaksFinder中使用了AOP的思想,不会插入到业务代码当中,即插即用。检测流程可以简化为:
- 给分类统一提供一个交换方法的方法(此方法保证一定可以交换方法)
- 然后在运行中统一遍历view,viewController,将名字加入栈中
- 规避掉一些方法(白名单,Target-Action方法)
- 检测是否有循环引用