# 产生的背景
在移动设备上内存是一块公用的区域,如果一个 App 没有做好内存管理那么一定会导致性能急剧下降甚至会崩溃。
Facebook 的 iOS 端有许多的地方都共享着一块内存,如果任何一个地方占用太多的内存的话就会影响到整个 App,比如一个地发生了内存泄漏,就会出现这种情况。我们把一组内存分配我们的一个对象,但是当我们使用完之后忘记释放他,这就通常就会引起内存泄漏,这就意味着系统永远不能回收这块内存也就导致这块内存一直不能分配给别的对象。在 Facebook 里我们有许多许多的工程师在代码的不同部分工作,内存泄漏时不可避免的,当一旦有内存泄漏发生我们就需要立即找到并且修复。虽然现在有好多检测内存泄漏的工具但是这些工具并不完善,他们仍然需要开发者去做一些工作:
- 1:打开 Xcode 并且 Build
- 2:运行 instrument
- 3:使用 App 尽可能的去复现
- 4:寻找内存泄漏的来源
- 5:解决问题
这意味着需要大量的体力活,并且都是些重复无聊的工作,这也导致了我们不能在开发周期就定位并且修复问题。
将这个过程自动化可以让我们在不需要太多的开发者的情况下更快的去找到内存泄漏。为了解决这个问题我们已经建立一套工具使我们能够自动的去完成这些过程,并且这套工具已经解决了我们自己代码的一些问题,今天我们很激动的宣布我们把他们发布了出来 FBRetainCycleDetector, FBAllocationTracker, and FBMemoryProfiler.
# 循环引用
Objective-C 使用引用计数来管理内存的,内存中的一个对象可以引用其他的对象,只要有一个对象使用它,那么他就会一直被留在内存中。我们也可以说一个对象持有另一个对象。
一般来说这都没问题。但是有一种情会使我们陷入僵局。当两个 obj 不在相互持有,而是通过另一个 obj 来互相持有,这样就会陷入一个循环引用的状态就像下面这张图
一个 View Controller 持有一个 Viw View 中持有 delegate delegate 中持有 ViewController。这样就形成一个环状,谁也无法释放。
循环引用会导致一些列的的问题,如果一个对象在 RAM 中无限的占用空间,充其量也只是浪费一点点内存。如果这些泄漏的对象正在做一些其他的事情那么就会导致 App 的其他的地方再也无法使用这块内存。更严重的如果循环引用过多,就会浪费掉大量的内存最终导致程序的 crash。
在我们进行人工调试的时候我们已经找出了大量的很明显的循环引用,我们能很好地定位他,但是在运行之后我们就很难发现他们,但是我们这 SDK 能很轻松的做这些事。
# 在 Runtime 下的循环引用检测
在 OC 中找循环引用其实就类似于在一个节点为对象,链接线为引用关系的有向无环图中寻找一个环。打个比方如果 A 引用 B 那么 A 到 B 就会有箭头指向可以看这个
我们发现这张图在箭头处发生循环引用
这个是一个很简单的引用关系我们必须确保我们能像左侧那样的来引用对象,其实对于每一个对象我们都获取他所有引用 (持有的) 对象,有的是强引用有的是弱引用,但是循环引用只发生在强引用上,对于每个对象我们需要搞清楚如何找到引用关系。
幸运的是 OC 给我提供了强大的 Runtime,这足够让我们去挖掘其中的关系,图中的一个点可能是 block 或者是 Object 中的其中一个,让我们来分别讨论下
# Objects
Runtime 中有很多工具去帮助我们了解其中的东西,首先我们搞懂到所有实例变量的引用关系
1 | const char *class_getIvarLayout(Class cls); |
对于一个给定的对象,实例变量布局图告诉我们了他都引用了哪些对象,他会给我提供一个索引,这个索引相当于一个偏移量,该对象加上这个偏移量就是他所引用的对象的地址。运行时会给我们提供一个 “弱引用” 的布局图,也就是该对象所有弱引用的对象,强引用和弱引用之间的区别我们可以猜想为就是强引用的布局图。
对于 objective-c++ 来说我们可以用结构体来定义一个对象,这些对象不会再实例变量的布局图中被获取到,不过 Runtime 给我提供了 “类型编码” 来处理这个问题,对于每个实例变量,类型编码描述了变量是如何构造的。如果它是一个 struct,类型编码可以描述出它包含的字段和类型。我们解析类型编码以找到哪些实例变量是 objective-c 的对象。然后我们可以像在布局图中那样计算他的偏移量然后拿到他所引用的对象的地址。
还有一些我们不会深入讨论的边缘案例。这些都是不同的集合,我们必须列举它们来获取它们的保留对象,这可能会产生一些副作用。
# Blocks
block 和对象有一点不同。运行时不允许我们轻松地查看它们的布局,但是我们仍然可以进行猜测。在处理 block 时,我们使用了 Mike Ash 在他的项目 Circle 中提出的想法:也正是这个项目激发 FBRetainCycleDetector 的项目。
$emsp; 我们可以使用 application binary interface for blocks (ABI),他可以向我们展示一个 block 在内存中是什么样子的,如果我们知道了我们所研究的 block 的表现形式我们就可以用一个假的结构体来模仿实现 block 的功能,之后我们就能知道了哪些对象被 block 引用并且一直在那里,但是不幸的是我们并不知道这些事强引用还是弱引用。
为了做到这些我们使用黑盒的方法,首先我们创建一个假的对象来模仿我们所研究的 Block,因为是我们自己创建的假对象,所以我们可以完全的操作他,我们给这个假装的 block 上面装上释放探测器,释放探测器是监听发给他们的释放消息的一个对象,如果一个所有者想放弃对对象们所有权的时候,释放探测器会给他所有强引用的对象发出消息,当我们释放我们的假对象时,我们可以检查哪些探测器收到了这样的消息。便知道了哪些 block 是被强引用的。这样一来我们可以找到我们原始 block 所持有的真是的对象,如下图所示
# 自动化
员工在进行持续执行时,这个工具就会非常出色。自动化在客户端上是非常容易的,我们使用定时器来建立一个循环引用检测,用来周期性的扫描一部分内存去寻找循环引用,不过还是有点问题,我们第一次运行检测器的时候我们发现他不快速的扫描完整个内存空间,所以我们需要给他提供一个候选检测对象,为了做到这一点,我们建立了 FBAllocationTracker,他可以追踪任何一个 Nsobject 对象的创建和销毁,他同样可以在任何给定的时刻以最小的性能开销来获取任何类的实例对象。
在客户端上实现了这样的自动化操作意味着我们可以更加简单的在 NSTimer 上使用 FBRetainCycleDetector 和添加用了追踪实例的 FBAllocationTracker,现在让我们来看下后端到底发生了什么,循环引用可以有多个对象来组成,如果创建了许多的引用环并且有一个泄漏了那么事情将会变得非常复杂。如下如所示 A->B 就是一个坏的引用环导致了 A->B->C->D 和 A->B->C->E
遇到这样的问题就会给我们的 SDK 带来两个问题:
如果这两个环是由于一个不良引用引起我们就直接标记一处不良引用就可以了,不用标记两处,这样会给开发者一个错误信号。
如果这两个环是虽然由一个不良引用引起但是会导致两个不同的错误,那么我们就需要将这个两个循环引用都标记出来
因此我们需要建立一个循环引用群,我们通过下面这些启发写了一种算法。
- 1:把给定日期中所检测出的所有循环引用收集起来。
- 2:找到每个循环引用环中 Facebook 特定的类名。
- 3:找到每个环中最小的那个环。
- 4:把最小周期放到一个组中。
- 5:仅仅只像开发者报告最小的周期。
有了这些最后一部分就是找出谁可能会意外的引入一个循环引用,我们通过 “git/hg blame” 来做到这些。猜测这可能是导致出现问题的最新修改。最后一个提交代码的人会收到一个通知。整个系统可以如下展示
# 手工配置
尽管自动化帮助简化了寻找循环引用的过程,并减少了开发人员的工作,但是手动配置仍然很重呀。我们开发的另一款工具允许任何人查看应用程序的内存使用情况,甚至不用将手机插入电脑。FBMemoryProfiler 可以很容易地添加到任何应用中,让你手动配置你的工程,并在应用中运行循环引用检测,它可以通过使用 FBAllocationTracker 和 fbretaincycle 检测器来完成。
原文链接 https://code.facebook.com/posts/583946315094347/automatic-memory-leak-detection-on-ios/