What I Talk About When I Talk About Leaks

前言

最近为[伯乐在线]翻译一篇文章(该文章尚未发布),作者 Russ Bishop 谈到自己查找内存泄漏的一次痛苦经历,读后很有感触,但是那些对于 Instruments 使用不太熟练的读者可能由于 Bishop 忽略了一些细节,看后估计会觉得喝白水般不能解渴,所以写一篇自己的经历将 Bishop 忽略的细节补全作为“译后感”吧。

一种麻烦的检查内存泄漏方式

为什么麻烦还要介绍呢,因为看到有人这样调试的。这种方式需要写些代码,这些代码完全是调试用的,而且只能让你知道是否存在泄漏,但是无法定位,看看这种方式如何操作。

在 dealloc 方法中写一个日志,如果退出页面的时候能够打印出日志内容,说明该页面没有问题,如果没有打印,那么就是有问题的。

- (void)dealloc {
     DLog(@"release");
 }

这样如果页面中有输出 release,说明该页面 CollageViewController 是没有内存泄漏的

2016-04-01 12:51:56.992 [267:19048] -[CollageViewController dealloc] - [Line 88] -- release

但是如果故意把循环引用引入,这个时候会有 Warning 警告,同时 release 也不会打印了

self.paperSizeView.paperSizeBlock = ^(PaperSizeModel *sizeModel) {
    self.paperSize = sizeModel.paperSize;
}

要把 self 改为 WeakSelf。

上面的 DLog 是我常用的一个日志宏,调试的时候能输出函数名称和所在行数,定义如下

#ifdef DEBUG
#define DLog(args, ...) NSLog((@"%s - [Line %d] -- " args), __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#   define DLog(...)
#endif

如果觉得这样的方式输入的内容还不够丰富,建议使用 喵神的代码

#define NSLog(format, ...) do { \
    fprintf(stderr, "<%s : %d> %s\n",  \
    [[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
    __LINE__, __func__);  \
   (NSLog)((format), ##__VA_ARGS__);   \
   fprintf(stderr, "-------\n");       \
} while (0)

注意,这种方式在某些情况下仍然能执行 dealloc,并不是十分可靠,还要确认 Instruments 的 Leaks 下没有报错才可以,例如以下这种比较常见的循环引用,需要使用 Leaks 查看。

常见的循环引用

循环引用

@interface RetainCycleModel : NSObject
@property (strong, nonatomic) id obj;
@end


RetainCycleModel *model1 = [[RetainCycleModel alloc] init];
RetainCycleModel *model2 = [[RetainCycleModel alloc] init];

[model1 setObj:model2];
[model2 setObj:model1];

上例中 model1 强引用了 model2,model2 又强引用了 model1。Demo 代码点击这里

NSTimer 使用后没有释放

self.refreshTimer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                      target:self
                                                    selector:@selector(updateProgressTimer:)
                                                    userInfo:nil
                                                     repeats:YES];

scheduledTimerWithTimeInterval 方法的 target 参数说明如下

The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.

timer 持有 self(target) 强引用,同时 self.refreshTimer 是 self 强引用了 timer,出现了循环引用。所以必须在恰当的地方调用 invalidate 将其释放。invalidate 的作用如下:

Removes the object from all runloop modes (releasing the receiver if it has been implicitly retained) and releases the 'target' object.

注意下述方法当计数器的 repeats 设置为 YES 的时候, self 的引用计数会加 1。可能会导致 self 不能 release,dealloc 无法被执行

- (void)method {
    self.refreshTimer  =  [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(function:) userInfo:nil repeats:YES]; 
}

- (void)dealloc {
    [self.refreshTimer invalidate];
}

注册通知后没有移除

NSNotification addObserver 使用后没有(在 dealloc 中) removeObserver,或者 delegate 使用后没有置为 nil

如何用 Instrument 定位 Leak

首先使用静态分析 Static Analyzer,之后使用 Instrument -> Leaks,再次查看 Allocation

还是拿 [LEAF Photo] 举例,运行 Allocation。

运行后,将所有的页面都跑一遍,会看到内存一直增长,当返回页面后内存应该是下降趋势的。如果内存没有返回到初始状态,说明在某个页面是有内存泄漏的。这时可以在搜索框搜索自己写的类名。

img

看,这是进入 CollageViewController 后退出后可以发现内存不断增长,在内存中搜索类名能看到这个视图控制器没有被释放,还有一些其他相关的类。这样就能知道是否有泄漏存在了。当然了,这个代码很容易查到原因,只需要点击进入每行对应的代码查看,查看到 CollagePaperSizeView 时会指引到 paperSizeView 的 Block 中有循环引用。

如果问题这么容易就定位到了,Bishop 也就不用写一大篇文章了,上面是刚好我们能搜到类名,正好出现问题的地方用了类名。如果搜索不出来应当怎么办呢,那么 Mark Generation 该上场了。

就像 Bishop 所说的一样,按照重现步骤操作,在出现问题的页面进入之前 Mark 一下,进入页面退出后再 Mark 一下。

这样会生成 Generation A 和 Generation B,展开 Generation B ,看 Persistent 数最多的,这里是 non-object,再展开估计也看不出来什么了,点击进入 non-object,查看详细,接下来的页面也是很多数据,可以将 Responsible Libaray 列排序,然后找到你的项目名字。

img

这里就没有那么多噪声了,很容易就能找到哪个是出问题的地方,看这里提示 save 方法有问题,就是该方法中出现了循环引用了。

PS: Xcode 8 添加了 Debug Memory Graph 工具,可以通过对象的图形关联图确认是否有循环应用

缓存

之后我想起来:这就是我们通常所谓的 Xcode,我打开终端,然后执行 fuxcode (我写的一个清理所有 DerivedData 的脚本)。又一次的重新编译之后 Instruments 确认没有任何泄漏了。

Xcode 有缓存估计很多人都经历过这个痛楚吧。写个脚本把所有的 DerivedData 删除 (哦,删吧,删了能把硬盘腾出很多地方),或者只删除当前项目的 DerivedData 吧,点击菜单 Xcode -> Product,按住 Option 键,Clean 变成 Clean Build Folder,清理下编译目录即可。

请我喝汽水儿