作者:Tim Bray
Dalvik虚拟机支持垃圾收集,但是这不意味着你可以不用关心内存管理。你应该格外注意移动设备的内存使用,在上面内存空间是受到限制的。在这篇文章里面,我们来看看Android SDK里面的一些内存剖析工具(profiling tools)是如何帮助我们修整应用程序的内存使用。
一些内存使用问题是很明显的,例如,如果在每次用户触摸屏幕的时候应用程序有内存泄露,将会有可能触发OutOfMemoryError,最终程序崩溃。另外一些问题却很微妙,也许只是降低应用程序和整个系统的性能(当高频率和长时间地运行垃圾收集器的时候)。
必要的工具:
Android SDK提供了2个主要的剖析应用程序内存使用情况的工具:DDMS里的一个分页Allocation Tracker和heap dumps。Allocation Tracker是很有用的,特别是当你想得到程序在一定的时间里内存的分配情况的一种感性认识的时候。但是它不能给你任何关于程序heap总体情况的任何信息。关于Allocation Tracker的更多信息,请看文章Tracking Memory Allocations 。文章剩下的内容将把重点放在heap dumps,它是更强大的内存分析工具。
一个heap dump就是一个程序heap的快照,它保存为一种叫做HPROF的二进制格式。Dalvik用的也是类似的格式,但是不完全一样。这里是Java的HPROF工具 。有很多方法去生成一个运行时应用程序的heap dump。其中一种就是使用在DDMS里边的Dump HPROF file按钮。如果想产生更精确的dump数据,可以在程序中使用android.os.Debug.dumpHprofData() 方法。
分析heap dump,你可以使用一些标准的工具比如jhat 或者Eclipse Memory Analyzer(MAT) 。不过,首先你需要把.hprof文件从Dalvik格式转换成J2SE HPROF格式。你可以使用Android SDK提供的hprof-conv工具。例如:
hprof-conv dump.hprof converted-dump.hprof
调试一个内存泄露实例:
在Dalvik运行时里边,程序员不能显式地分配和释放内存,所以这里的内存泄露跟c和c++里面的不同。在你的代码里边,内存泄露就是你保留了一个并不再需要的类对象的引用。有时候仅仅一个引用就会阻碍gc对一大堆对象的回收。
我们来过一个实际的例子,Android SDK里面提供的范例程序Honeycomb Gallery sample app 。它是一个photo gallery程序,用来演示一些新的Honeycomb API的使用。(下载和编译这些代码,请看这些命令 。)我们会有意地加入一个内存泄露在程序里边,然后来演示如何调试它。
想象一下我们想修改程序让它从网络下载图片。为了让它更具备灵活性,我们可以考虑实现一个缓存,保存最近查看过的图片。我们可以对ContentFragment.java做一些小的修改来达到这个目的。在class顶部,我们增加一个新的静态变量:
private static HashMap
这里就是我们保存缓存的地方。现在我们可以修改updateContentAndRecycleBitmap()方法,让它在下载之前先查看是否数据已经存在,如果不存在就去下载,然后添加数据到缓存。
void updateContentAndRecycleBitmap(int category, int position) { if (mCurrentActionMode != null) { mCurrentActionMode.finish(); } // Get the bitmap that needs to be drawn and update the ImageView. // Check if the Bitmap is already in the cache String bitmapId = "" + category + "." + position; mBitmap = sBitmapCache.get(bitmapId); if (mBitmap == null) { // It's not in the cache, so load the Bitmap and add it to the cache. // DANGER! We add items to this cache without ever removing any. mBitmap = Directory.getCategory(category).getEntry(position) .getBitmap(getResources()); sBitmapCache.put(bitmapId, mBitmap); } ((ImageView) getView().findViewById(R.id.image)).setImageBitmap(mBitmap); }
我已经在这里故意引入了一个内存泄露的问题:我们把图片加入了缓存但是从来没有移除他们。在真实的应用里,我们可以会用某种方法来限制缓存的大小。
在DDMS里检查heap的使用情况
Dalvik Debug Monitor Server(DDMS)是主要的Android调试工具之一,也是ADT Eclipse plug-in 的一部分,独立的程序版本也可以在Android SDK的根目录下的tools/下面找到。关于DDMS更多的信息,请参考使用DDMS 。
我们来使用DDMS检查这个应用的heap使用情况。你可以使用下面的两种方法启动DDMS:
ddms
(or ./ddms
on Mac/Linux) in the tools/
directory
在左边的面板选择进程com.example.android.hcgallery,然后在工具条上边点击Show heap updates按钮。这个时候切换到DDMS的VM Heap分页。它会显示每次gc后heap内存的一些基本数据。要看第一次gc后的数据内容,点击Cause GC按钮:
我们可以看到现在的值(Allocated列)是有一些超过8MB。现在滑动相片,这时看到数据在增大。因为只有仅仅13个相片在程序里边,所以泄露的内存只有这么大。在某种程度上来说,这时最坏的一种内存泄露,因为我们没法得到OutOfMemoryError来提醒我们说现在内存溢出了。
生成heap dump
我们现在使用heap dump来追踪这个问题。点击DDMS工具条上面的Dump HPROF文件按钮,选择文件存储位置,然后在运行hprof-conv。在这个例子里我们使用独立的MAT版本(版本1.0.1),从MAT站点下载 。
如果你使用ADT(它包含DDMS的插件)同时也在eclipse里面安装了MAT,点击“dump HPROF”按钮将会自动地做转换(用hprof-conv)同时会在eclipse里面打开转换后的hprof文件(它其实用MAT打开)。
启动MAT然后加载刚才我们生成的HPROF文件。MAT是一个强大的工具,讲述它所有的特性超出了本文的范围,所以我只想演示一种你可以用来检测泄露的方法:直方图(Histogram)视图。它显示了一个可以排序的类实例的列表,内容包括:shallow heap(所有实例的内存使用总和),或者retained heap(所有类实例被分配的内存总和,里面也包括他们所有引用的对象)。
如果我们按照shallow heap排序,我们可以看到byte[]实例在顶端。自从Android3.0(Honeycomb),Bitmap的像素数据被存储在byte数组里(之前是被存储在Dalvik的heap里),所以基于这个对象的大小来判断,不用说它一定是我们泄露掉的bitmap。
右击byte[]类然后选择List Objects > with incoming references。它会生成一个heap上的所有byte数组的列表,在列表里,我们可以按照Shallow Heap的使用情况来排序。
选择并展开一个比较大的对象,它将展示从根到这个对象的路径--就是一条保证对象有效的链条。注意看,这个就是我们的bitmap缓存!
MAT不会明确告诉我们这就是泄露,因为它也不知道这个东西是不是程序还需要的,只有程序员知道。在这个案例里面,缓存使用的大量的内存会影响到后面的应用程序,所以我们可以考虑限制缓存的大小。
使用MAT比较heap dumps
调试内存泄露时,有时候适时比较2个地方的heap状态是很有用的。这时你就需要生成2个单独的HPROF文件(不要忘了转换格式)。下面是一些关于如何在MAT里比较2个heap dumps的内容(有一点复杂):
总结
这本篇文章里面,我展示了Allocation Tracker和heap dumps是如何给你一种对程序内存使用的感性认识。我也展示了Eclipse Memory Analyzer(MAT)可以帮助追逐我们程序里面的内存泄露问题。MAT是一个强大的工具,我也仅仅触碰了一些皮毛,如果你想学习更多内容,我建议读一些下面的文章: