最近需要提供一套检测 app 内存泄漏的工具,经过反复的比较,最终选定了 MLeaksFinder。它的优点是实现简单,扩展方便。它内部寻找循环引用则是用的 Facebook 开源的 FBRetainCycleDetector。
MLeaksFinder
介绍
MLeaksFinder 的原理十分简单:
- 通过运行时 hook 系统的
viewdidDisappear
等页面消失的方法,在 hook 的方法里面添加willDealloc()
方法,各个子类自己实现willDealloc()
方法。 - NSObject的
willDealloc()
方法会有一个延迟执行 2s 的 alert 弹框,如果 2s 以后对象被释放,系统会把对象指针设置为nil,2s 以后也就不会有弹框出现,所以根据 2s 以后有没有弹框来判断对象有没有正确的释放。 - 最后会有一个 proxy 实例
objc_setAssociatedObject
在 object 上,如果上述弹窗提示未被释放的对象最后又释放了,则会调用 proxy 实例的dealloc
方法,然后弹窗提示用户对象最终还是释放了,避免了错误的判断。
不足之处:
只能自动地检测 UIViewController
和 UIView
相关的对象。
扩展
可以利用 iOS 运行时机制,遍历待校验对象的成员变量列表,当然还有他们的父类。通过递归调用,逐渐一层层递进遍历。不过有几个注意点:
- 如果全部遍历,那会非常占用 cpu 资源,导致界面卡死。所以要做个过滤,可以自己建名单,也可以通过别的方式,我们的工程所有的类都是一个前缀,所以根据类的前缀来过滤。
- 对于有循环引用的变量,遍历下去会无限循环,所以需要加个判断。
- 对于
NSArray
,NSDictionary
,NSSet
对象,不需要遍历它们的成员变量,而需要遍历它们的存储的对象,所以增加 3 个对象的 category 来进行特殊处理。 - 成员变量校验的功能并不需要时时打开,可以加个开关控制。
//NSObject+MemoryLeak.m
//防止循环引用导致无限循环
- (BOOL)leakChecked
{
NSNumber *leak = objc_getAssociatedObject(self, kCheckKey);
return [leak boolValue];
}
- (void)setLeakChecked:(BOOL)leakChecked
{
objc_setAssociatedObject(self, kCheckKey, @(leakChecked),OBJC_ASSOCIATION_RETAIN);
}
- (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;
//增加的递归遍历代码
#ifdef NEW_MEMORY_LEAKS_ALL_OBJECT_FINDER_ENABLED //增加开关
if([self leakChecked])
{
return NO;
}
[self setLeakChecked:YES];
NSMutableArray<NSString *> *pArray = [[NSMutableArray alloc] init];
[self configClassPropertiesWithClass:[self class] array:pArray];
for (NSString *key in pArray) {
id valueObj = [self valueForKey:key];
if (valueObj) {
[self willReleaseObject:valueObj relationship:key];
}
}
#endif
__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;
}
- (void)configClassPropertiesWithClass:(Class) class array:(NSMutableArray<NSString *> *) pArray {
if (class == [NSObject class]) {
return;
}
NSBundle *bundle = [NSBundle bundleForClass:class];
if(bundle != [NSBundle mainBundle]) {
return;
}
[self configClassPropertiesWithClass:[class superclass] array:pArray];
unsigned int count;
objc_property_t *properties = class_copyPropertyList(class, &count);
for (int i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
NSString *propertyAttributes = [NSString stringWithUTF8String:property_getAttributes(property)];
if (propertyAttributes.length) {
NSArray<NSString *> *desArray = [propertyAttributes componentsSeparatedByString:@","];
if (desArray.count) {
NSString *classNameDes = [desArray firstObject];
if ([classNameDes length] > 4) {
NSString *className = [classNameDes substringWithRange:NSMakeRange(3, [classNameDes length] - 4)];
//根据前缀过滤
if ([className hasPrefix:@"CT"]) {
[pArray addObject:propertyName];
}
}
}
}
}
}
//3 个集合类的 category
//NSArray+MemoryLeak.m
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
for (id obj in self) {
[obj willDealloc];
}
return YES;
}
//NSSet+MemoryLeak.m
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
id obj;
NSEnumerator * enumerator = [self objectEnumerator];
while (obj = [enumerator nextObject]) {
[obj willDealloc];
}
return YES;
}
//NSDictionary+MemoryLeak
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
for (id obj in self.allValues) {
[obj willDealloc];
}
return YES;
}
后续优化
检测是以 ViewController
为载体,所以无法检测单例中的成员变量,这个后续可以加上。
FBRetainCycleDetector
MLeaksFinder 只是用来找到内存泄漏的变量,之后找循环引用的环则交给了 FBRetainCycleDetector。
找出循环引用主要要做两个工作:
- 找出 Object( Timer 类型因为 Target 需要区别对待 ),每个 Object associate 的 Object,Block 这几种类型的 Strong Reference。
- 最开始就是自身,把 Self 作为根节点,沿着各个 Reference 遍历,如果形成了环,则存在循环依赖。
强引用
@interface FBObjectiveCGraphElement : NSObject
@interface FBObjectiveCBlock : FBObjectiveCGraphElement
@interface FBObjectiveCObject : FBObjectiveCGraphElement
@interface FBObjectiveCNSCFTimer : FBObjectiveCObject
@interface FBAssociationManager : NSObject // FBObjectiveCGraphElement类里通过它获得所有关联的强引用
获得所有的 Strong Reference 就需要以上 5 个类。
每个 Object 都被包装成 FBObjectiveCGraphElement
类型。然后调用 allRetainedObjects
获得所有强引用。所以难点就在于如何获得 Object 的强引用。
Object
Objective-C 中引入了 Ivar Layout 的概念,对类中的各种属性的强弱进行描述。所以可以根据描述来获得所有的强引用。
Associated Object
fishhook 可以动态修改 C 语言函数实现。所以可以把 objc_setAssociatedObject
,objc_removeAssociatedObject
替换,保存每次 associate 的强引用对象。
Block
所有 Block 引用的对象会存在 Block 的地址后面,然后根据 Block 结构里的 dispose_helper
(析构)方法获得强引用。
Timer
CFRunLoopTimerContext
的 info 里存了 Timer 的结构,可以获得 Target。
以上我只是大致的介绍了下,详细实现可以参考这:
检测 NSObject 对象持有的强指针
检测强引用的 Associated Object
检测 Block 持有的强指针
疑问:
typedef struct {
long _unknown; // This is always 1
id target;
SEL selector;
NSDictionary *userInfo;
} _FBNSCFTimerInfoStruct;
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
} CFRunLoopTimerContext;
Block 的结构可以通过 Clang -rewrite-objc
来获得,但是 CFRunLoopTimerContext
的结构里有个 void * info
。void *
是不可逆的,除非本身就知道它的结构。但是 FB 用 _FBNSCFTimerInfoStruct
来接收,真的很好奇它是怎么来的,issue 里有人问了这个问题,但是没人回答:
How do you know zhe define of ‘_FBNSCFTimerInfoStruct’?
网上找不到相关回答,Apple Source 开源的代码里也找不到,我个人猜测,应该是试出来的......
检测环
其实就是深度遍历,需要一个 stack 和 objectsOnPath。已经遍历的节点存在 objectsOnPath 里,之后遍历的节点要是已经在 objectsOnPath 中,则存在循环引用。最后会做去重操作,还限制了深度,防止 CPU 消耗过大。