使用Instrument调试内存问题


updated @2014/01/10 补充了WWDC2013:session 410的内容 updated @2018/08/12 : 补充了WWDC2018:session416

Tracking Memory Leak Using Allocation

调试内存最基本的手段是使用Instrument的Allocation,Allocation可以帮助找到当前内存中活跃的object,通常可以用来观察对象有没有释放。假设我们有一个10个cell的tableview列表,我们运行Allocation,得到的状态如下:

debug-1

这个时候我们可以通过筛选类名前缀,来看当前存活的class,例如上图中,我的类名为ET开头,上面所有ET开头的类都是当前活跃在内存中的,我们可以看到有10个cell,20个item。此时我们退出这个界面,那么上面的对象,除了单例对象外应该全部释放掉。如果发现有没有释放的类名,则就要引起警惕了,要仔细排查没有dealloc的原因。

通常情况下,原因很好发现。但是,当仔细review了代码后,仍然找不到问题的时候,就要考虑是不是系统对象引用了你的对象。这种对象由于不是以自己的类名前缀开通,通常很难发现。假如,当我们退出这个界面时,发现10个cell都没释放,然后我们仔细review了代码,发现没有ET开头的对象引用它。这时候就要看看系统对象了,对于cell,如果没有外部引用,cell是会被tableview引用的,这时候我们需要搜索UITableView,如下图:

debug-2

这时候我们发现,有一个ETLibDemo创建的UITableView,就是这个UITableView没释放,我们后面可以继续排查为什么tableview没有释放。

Allocation除了帮助我们发现当前heap上活跃的object外,还有一个很重要的功能是HeapShot。WWDC关于memory的session中,曾反复提到使用HeapShot来detect memory growth:

“Memory shouldn’t grow without bound when repeating operation”。意思是当重复相同操作时,内存不应该有增长。例如:push,pop同一controller,滚动tableview等。实际中,我们如何来验证呢?如下图:

Jayson updated the picture @2014/01/10

debug-3

图中,A,B,C,D,E五个点为HeapShot的地方:

debug-4

  • A点:启动程序
  • B点:第一次进入页面X,然后返回。内存出现一个峰值,我们发现内存开始增长(黄色的斜线),并没有回落到进入第一个页面之前的高度,差值为:174.89kb。这很正常,因为我们可能在这个过程中创建了一些单例变量,初始化cache等,因此这个阶段通常为warm up。
  • C点:第二次进入页面X,然后返回。我们发现内存回到了和进入X之前相同的高度,差值为10kb。理论上来说,差值为0才说明是没有写漏,但我个人认为,10kb完全可以归结为内存碎片,这说明,页面X没有泄露内存。此时我们可以将红线设为baseline。也就是说,当再次进入页面X的时候,退出时,应该回到红线的位置。
  • D点:第一次进入页面Y,然后返回。我们发现内存暴涨了800多K,这是由于页面Y在大量加载图片,图片被缓存到内存中,因此在页面返回的时候,没有回到baseline。
  • E点:第二次进入页面Y,然后返回。我们发现基本回到了D点,但是略有偏高,差值在80K。这里就要稍微警惕下,如果说我们在页面Y中大量malloc,free内存,那么有可能产生大量的内存碎片。但是如果我们没有大量分配内存,就要警惕有object没被释放而造成泄露了。

实践中,通过HeapShot来分析controller的内存管理还是很方便的。

Leaks Usually Doesn't Help

Instrument的leak选项,通常是大家都愿意点的,但实际中,我发现,作用其实不大,因为Leak只检测unreferenced object,即没有人引用的object,但还没有被释放掉。而一些常见的leak,比如retain-cycle等,leak是检测不到的。

Tracking Deeper In VM Region

新的Allocation还提供一个feature是”Only Track VM Allocation”。It’s a very cool feature!顾名思义,我们可以通过Allocation看到整个app在VM上的memory footprint!这意味着任何一个细小的内存动作都能被捕捉到,只要有我们有足够的耐心,可以逐行分析内存的使用情况。这个feature配合VM Tracker可以更有效,更快速的定位内存问题。

下面我们来实际演练一下:我们首先启动app,这个时候,什么都没做,我们看看VM Region和VM Tracker分别有什么变化:

右边是VM Tracker,我们看到了mapped-file:虚拟内存到物理RAM的映射表,看到了__TEXT,__DATA,还有Dirty Memory…我们看到Dirty Memory只占8%,这是由于app刚刚启动,产生在Heap上的空间还不大,Dirty Memory的比重还不大。

然后我们创建100个imageView,并让他们加载存在硬盘上的图片

for (int i=0; i<100; i++) 
{        
   ETImageView* imgv = [[ETImageView alloc]initWithFrame:CGRectMake(i%4 * 80, i/4 * 80, 80, 80)];
   imgv.enableFadeInAnimation = YES;
   [_imgList addObject:imgv];
   
   [self.scrollView addSubview:imgv];
 }

然后我们在debug:

debug-8

我们可以点开里面的每一项仔细阅读分析,其中Mapped Files主要是系统的一些公共类库,这部分memory属于shared memory,这部分内存通常与应用程序无关。LARGE_MALLOC这部分内存主要集中在将图片File变成NSData:

debug-10

然后是大量的ImageIO操作,通过ImageIO的stack trace我们发现一个很有趣的事情: UIImageView在显示image的时候,实际上对image做了copy操作,然后重新create新的image,原因也很好理解,因为原图片的大小尺寸一般和imageview的尺寸不一致,即使一致的情况下,也会有pixel alignment的问题,因此,imageview要对原图进行down sampling或者up sampling,pixel alignment等操作,来最终适应imageview的尺寸。但是这回导致耗费大量的内存,性能也会受到一定的影响,因此在做图片优化的时候,尽可能的让这部分时间缩短。

debug-9

VM Region或者VM Tracker是一个很强大的内存分析工具,通过它可以看出一些iOS的源码设计。

Memory Graph

XCode8 引入内存可视化工具,XCode 10将这个功能进行了增强,可以通过命令行工具生成内存快照

Resources