iOS开发之---自动监测内存泄漏(译)

看到facebook的一套内存泄漏检测工具,感觉不错,想要查看原文可以点击这里,后续在去分析相关的开源工具

手机设备上的内存属于共享资源。应用不合理的使用它会导致内存耗尽,崩溃以及导致性能的大幅度降低。

Facebook的iOS客户端有许多特性,它们共享同一个内存空间,所以假如某个特定的特性消耗太多的内存,这会影响到整个应用,比如某个特性意外的出现内存泄漏。

当我们为一组对象分配内存,如果使用完没有释放相应的内存就会导致内存泄漏情况的发生,这意味着系统无法回收该内存来用于其它用途,最终导致内存耗尽。

在Facebook,许多工程师在不同的代码仓库上工作,这不可避免会有内存泄漏的情况发生,当出现这种情况时,我们需要快速的找到并修复它们。

已经有一些工具来辅助我们找到内存泄漏,不过需要大量的人工干预:

  1. 打开Xcode,选择build for profiling.
  2. 载入Instruments工具
  3. 使用app, 尝试尽可能多的重现场景和行为
  4. 查看instrument的leaks/memory
  5. 查找内存泄漏的根源
  6. 修复问题

这意味着每次都需要大量的手动操作,导致我们可能在开发周期内无法尽早的定位以及修复内存泄漏的问题。

如果该过程能够自动化,我们就能够在太多开发者干预的情况下快速找到内存泄漏。为此我们构建一系列的工具来自动化查找以及修复代码仓库中的一些问题,这些工具包括:FBRetainCycleDetector, FBAllocationTracker以及FBMemoryProfiler

Retain cycles(循环引用)

Objective-C使用引用计数来管理内存以及释放不使用的对象,任何一个对象可以持有(retain)其它对象,这样只要前面的对象需要使用它,该对象就会一直保存在内存,可以认为对象“拥有”其它对象。

大部分情况下这都工作的很好,但是假如两个对象最后互相“拥有”对方,直接或着更多通过其它对象间接的连接它们,这就会陷入一个僵局。这种持有引用的环就叫做循环引用。

retain cycle

循环引用会导致一系列的问题,最优的情况是对象一直在RAM中只占据一点点的空间。如果泄漏的对象只是做一些无关紧要的工作,那结果只是应用会少一些可使用的内存。最差的情况下,假如泄漏的内存超过了可使用的内存空间,应用可能会崩溃。

在手动性能分析的过程中,发现我们往往会有很多循环引用的情况,在开发的时候很容易出现循环引用,但却不容易在后面找到。Retain Cycle Detector可以帮助我们很容易的找到它们。

运行时检测循环引用

在Objective-C中查找循环引用类似于在一个有向无环图(directed acyclic graph)中查找环,节点就是对象,而边则是对象之间的引用(如果对象A retain 对象B,那么A到B之间就存在引用)。我们的Objective-C对象已经存在于我们的图当中,我们要做的就是使用深度优先方法历遍搜索它。(看到算法的重要性了。。)

depth-first search

虽然这只是个简单的抽象,但实际效果却不错。我们必须确保我们能够像节点一样使用对象,对于每个对象,我们能够获取它所引用的所有对象,这些引用可能是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对象所持有的强引用对象。

block

自动化

这些工具在工程师内部构建的时候能够持续自动的运行,确实闪闪发光。

在客户端的自动化很简单。我们定时的运行Retain Cycle Detector,定期的去扫描内存查找循环引用,不过这并不是这么一帆风顺。我们第一次运行Detector时,我们意识到它无法很快的扫描整个内存空间,我们需要首先提供一组候选对象来让Detector检测。

为了更有效的处理上述问题,我们创建了FBAllocationTracker。这个工具能够记录所有NSObject子类对象的创建和销毁,它能够以极小的性能代价在任意时刻快速获取任何类的对象实例。

客户端有了上述的自动化过程,意味着我们只需要在NSTimer上运行FBRetainCycleDetector,在配合FBAllocationTracker来抓取我们想要分析的实例即可。

现在让我们深入的看一下背后具体发生了什么。

循环引用可以包含任意数量的对象,当由于一个坏的连接(bad link)导致很多环的产生,事情就变的更复杂了。

bad link

在上面的环中,A->B就是一个坏的连接(bad link),它创建了两个环:A-B-C-D和A-B-C-E。这会有两个问题:

  • 如果由同一个坏的连接导致两个循环引用,我们不想用不同的标记来分别标记它们;
  • 我们不想给可能代表两个不同问题的两个循环引用一起标记,即便他们共享一条连接。

所以我们需要给循环引用定义类簇(clusters)。我们写了一个算法来找出这些问题,算法如下

1. 在给定的时间,收集所有的环;
2. 对于每个环,提取Facebook特定的类名称;
3. 对于每个环,找出该环包含的最小环;
4. 将每个环添加到由上面找到的最小环所代表的组中;
5. 只报告最小环;

最后要做的就是找到谁第一时间偶然地引入了循环引用,可以通过对环所涉及的代码进行'git/hg blame',我们猜测可能最新的代码导致了该问题,所以最后一个接触该代码的人会收到一个task来修复该问题。

整个过程如下图所示:

process

手动性能分析

虽然自动化能简化发现循环引用的过程,减少开发人员的消耗,但手动性能分析还是必不可少。我们创建了另外一个工具,允许任何人查看内存的使用情况,甚至不需要把手机插到电脑上。

FBMemoryProfiler可以很容易的添加到任意应用,让你在应用内部手动配置构建文件以及运行循环引用检测,该工具借助FBAllocationTracker和FBRetainCycleDetector实现该功能。

FBMemoryProfiler

![Uploading FBMemoryProfiler_405532.gif . . .]

代(Generations)

FBMemoryProfiler一个最大的特性是提供"代追踪(generation tracking)",类似苹果instruments的generation tracking,Generations是两个时间标记之间所有仍然活着的对象的快照。

使用FBMemoryProfiler的界面,我们可以标记一个generation,比如创建了三个对象;然后我们标记另一个generation并继续创建对象。第一个generation包含了我们三个最初的对象,如果有对象被释放了,那么该对象就会在第二个generation中被移除。

假如有一个重复的任务,我们认为可能有内存泄漏的情况发生,这时候Generation tracking就很有效了。比如导航进入一个View Controller然后退出,每次开始任务之前,我们标记一个generation,然后在每次generation标记之间进行调查,如有对象并不应该存活那么长,那我们可以在FBMemoryProfiler的界面上清楚的看到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容