MLeaksFinder / FBRetainCycleDetector 分析

最近需要提供一套检测 app 内存泄漏的工具,经过反复的比较,最终选定了 MLeaksFinder。它的优点是实现简单,扩展方便。它内部寻找循环引用则是用的 Facebook 开源的 FBRetainCycleDetector。

MLeaksFinder

介绍

MLeaksFinder 的原理十分简单:

  1. 通过运行时 hook 系统的 viewdidDisappear 等页面消失的方法,在 hook 的方法里面添加willDealloc()方法,各个子类自己实现 willDealloc()方法。
  2. NSObject的 willDealloc()方法会有一个延迟执行 2s 的 alert 弹框,如果 2s 以后对象被释放,系统会把对象指针设置为nil,2s 以后也就不会有弹框出现,所以根据 2s 以后有没有弹框来判断对象有没有正确的释放。
  3. 最后会有一个 proxy 实例 objc_setAssociatedObject 在 object 上,如果上述弹窗提示未被释放的对象最后又释放了,则会调用 proxy 实例的 dealloc 方法,然后弹窗提示用户对象最终还是释放了,避免了错误的判断。

不足之处:
只能自动地检测 UIViewControllerUIView 相关的对象。

扩展

可以利用 iOS 运行时机制,遍历待校验对象的成员变量列表,当然还有他们的父类。通过递归调用,逐渐一层层递进遍历。不过有几个注意点:

  1. 如果全部遍历,那会非常占用 cpu 资源,导致界面卡死。所以要做个过滤,可以自己建名单,也可以通过别的方式,我们的工程所有的类都是一个前缀,所以根据类的前缀来过滤。
  2. 对于有循环引用的变量,遍历下去会无限循环,所以需要加个判断。
  3. 对于 NSArrayNSDictionaryNSSet 对象,不需要遍历它们的成员变量,而需要遍历它们的存储的对象,所以增加 3 个对象的 category 来进行特殊处理。
  4. 成员变量校验的功能并不需要时时打开,可以加个开关控制。

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

找出循环引用主要要做两个工作:

  1. 找出 Object( Timer 类型因为 Target 需要区别对待 ),每个 Object associate 的 Object,Block 这几种类型的 Strong Reference。
  2. 最开始就是自身,把 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_setAssociatedObjectobjc_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 * infovoid * 是不可逆的,除非本身就知道它的结构。但是 FB 用 _FBNSCFTimerInfoStruct 来接收,真的很好奇它是怎么来的,issue 里有人问了这个问题,但是没人回答:
How do you know zhe define of ‘_FBNSCFTimerInfoStruct’?
网上找不到相关回答,Apple Source 开源的代码里也找不到,我个人猜测,应该是试出来的......

检测环

其实就是深度遍历,需要一个 stack 和 objectsOnPath。已经遍历的节点存在 objectsOnPath 里,之后遍历的节点要是已经在 objectsOnPath 中,则存在循环引用。最后会做去重操作,还限制了深度,防止 CPU 消耗过大。

作者:levi
comments powered by Disqus