内存泄漏监听--基于runtime

有段时间没有写博客了,最近被一些琐事所牵绊住了,大致梳理了下明年的一个学习计划,明年可能会在Swift与CoreText两块进行一些系统的学习,其实这两块的内容之前也有用过,但也只是停留在用过上面。

监听内存泄漏的Demo,其实在上个月就写了,现在还有一些不太成熟,仅适合自己Debug使用,在总结这块的代码之前,先来整理一下大致的思路吧。

在此之前,我们监听内存泄漏主要靠Instruments工具,以及静态扫描等手段,在使用Instrments通常只是指明了一个大概方向,很多时候无法定位到具体是哪个对象存在泄漏。另外一个就是需要专门跑Instruments工具,耗费大量的时间。

另外一个影响我去写这个Demo的驱动因素是,我们在使用了ARC,以及使用静态扫描后,很多一些明显的内存泄漏已经不复存在了。留下的是一些比较隐蔽的泄漏,如循环引用,被一些Timer事件给hold住,或者在与MRC代码一起开发的过程中造成的使用不当而造成的泄漏。

我的这个Demo只负责监听我们自己写的类,系统的类和方法以及C语言的一些结构体等内容,不在我的监听范围之内。所以我写的这个Demo也只是对于Instruments工具的一个补充。


下面来讲一下整个代码的大致思路:

首先,我们需要知道对象是何时创建,何时销毁的。

我们需要通过runtimehook代码的行为,进行hook的代码如下,用于监听所有基于NSObject生成的对象的init方法和dealloc方法。

