[译文]在iOS上自动检测内存泄露

l147852369 8年前
   <p>手机设备的内存是一个共享资源。应用程序可能会不当的耗尽内存、崩溃,或者遭遇大幅度的性能降低。</p>    <p>非死book iOS客户端有很多功能,并且它们共享同一块内存空间。如果任何特定的功能消耗过多的内存,就会影响到整个应用程序。这是可能发生的,比如,这个功能导致了内存泄露。</p>    <p>当我们分配了一块内存,并设置了对象之后,如果在使用完了之后忘记释放,这就会发生内存泄露。这意味着系统是无法回收内存并交予他人使用,这也最终意味着我们的内存将会逐渐耗尽。</p>    <p>在非死book,我们有很多工程师在代码库的不同部分上工作。这不可避免的会发生内存泄露。当发生内存泄露之后,我们需要尽快找到并修复它们。</p>    <p>一些工具已经可以找到内存泄露,但是它们需要大量的人工干预:</p>    <ol>     <li>打开Xcode,给性能分析(profiling)编译。</li>     <li>载入Instruments。</li>     <li>使用应用程序,尝试尽可能多的重现场景和行为。</li>     <li>查看内存和泄露。</li>     <li>追踪内存泄露的根源。</li>     <li>修复这个问题。</li>    </ol>    <p>这意味着每次都需要重复大量的手动操作。为此,在我们的开发周期上,我们可能无法尽可能早的定位和修复内存泄露问题。</p>    <p>自动化可以在不需要更多开发者的情况下,更快的找到内存泄露。为了解决这个问题,我们做了一套工具来自动化的处理和修复我们代码库中的一些问题。今天,我们很兴奋的发布这些工具: <a href="/misc/goto?guid=4959670817135161254" rel="nofollow,noindex">FBRetainCycleDetector</a> 、 <a href="/misc/goto?guid=4959670817233880133" rel="nofollow,noindex">FBAllocationTracker</a> 、 <a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 。</p>    <h2>循环引用(Retain cycles)</h2>    <p>Objective-C 使用引用计数去管理内存和释放不使用的对象。内存中的任何一个对象都可以持有( retain )其他的对象,只要前面的对象需要它,对象就会一直保持在内存中。查看这个的一个方法是这个对象持有其他的对象。</p>    <p>在大部分时间内,这都工作的很好,但当两个对象互相持有的时候,这就会陷入一个僵局。直接,或者更常见的,通过间接对象连接它们。这种持有引用的环我们叫做循环引用(Retain cycles)。</p>    <p><img src="https://simg.open-open.com/show/626c9eb9d90e165ef115c037ba0af5ce.png"></p>    <p>循环引用会导致一些列的问题。最好的情况下,对象只会在内存中占有一点点位置。如果这个被泄露的对象正积极地做个一些不平凡的事情,应用程序的其他部分就只会有更少的内存。最坏的情况下,如果泄露导致使用超出可用内存的容量,那么,应用程序会崩溃。</p>    <p>在手动性能分析期间,我们发现,我们往往有一些循环引用。我们很容易引起内存泄露,但是很难找到它们。循环引用检测器可以很容易的找到它们。</p>    <h2>在运行时检测循环引用</h2>    <p>在 Objective-C 中找循环引用类似于在一个有向无环图(directed acyclic graph)中找环, 而节点就是对象,边就是对象之间的引用(如果对象A持有对象B,那么,A到B之间就存在着引用)。我们的 Objective-C 对象已经在我们的图中,我们要做的就是用深度优先搜索遍历它。</p>    <p>这有点抽象,但效果很好。我们必须确保我们可以像节点一样使用对象,对于每个对象,我们都可以获取到它引用的所有对象。这些引用可能是 weak ,也可能是 strong 。只有强引用才会导致循环引用。对于每个对象来说,我们需要知道如何找出这些引用。</p>    <p>幸运的是,Objective-C提供了一个强有力的、内省的运行时库。这让我们在图中可以有足够的数据去挖掘。</p>    <p>图中的节点可以是对象,也可以是Block。让我们来分别讨论一下。</p>    <h3>对象</h3>    <p>运行时有很多工具允许我们对对象进行内省。</p>    <p>我们要做的第一件事是获取对象的实例变量的布局(ivar layout)。</p>    <pre>  const char *class_getIvarLayout(Class cls);  const char *class_getWeakIvarLayout(Class cls);  </pre>    <p>对于对象,实例变量的布局描述了我们在哪儿可以找到其他对象的引用。它会提供给我们一个索引(index),这代表我们需要在对象地址上添加一个偏移量(offset),就可以得到它所引用的对象的地址。运行时也允许我们获取“弱引用实例变量布局(weak ivar layout)”。</p>    <p>这也部分支持Objective-C++。在Objective-C++中,我们可以在结构体中定义对象,但是这不会在实例变量布局中获取到。运行时提供了“类型编码(type encoding)”来处理这个问题。对于每一个实例变量来说,类型编码描述了变量是如何结构化的。如果这是一个结构体,它会描述它包含了哪些字段和类型。我们计算出它们的偏移量,在图中,找出它们所指向的对象。</p>    <p>也有一些边缘条件我们不会深入。大部分是一些不同的集合,我们不得不列举它们去获得它们持有的对象,这可能会导致一些副作用。</p>    <h2>Block</h2>    <p>Block和对象有一点不一样。运行时不会让我们很轻易的看到它们的布局,但是我们仍然可以猜测。</p>    <p>在处理Block的时候,我们可以使用 Mike Ash 在他的项目 <a href="/misc/goto?guid=4959670817415554240" rel="nofollow,noindex">Circle</a> (第一时间启发 <a href="/misc/goto?guid=4959670817135161254" rel="nofollow,noindex">FBRetainCycleDetector</a> 的项目)中提出的想法。</p>    <p>我们可以使用的是 <a href="/misc/goto?guid=4959670817521725983" rel="nofollow,noindex">ABI</a> (application binary interface for blocks - 应用程序二进制Block接口)。它描述了Block在内存中的样子。如果我们知道我们在处理的引用是一个Block,我们可以把它丢在一个假的结构体中来模仿Block。在放到一个C语言的结构体之后,我们可以知道Block所持有的对象。不幸的是,我们不知道这些引用是强引用还是弱引用。</p>    <p>为了解决这个问题,我们使用了一个黑盒技术。我们创建一个对象来假扮我们想要调查的Block。因为我们知道Block的接口,我们知道在哪可以找到Block持有的引用。我们伪造的对象将会拥有“释放检测(release detectors)”来代替这些引用。释放检测器是一些很小的对象,它们会观察发送给它们的释放消息。当持有者想要放弃它的持有的时候,这些消息会发送给强引用对象。当我们释放我们伪造的对象的时候,我们可以检测哪些检测器接收到了这些消息。只要知道哪些索引在伪造的对象的检测器中,我们就可以找到原来Block中实际持有的对象。</p>    <p><img src="https://simg.open-open.com/show/2c0dab71951eb2748135edcd9396f94d.png"></p>    <h2>自动化</h2>    <p>让这工具真正闪光的是,在工程师内部构建的时候,它会连续的、自动的运行。</p>    <p>客户端部分自动化是简单的。我们在定时器上运行循环引用检测器,定期扫描内存去寻找循环引用,虽然这不是完全没有问题。当我们第一次运行分析器的时候,我们意识到它不足以很快的扫描整个内存空间。当它开始检测的时候,我们需要给它提供一组候选对象。</p>    <p>为了更有效的解决这个问题,我们开发了 <a href="/misc/goto?guid=4959670817233880133" rel="nofollow,noindex">FBAllocationTracker</a> 。这个工具会主动跟踪 NSObject 子类的创建和释放。它可以以一个很小的性能开销来获取任何类的任何实例。</p>    <p>对于客户端的自动化,只要在 NSTimer 上使用 <a href="/misc/goto?guid=4959670817135161254" rel="nofollow,noindex">FBRetainCycleDetector</a> ,再用 <a href="/misc/goto?guid=4959670817233880133" rel="nofollow,noindex">FBAllocationTracker</a> 来抓取实例来配合跟踪就行。</p>    <p>现在,让我们来仔细看看后台会发生什么。</p>    <p>循环引用可以包含任何数量的对象。一个坏的连接会导致很多环的时候,这就复杂了。</p>    <p><img src="https://simg.open-open.com/show/3a5fd8c1abc6eae637ca48c07a98906f.png"></p>    <p>在环中,A→B是一个坏连接,创建了两个环:A-B-C-D 和 A-B-C-E。</p>    <p>这有两个问题:</p>    <ol>     <li>我们不想给一个坏连接导致的两个循环引用分别标记。</li>     <li>我们不想给可能代表两个问题的两个循环引用一起标记,即使它们共享一个连接。</li>    </ol>    <p>所以我们需要给循环引用定义簇组(clusters),鉴于这些启发,我们写了个算法来找到这些问题。</p>    <ol>     <li>在给定的时间收集所有的环。</li>     <li>对于每一个环,提取非死book特定的类名。</li>     <li>对于每一个环,找到包含在环内的被报告的最小的环。</li>     <li>依据上面的最小环,将环添加到组中。</li>     <li>只报告最小环。</li>    </ol>    <p>最后一部分是找出谁第一时间偶然引入了循环引用。我们可以通过环中的”git/hg责任”的部分代码来猜测最近的变化所导致的问题。最后一个接触这个代码的人将会收到修复代码的任务。</p>    <p>整个系统如下:</p>    <p><img src="https://simg.open-open.com/show/70558f55ef5f041cb16827c78174264a.png"></p>    <h2>手动性能分析</h2>    <p>虽然自动化有助于简化发现循环引用的过程,降低人员的消耗,手动性能分析依然有它的用武之地。我们创建的另一个工具允许任何人查看内存使用,甚至不需要把他的手机插到电脑上。</p>    <p><a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 可以很容易的添加到任何应用程序,可以让你手动配置构建文件,可以让你在应用程序内运行循环应用检测。它会借用 <a href="/misc/goto?guid=4959670817233880133" rel="nofollow,noindex">FBAllocationTracker</a> 和 <a href="/misc/goto?guid=4959670817135161254" rel="nofollow,noindex">FBRetainCycleDetector</a> 来实现此功能。</p>    <h2>生成(Generations)</h2>    <p><a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 的一个很伟大的特性是“生成追踪(generation tracking)”,类似于苹果的Instruments的生成追踪。生成只是简单的在两次标记之间拍摄所有仍然活着的对象的快照。</p>    <p>使用 <a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 的界面,我们可以标记生成,例如,分配三个对象。然后我们标记另一个生成,之后继续分配对象。第一个生成包含我们一开始的三个对象。如果任意一个对象被释放了,它会从我们第二个生成中移除。</p>    <p><img src="https://simg.open-open.com/show/16ad2905401e680b749c8308a7cc0320.png"></p>    <p>当我们有一个重复的任务,我们认为可能会内存泄露的时候,生成追踪是很有用的,例如,导航View Controller的进出。在每次开始我们的任务的时候,我们标记一个生成,然后,对之后的每个生成进行调查。如果一个对象不应该活这么长时间,我们可以在 <a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 界面清楚地看到。</p>    <h2>Check Out</h2>    <p>无论你的应用程序是大是小,功能是多是少,好的工程师都应有好的内存管理。在这些工具的帮助之下,我们可以更简单的找到并修复这些内存泄露,所以我们可以花费更少的时间去手动处理,这样就可以有更多的时间去编写更好的代码。我们也希望你可以发现它们是有用的。在Github上check out下来吧。 <a href="/misc/goto?guid=4959670817135161254" rel="nofollow,noindex">FBRetainCycleDetector</a> , <a href="/misc/goto?guid=4959670817233880133" rel="nofollow,noindex">FBAllocationTracker</a> 和 <a href="/misc/goto?guid=4959670817329786269" rel="nofollow,noindex">FBMemoryProfiler</a> 。</p>    <p>来自: <a href="/misc/goto?guid=4959670817729015962" rel="nofollow">http://ifujun.com/yi-wen-zai-iosshang-zi-dong-jian-ce-nei-cun-xie-lu/</a></p>    <p>原文链接: <a href="/misc/goto?guid=4959670817817484886" rel="nofollow,noindex">https://code.非死book.com/posts/583946315094347/automatic-memory-leak-detection-on-ios/</a></p>