Facebook的iOS客户端有许多特性,它们共享同一个内存空间,所以假如某个特定的特性消耗太多的内存,这会影响到整个应用,比如某个特性意外的出现内存泄漏。
当我们为一组对象分配内存,如果使用完没有释放相应的内存就会导致内存泄漏情况的发生,这意味着系统无法回收该内存来用于其它用途,最终导致内存耗尽。
在Facebook,许多工程师在不同的代码仓库上工作,这不可避免会有内存泄漏的情况发生,当出现这种情况时,我们需要快速的找到并修复它们。
已经有一些工具来辅助我们找到内存泄漏,不过需要大量的人工干预:
- 打开Xcode,选择build for profiling.
- 载入Instruments工具
- 使用app, 尝试尽可能多的重现场景和行为
- 查看instrument的leaks/memory
- 查找内存泄漏的根源
- 修复问题
这意味着每次都需要大量的手动操作,导致我们可能在开发周期内无法尽早的定位以及修复内存泄漏的问题。
如果该过程能够自动化,我们就能够在太多开发者干预的情况下快速找到内存泄漏。为此我们构建一系列的工具来自动化查找以及修复代码仓库中的一些问题,这些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler
Retain cycles(循环引用)
Objective-C使用引用计数来管理内存以及释放不使用的对象,任何一个对象可以持有(retain)其它对象,这样只要前面的对象需要使用它,该对象就会一直保存在内存,可以认为对象“拥有”其它对象。
大部分情况下这都工作的很好,但是假如两个对象最后互相“拥有”对方,直接或着更多通过其它对象间接的连接它们,这就会陷入一个僵局。这种持有引用的环就叫做循环引用。
循环引用会导致一系列的问题,最优的情况是对象一直在RAM中只占据一点点的空间。如果泄漏的对象只是做一些无关紧要的工作,那结果只是应用会少一些可使用的内存。最差的情况下,假如泄漏的内存超过了可使用的内存空间,应用可能会崩溃。
在手动性能分析的过程中,发现我们往往会有很多循环引用的情况,在开发的时候很容易出现循环引用,但却不容易在后面找到。Retain Cycle Detector可以帮助我们很容易的找到它们。
运行时检测循环引用
在Objective-C中查找循环引用类似于在一个有向无环图(directed acyclic graph)中查找环,节点就是对象,而边则是对象之间的引用(如果对象A retain 对象B,那么A到B之间就存在引用)。我们的Objective-C对象已经存在于我们的图当中,我们要做的就是使用深度优先方法历遍搜索它。
虽然这只是个简单的抽象,但实际效果却不错。我们必须确保我们能够像节点一样使用对象,对于每个对象,我们能够获取它所引用的所有对象,这些引用可能是weak或者strong,不过只有strong的引用才会导致循环引用。因此对于每个对象,我们需要知道如何找出这些强引用。幸运的是,Objective-C提供了一套强有力、内省的运行库,能够提供我们足够的数据去挖掘这张图。
图中的节点可以是一个对象或一个block,让我们分别讨论。
对象(Objects)
运行时有许多工具能够让我们对对象进行内省学习,我们要做的第一件事就是获取对象所有实例变量的布局(ivar layout)
//runtime.h
const char *class_getIvarLayout(Class cls);
const char *class_getWeakIvarLayout(Class cls);
对于一个给定的对象,实例变量布局描述了我们该去哪查找其所引用的其它对象。它会提供一个“索引”,这个索引代表着偏移量(offset),我们在对象地址上加上该偏移量来获取它所引用对象的地址。运行时还允许我们获取“弱引用实例变量的布局(weak ivar layout)”,我们可以认为这两种布局的差别在于强引用布局。
这也部分支持Objective-C++。在Objective-C++中,我们可以在结构体中定义对象,但这不会在实例变量布局(ivar layout)中获取到,运行时提供“类型编码(type encoding)”来解决这个问题。对于每个实例变量,类型编码描述了变量如何结构化的。如果变量是一个结构体,它描述了变量包含的字段和类型。我们通过解析类型编码来找出哪些实例变量是Objective-C对象。我们计算出它们的偏移量(offset),然后在布局中找到它们指向对象的地址。
有些边缘情况我们不会深入。大部分是一些不同的集合,我们需要历遍它们来获取它们持有的对象,这可能会有一些副作用。
Blocks
Blocks跟对象有些区别。运行时没有让我们很容易看到它们的布局,但我们仍然可以猜测。在处理Blocks的时候,我们采用Mike Ash在他的Circle项目中的思路。
我们可以使用的是ABI(application binary interface for blocks),它描述了block在内存中的样子。如果我们知道在处理的引用是一个block,那我们可以使用一个假的结构体来模拟该block对象。将block转换成一个C结构体后,我们就可以知道block持有哪些对象,不过不幸的是,我们不知道这些引用是强引用还是弱引用。
为了解决这个问题,我们使用一个黑盒技术,我们创建一个伪造对象假装是我们要研究的block。因为我们知道block的接口,我们知道在哪可以找到block持有的引用,伪造的对象使用"release detectors"来替代这些引用。release detectors是一些小的对象,它们会观察发送给他们release的消息。当持有者想要放弃对象的拥有权时,release消息就会发送给它所强引用的对象。当我们释放该伪造的对象后,可以检查哪些detectors收到了release消息。知道了接收release消息的detectors的索引位置之后,我们就可以找到block对象所持有的强引用对象。
自动化
这些工具在工程构建的时候能够持续自动的运行。
在客户端的自动化很简单。我们定时的运行Retain Cycle Detector,定期的去扫描内存查找循环引用,不过这并不是这么一帆风顺。我们第一次运行Detector时,我们意识到它无法很快的扫描整个内存空间,我们需要首先提供一组候选对象来让Detector检测。
为了更有效的处理上述问题,我们创建了FBAllocationTracker。这个工具能够记录所有NSObject子类对象的创建和销毁,它能够以极小的性能代价在任意时刻快速获取任何类的对象实例。
客户端有了上述的自动化过程,意味着我们只需要在NSTimer上运行FBRetainCycleDetector,在配合FBAllocationTracker来抓取我们想要分析的实例即可。
现在让我们深入的看一下背后具体发生了什么。
循环引用可以包含任意数量的对象,当由于一个坏的连接(bad link)导致很多环的产生,事情就变的更复杂了。
在上面的环中,A->B就是一个坏的连接(bad link),它创建了两个环:A-B-C-D和A-B-C-E。这会有两个问题:
- 如果由同一个坏的连接导致两个循环引用,我们不想用不同的标记来分别标记它们;
- 我们不想给可能代表两个不同问题的两个循环引用一起标记,即便他们共享一条连接。
所以我们需要给循环引用定义类簇(clusters)。我们写了一个算法来找出这些问题,算法如下
- 在给定的时间,收集所有的环;
- 对于每个环,提取Facebook特定的类名称;
- 对于每个环,找出该环包含的最小环;
- 将每个环添加到由上面找到的最小环所代表的组中;
- 只报告最小环;
最后要做的就是找到谁第一时间偶然地引入了循环引用,可以通过对环所涉及的代码进行'git/hg blame',我们猜测可能最新的代码导致了该问题,所以最后一个接触该代码的人会收到一个task来修复该问题。
整个过程如下图所示:
手动性能分析
虽然自动化能简化发现循环引用的过程,减少开发人员的消耗,但手动性能分析还是必不可少。我们创建了另外一个工具,允许任何人查看内存的使用情况,甚至不需要把手机插到电脑上。
FBMemoryProfiler可以很容易的添加到任意应用,让你在应用内部手动配置构建文件以及运行循环引用检测,该工具借助FBAllocationTracker和FBRetainCycleDetector实现该功能。
代(Generations)
FBMemoryProfiler一个最大的特性是提供"代追踪(generation tracking)",类似苹果instruments的generation tracking,Generations是两个时间标记之间所有仍然活着的对象的快照。
使用FBMemoryProfiler的界面,我们可以标记一个generation,比如创建了三个对象;然后我们标记另一个generation并继续创建对象。第一个generation包含了我们三个最初的对象,如果有对象被释放了,那么该对象就会在第二个generation中被移除。
假如有一个重复的任务,我们认为可能有内存泄漏的情况发生,这时候Generation tracking就很有效了。比如导航进入一个View Controller然后退出,每次开始任务之前,我们标记一个generation,然后在每次generation标记之间进行调查,如有对象并不应该存活那么长,那我们可以在FBMemoryProfiler的界面上清楚的看到。
原文链接:Automatic memory leak detection on iOS - Facebook Engineering