+ (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel
{
    Method originMethod = class_getInstanceMethod(self, origSel);
    Method newMethod = class_getInstanceMethod(self, altSel);

    if (originMethod && newMethod) {
        if (class_addMethod(self, origSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(self, altSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        } else {
            method_exchangeImplementations(originMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
- (void)MYDealloc
{
    [[MemoryManager sharedManager] removeObjectWidthddress:[NSString stringWithFormat:@"%p",self]];
    [self MYDealloc];
}

- (instancetype)MYInit
{
    BOOL isCustom = [self isCustomClass];
    if (isCustom && [self isKindOfClass:[UIViewController class]]) {
        [[MemoryManager sharedManager] startAnalysis];
    }
    id object = [self MYInit];
    if (isCustom) {
        [[MemoryManager sharedManager] cacheObject:self.class address:[NSString stringWithFormat:@"%p",self]];
    }
    return object;
}

这里值得注意的是,在NSObject执行init方法时,先判断是不是我们自己自定义的类,代码如下,如果不进行这一步操作,监听的范围过大,容易把iOS里的一些私有对象给监听到,而这是我们不需要的。但至于具体如何识别哪些是我们系统的对象呢?一会再讲。

- (BOOL)isCustomClass
{
    static NSString *totalClass = @"ViewController,MYModel,MemoryManager,MYSubModel,AppDelegate,BViewController";
    NSString *className = NSStringFromClass([self class]);
    if ([totalClass containsString:className]) {
        return YES;
    }
    return NO;
}

如何识别监听的对象是我们自定义的?

这个要谈到上回写的安装包瘦身的博客了,里面谈到了如何获得LinkMap文件,以及如何提取里面所有的类和函数。另外一个方法是通过. pbxproj文件,把里面引用的文件全部用正则表达式扫描一遍。
显然前者的方案更加好一点。


如何判断存在内存泄漏?

其实简单来讲,就是本该释放的对象没有释放,我们就可以认为存在内存泄漏。那么什么是“本该释放”呢?这里涉及到一个时间戳的问题,例如我们进入某个ViewController,然后从这个ViewController退出来后,那么这个ViewController,以及与此相关的一些对象就应该被释放,而如果其没有释放,则有存在泄漏的风险。
当然,有时候我们在进入ViewController时如果存在一些Timer或者网络请求的时候,是有可能需要等一段时间后才会释放的,另外如果我们的本意就是要缓存这些内容的话,也可以认为没有泄漏。
于是我创建了一个内存管理器,MemoryManager,把生成的对象按ViewController为单位进行缓存,当发生push或者present行为时,创建一个HashTable,并把在这个页面生成之后生成的对象全面存入进来。当发生pop或者dismiss行为时,对之前的页面进行内存泄漏判断。

@interface MemoryManager : NSObject
+ (MemoryManager *)sharedManager;

- (void)cacheObject:(Class)cls address:(NSString *)address;   //缓存生成的对象

- (void)removeObjectWidthddress:(NSString *)address;  //移除释放的对象

- (void)clearCaches;

- (void)startAnalysis;  //开始分析内存

- (void)stopAnalysis;    //结束分析内存

- (void)needAnalysis;  //此时可以分析内存了
@end

@implementation MemoryManager
{
    NSMutableArray *_memoryArray;
    BOOL _isAnalysis;
    OSSpinLock _spinlock;
    double _startTime;
}

+ (MemoryManager *)sharedManager
{
    static MemoryManager *share = nil;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        share = [[self alloc] init];
    });
    return share;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _memoryArray = [NSMutableArray array];
        _startTime = 0;
        _spinlock = OS_SPINLOCK_INIT;
    }
    return self;
}



- (void)cacheObject:(Class)cls address:(NSString *)address   //缓存生成的对象
{
    if (_isAnalysis) {
        OSSpinLockLock(&_spinlock);
        NSMapTable *hashTable = [_memoryArray lastObject];
        [hashTable setObject:cls forKey:address];
        OSSpinLockUnlock(&_spinlock);
    }
}

- (void)removeObjectWidthddress:(NSString *)address  //移除释放的对象
{
    OSSpinLockLock(&_spinlock);
    for (long i = _memoryArray.count -1; i >= 0; i--) {
        NSMapTable *hashTable = [_memoryArray objectAtIndex:i];
        if ([hashTable objectForKey:address]) {
            [hashTable removeObjectForKey:address];
            break;
        }
    }
    OSSpinLockUnlock(&_spinlock);
}

- (void)clearCaches
{
    OSSpinLockLock(&_spinlock);
    [_memoryArray removeLastObject];
    OSSpinLockUnlock(&_spinlock);
}

- (void)printAllLeakObject:(NSMapTable *)hashTable //打印所有存在内存泄漏的对象
{
    NSEnumerator *enumerator = [hashTable keyEnumerator];
    id value;

    while ((value = [enumerator nextObject])) {
        /* code that acts on the map table's keys */
        NSLog(@"内存泄漏%@",[hashTable objectForKey:value]);
    }
}

- (void)startAnalysis  //开始分析内存
{
    _isAnalysis = YES;
    OSSpinLockLock(&_spinlock);
    NSMapTable *table = [NSMapTable mapTableWithKeyOptions:NSHashTableCopyIn valueOptions:NSHashTableStrongMemory];
    [_memoryArray addObject:table];
    OSSpinLockUnlock(&_spinlock);
}

- (void)stopAnalysis    //结束分析内存
{
    _isAnalysis = NO;
    _startTime = 0;

    __block NSMapTable *table = [_memoryArray lastObject];
    OSSpinLockLock(&_spinlock);
    [_memoryArray removeLastObject];
    OSSpinLockUnlock(&_spinlock);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [[MemoryManager sharedManager] printAllLeakObject:table];
    });
}

- (void)needAnalysis
{
    _startTime = CFAbsoluteTimeGetCurrent();
}
@end

存在的问题

上面这个思路还存在一些问题,例如连续push好几个页面,例如setViewControllers:
另外一个问题,在push某个页面后,一些全局行为,例如通知、网络回包等也会被统计进来,而这些其实是与ViewController是无关的。
第三个问题,一些无法被归纳为push或者present的行为,例如新生成一个Window,这种行为如何捕捉。


问题的解决

1、hook UINavigationController里面的所有poppush方法。

@implementation UINavigationController (MethodSwizzle)

- (void)MYPush:(UIViewController *)vc animated:(BOOL)animation
{
    [[MemoryManager sharedManager] needAnalysis];
    [self MYPush:vc animated:animation];
}

- (void)MYPop:(BOOL)animation
{
    [[MemoryManager sharedManager] stopAnalysis];
    [self MYPop:animation];
}

+ (void)switchAllMethod
{
    [UINavigationController swizzleMethod:@selector(pushViewController:animated:) withMethod:@selector(MYPush:animated:)];
    [UINavigationController swizzleMethod:@selector(popViewControllerAnimated:) withMethod:@selector(MYPop:)];
}
@end

1、hook UIViewController里面的所有presentdismiss方法。

@implementation UIViewController (MethodSwizzle)

- (void)MYPresent:(UIViewController *)vc animated:(BOOL)animation cmopletion:(void (^ __nullable)(void))completion
{
    [[MemoryManager sharedManager] needAnalysis];
    [self MYPresent:vc animated:animation cmopletion:completion];
}

- (void)MYdismiss:(BOOL)animation completion:(void (^ __nullable)(void))completion
{
    [[MemoryManager sharedManager] stopAnalysis];
    [self MYdismiss:animation completion:completion];
}

+ (void)switchAllMethod
{
    [UIViewController swizzleMethod:@selector(presentViewController:animated:completion:) withMethod:@selector(MYPresent:animated:cmopletion:)];
    [UIViewController swizzleMethod:@selector(dismissViewControllerAnimated:completion:) withMethod:@selector(MYdismiss:completion:)];
}
@end

不足之处

1、对于一些全局变量的问题暂时没有一个好的办法去监听,如果大家有好的想法可以一起来探讨。
2、对于一些系统变量无法做到全部监听,例如以NSUI开头的变量,有很多是私有的,例如创建一个UIViewController,里面就有可能存在很多私有变量。
3、一些非pushpresent行为,暂时无法监听到。


优势

可以快速帮助我们定位到是哪个对象存在内存漏泄,在哪个ViewController下面发生的。这样,我们就可以省去很多时间去定位原因了,让我们释放出来多做一些更有价值的东西。
另外重视内存泄漏,可以少一些低内存问题导致的程序Crash,让其成为一个更加值得依赖的优秀作品。


最后:

这里由于hook了NSObject的方法,必然会在性能上面打一些折扣,建议只在Debug时使用,有更多好的想法欢迎留言。祝大家圣诞快乐!

2015-12-18 16:43245