移动设备的内存是一块共享资源。内存管理不恰当的APP会内存使用暴增,严重的性能损耗,甚至崩溃。
Facebook的iOS版APP功能庞大繁多,但在iOS只有相同的内存使用空间。如果任何一个功能消耗了过多的内存,则会影响整个APP的性能。而这个当一个功能出现内存泄漏时是有极大可能性发生的。
当我们给对象分配一块内存后,当使用完对象但忘了释放对象占用的内存,这就会造成内存泄漏,这意味着操作系统没法回收内存来给其他功能使用,最终可能导致APP超过允许使用的内存极限。
Facebook的工程师们在代码库不同的模块下进行开发工作,造成内存泄漏情况出现很难避免,如果发现内存泄漏,我们需要快速地找到内存泄漏的地方,然后快速修复他们。
已经有不少工具可以用来查找内存泄漏了。但是他们通常都需要太多的手动操作:
1.打开Xcode,然后 build for profiling.
2.启动Instruments
3.开始测试APP,尽可能测试每个APP使用场景。
4.观察内存是否泄漏或者内存暴增
5.在代码中找出问题所在
6.修复内存泄漏元凶
这意味着需要大量的重复手工操作,因此我们很难抽出时间在开发阶段定位和修复内存泄漏问题。
如果能自动化这个流程那就太棒了,这样我们就不需要太多的工程师参与就能更快速的定位内存泄漏问题。为了搞定这个目标,我们开发了一套工具,并且已经帮我们修复了代码库的大量问题。现在我们非常开心地宣布我们将开放这些工具:FBRetainCycleDetector, FBAllocationTracker和FBMemoryProfiler.
循环引用
Objective-C使用引用计数的方式来管理内存和决定是否释放对象。内存里的任何一个对象都可以持有其他对象,如果前者依然持有后者,那后者就一直在在内存中,我们可以理解为前者“拥有”后者。
大部分时候不会出现内存问题。但是某些情况就不是这么回事了,会出现某种所谓的”僵局“,比如两个对象通过直接互相“拥有”彼此,或者间接通过其他对象互相持有。这种循环拥有就是所谓的循环引用了。
循环引用会带来许多问题。幸运的话,这些循环引用的对象可能只是小对象,一直只是浪费一丁点内存。如果这些对象做了一些重要的操作并消耗大量内存的话,那可能意味着其他功能就只能使用剩下的内存了。倒霉的话,程序会因为的内存使用超出限制导致APP崩溃。
在手工使用profile时,我们发现了大量的循环引用问题,这些问题非常容易产生,但却非常难以发现。使用循环引用发现器的话这些困难都不算事了。
在运行时(runtime)发现循环引用
查找循环引用有点像在有向非循环图(directed acyclic graph)查找循环问题,对象就是节点,边线就是对象之间的引用(因此如果对象A持有对象B,就存在A到B的引用)。我们的OC对象已经在图表中了,我们需要做的就是使用深度优先查找(depth-first)遍历图表。
这个抽象比喻非常好的解释查找内存的工作。我们需要确定是否想节点一样使用对象,并且每个对象,我们能获取到它引用的所有其他对象,这种引用要么是抢引用要么是若引用。循环引用只在强引用之间产生,因此每个对象我只需要找出它的强引用。
非常幸运,OC提供了强大的、内省(译者注:仔细观察的意思)的(introspective)运行时(runtime)库,来帮助我们发觉这张图表。
图表的节点可能是对象也是可能是block,让我们分别介绍如何来遍历他们。
对象
运行时有很多工具帮助我们内观(introspect)对象,并从中学习。
我们要做的第一件事是获取对象的实例变量的布局(ivar layout)。1
2
3
const char *class_getIvarLayout(Class cls);
const char `*class_getWeakIvarLayout(Class cls);
对于对象,实例变量的布局描述了我们在哪儿可以找到其他对象的引用。它会提供给我们一个索引(index),这代表我们需要在对象地址上添加一个偏移量(offset),就可以得到它所引用的对象的地址。运行时也允许我们获取“弱引用实例变量布局(weak ivar layout)”。
这也部分支持Objective-C中,我们可以在结构体中定义对象,但是这不会在实例变量布局中获取到。运行时提供了“类型编码(type encoding)”来处理这个问题。对于每一个实例变量来说,类型编码描述了变量是如何结构化的。如果这是一个结构体,它会描述它包含了哪些字段和类型。我们计算出它们的偏移量,在图中,找出它们所指向的对象。
也有一些边缘条件我们不会深入。大部分是一些不同的集合,我们不得不列举它们去获得它们持有的对象,这可能会导致一些副作用。
Block
Block和对象有一点不一样。运行时不会让我们很轻易的看到它们的布局,但是我们仍然可以猜个大概。
在处理Block的时候,我们使用了Mike Ash在他的项目Circle中提出的思路:Circle最早启发了FBRetainCycleDetector的项目。
我们可以使用的是Block应用程序二进制接口(ABI,application binary interface for blocks)。它描述了Block在内存中的样子。如果我们知道我们在处理的引用是一个Block,我们可以把它投射成一个模仿该Block特征的虚构的结构体中。在放到一个C语言的结构体之后,我们就可以知道该Block所持有的对象了。可是,我们不知道这些引用是强引用还是弱引用。
我们使用了一个黑盒技术来搞定这个问题。我们创建一个对象来假扮我们想要查找的Block。因为我们知道Block的接口,我们知道在哪可以找到Block持有的引用。我们伪造的对象用“释放检测器(release detectors)”来代替这些引用。释放检测器是一些很小的对象,它们会观察发送给它们的释放消息,当持有者放弃持有时,释放消息会发送给强引用对象。当我们释放伪造对象时,我们可以检测哪些检测器接收到了这些消息。只要知道哪些索引在伪造的对象的检测器中,我们就可以找到原来Block中实际持有的对象了。
自动化
这个工具牛逼之处,就实在在程序员写代码的时候它会自动运行并持续检查内内存问题。
客户端的自动化方式很简单。我们在定时器上安装了一个循环引用检查器,并定期扫描内存来发现循环引用问题。当然有出现过一些小问题。当我们第一次运行检测器时,发现它不能足够快地检查所有内存空间,因此我们需要给它一组候选对象来进行检测。
为了更有效地解决这个问题,我们开发了FBAllocationTracker。这个工具会主动跟踪所有NSObject子类的创建和释放。它可以随时获取任何类的任何实例,并且性能消耗微不足道。
客户端的自动化内存检查只要在NSTimer上使用FBRetainCycleDetector,再用FBAllocationTracker来抓取实例来配合跟踪就行了。现在,让我们来仔细看看内部具体会发生什么。
循环引用可以包含任何数量的对象。一个坏的连接会导致很多环的时候,这就复杂了。
A->B是一个糟糕的连接,因为由于它导致了两个循环引用A-B-C-D and A-B-C-E。
这就导致了两个问题产生:
1.我们不想因为同一个坏引用把这个两个循环引用进行独立开来标记。
2.我们不想把两个循环引用放在一起处理,尽管他们有相同的连接,但他们事实上不同的循环引用。
因此我们需要为循环引用定义簇(clusters)。我们根据一下规则写了一个算法来查找这些问题:
1.汇总指定日期查找到的所有循环引用
2.为每个环,命名一个Facebook格式的类名
3.为每个环,找到该环内被汇报的最小的环
4.将上面的原则将环添加到一个组
5.只汇报最小的环
有了这些工具之后,最后一步就是找到导致循环引用的工程师。我们在造成环的代码部分使用‘git/hd指责’(’git/hg blame’),推测最新修改这部分代码的工程师导致了这个问题。因此这个工程师收到任务去修复这个问题。这个系统工作原理可以如图所示:
手动性能分析
虽然自动化有助于简化发现循环引用的过程,降低人员的消耗,手动性能分析依然有它的用武之地。我们创建的另一个工具允许任何人查看内存使用,甚至不需要把他的手机插到电脑上。
FBMemoryProfiler可以很容易的添加到任何应用程序,可以让你手动配置构建文件,可以让你在应用程序内运行循环应用检测。它会借用FBAllocationTracker和FBRetainCycleDetector来实现此功能。
代(Generations)
FBMemoryProfiler最屌的特性在于它提供了“代跟踪”,类似苹果Instruments的代跟踪。代用来快照两次事件标记之间的所有存活对象。
使用FBMemoryProfiler的UI进行操作,比如分配三个对象后我们可以标记一个代,然后继续分配更多对象再标记一个代,第一个代包含开始的三个对象,如果任何一个对象释放了,它就从第二个代里移除了。
:
当我们有一个重复的任务,它可能会内存泄露的时候,生成代追踪是很有用的,例如,导航View Controller的进出。在每次开始我们的任务的时候,我们标记一个代,然后,对之后的每个生成代进行调查。如果一个对象不应该活这么长时间,我们可以在FBMemoryProfiler界面清楚地看到。
欢迎检出(checkout)来用
不管你的APP大小,功能多还是少,好的内存管理意味着好的工程质量标配。有了这些工具,我们能更容易发现内存泄漏的问题,我们可以花更少的时间去手动处理这些问题,然后就有时间写更好的代码啦。希望喜欢这些工具,去github上检出这些工具试试看吧FBRetainCycleDetector, FBAllocationTracker和FBMemoryProfiler。