本文整理自Google I/O 2011: Memory management for Android Apps的演讲,需自行到墙外查看。
从早期G1的192MB RAM开始,到现在动辄1G -2G RAM的设备,为单个App分配的内存从16MB到48MB甚至更多,但OOM从不曾离我们远去。这是因为大部分App中图片内容占据了50%甚至75%以上,而App内容的极大丰富,所需的图片越来越多,屏幕尺寸也越来越大分辨率也越来越高,所需的图片的大小也跟着往上涨,这在大屏手机和平板上尤其明显。而且还经常要兼容低版本的设备。所以Android的内存管理显得极为重要。
在这里我们主要讲两件事情:
1.Gingerbread和Honeycomb中的一些影响你使用内存的变化
-heap size
-GC
-bitmaps
2.理解heap的用途分配
-logs
-merory leaks
-Eclispe Memory Analyzer(MAT)
首先第一部分,我们都知道Android是个多任务操作系统,同时运行着很多程序,都需要分配内存,不可能为一个程序分配越来越多的内存以至于让整个系统都崩溃,因此heap的大小有个硬性的限制,跟设备相关,从发展来说也是越来越大,G1:16MB,Droid:24MB,Nexus One:32MB,Xoom:48MB,但是一旦超出了这个使用的范围,OOM便产生了。如果你正在开发一个应用,想知道设备的heap大小的限制是多少,比方说根据这个值来估算自己应用的缓存大小应该限制在什么样一个水平,你可以使用ActivityManager.getMemoryClass ()来获得一个单位为MB的整数值,一般来说最低不少于16MB,对于现在的设备而言这个值会越来越大,24MB,32MB,48MB甚至更大。
但是对于一些内存非常吃紧的比如图片浏览器等应用,在平板上所需的内存更大了。因此在Honeycomb之后AndroidManifest.xml增加了largeHeap的选项
android:largeHeap="true" ... >
这允许你的应用使用更多的heap,可以用ActivityManager.getLargeMemoryClass ()返回一个更大的可用heap size。但是这里要警告的是,千万不要因为你的应用报OOM了而使用这个选项,因为更大的heap size意味着更多的GC时间,意味着应用的性能越来越差,而且用户也会发现其他应用很有可能会内存不足。只有你需要使用很多的内存而且非常了解每一部分内存的用途,这些所需的内存都是不可或缺的,这个时候你才应该使用这个选项。
刚刚我们提到更大的heap size意味着更多的GC时间,下面我们来谈谈Garbage Collection。
如上图所示,GC会选择一些它了解还存活的对象作为内存遍历的根节点,比方说thread stack中的变量,JNI中的全局变量,zygote中的对象等,然后开始对heap进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。如下图蓝色部分。
因此也可以看出,更大的heap size需要遍历的对象更多,回收垃圾的时间更长,所以说使用largeHeap选项会导致更多的GC时间。
在Gingerbread之前,GC执行的时候整个应用会暂停下来执行全面的垃圾回收,因此有时候会看到应用卡顿的时间比较长,一般来说>100ms,对用户而言已经足以察觉出来。Gingerbread及以上的版本,GC做了很大的改进,基本上可以说是并发的执行,也不是执行完全的回收,只有在GC开始以及结束的时候会有非常短暂的停顿时间,一般来说<5ms,用户也不会察觉到。
GC_XXX表明是哪类GC以及触发GC的原因。几种GC类型:
- GC_CONCURRENT:这是因为你的heap内存占用开始往上涨了,为了避免heap内存满了而触发执行的。
- GC_FOR_MALLOC:这是由于concurrent gc没有及时执行完而你的应用又需要分配更多的内存,内存要满了,这个时候不得不停下来进行malloc gc。
- GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC,也就是上文提到的Bitmap Pixel Data之类的。
- GC_HPROF_DUMP_HEAP:这是当你做HPROF这样一个操作去创建一个HPROF profile的时候执行的。
- GC_EXPLICIT:这是由于你显式的调用了System.gc(),这是不提倡的,一般来说我们可以信任系统的GC。
freed 2049K表明在这次GC中回收了多少内存。
65% free 3571K/9991K是heap的一些统计数据,表明这次回收后65%的heap可用,存活的对象大小3571K,heap大小是9991K。
external 4703K/5261K是Native Memory的数据。放Bitmap Pixel Data或者是NIO Direct Buffer之类的。第一个数字表明Native Memory中已分配了多少内存,第二个值有点类似一个浮动的阀值,表明分配内存达到这个值系统就会触发一次GC进行内存回收。
paused 2ms 2ms表明GC暂停的时间。从这里你可以看到越大的heap size你需要暂停的时间越长。如果是concurrent gc你会看到2个时间一个开始一个结束,这时间是很短的,但如果是其他类型的GC,你很可能只会看到一个时间,而这个时间是相对比较长的。
通过Log可以对内存信息有个基本的了解,但这不足以了解什么对象在使用内存,在哪使用了内存。这时候你需要用Heap Dumps。一个Heap Dump基本上来说就是一个包含你heap中所有对象信息的二进制文件。你可以用DDMS来生成这个文件,点击DDMS中下图的那个按钮。
同时Heap Dumps也有对应的API,你想要在特定的时间点获取一份Heap Dump,使用android.os.Debug.dumpHprofData()。获取到的文件要转换成标准的HPROF格式,使用如下命令:hprof-conv orig.hprof converted.hprof。然后用MAT或者jhat进行分析。
在讲MAT之前先讲下Memory Leaks。要清楚GC并不能防止Memory Leaks,所谓Memory Leaks就是引用到了已经没用的对象从而让这些对象避免了被GC回收,跟C/C++中的概念并不一样。容易导致内存泄漏的是一些Activity,Context,View,Drawable之类的引用,和一些非静态的内部类比方说Runnable之类的以及一些Caches。比如你旋转屏幕的时候在新的方向上产生一个新的Activity,如果有变量引用到旧的Activity就会导致其无法被GC,造成Memory Leaks。
通常通过上面介绍的Log信息,只要已用memory一直处于上升的情形而不回落,便大致能了解到应用存在Memory Leaks。不过MAT这类工具可以帮助你更好的对memory进行分析。使用MAT之前有2个概念是要掌握的:Shallow heap和Retained heap。Shallow heap表示对象本身所占内存大小,一个内存大小100bytes的对象Shallow heap就是100bytes。Retained heap表示通过回收这一个对象总共能回收的内存,比方说一个100bytes的对象还直接或者间接地持有了另外3个100bytes的对象引用,回收这个对象的时候如果另外3个对象没有其他引用也能被回收掉的时候,Retained heap就是400bytes。
1.明确调用System.gc();
这种内存回收会有一定的作用,但是请不要太期待。
System.gc()在调用时程序是处于阻塞状态,系统考虑到程序运行的流畅性,会分批次进行垃圾回收,每次只回收一部分,我曾经遇到过的状况是只调用了一次System.gc(),然后看到LOG显示垃圾的确回收了,但很快Out of memory便接踵而至,看来是垃圾还没有回收完,系统内存就溢出了,最后采用了一个无奈的方法,连续调用10次甚至20次System.gc(),问题便解决了。
2.图片处理完成后回收内存。
请在调用BitMap进行图片处理后进行内存回收。
bitmap.recycle();
这样会把刚刚用过的图片占用的内存释放。
3.图片读入内存时指定大小。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
bitmap = BitmapFactory.decodeFile(path, options); // 此时返回bm为空
options.inJustDecodeBounds = false;
// 缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
int be = (int) (options.outHeight / (float) heigh);
if (be <= 0)
be = 1;
options.inSampleSize = be;
// 重新读入图片,注意此时已经把options.inJustDecodeBounds 设回false了
bitmap = BitmapFactory.decodeFile(path, options);
4.Drawable和Bitmap的区别
对比项 |
Bitmap |
Drawable |
显示清晰度 |
相同 |
相同 |
占用内存 |
大 |
小 |
支持缩放 |
是 |
是 |
支持色相色差调整 |
是 |
否 |
支持旋转 |
是 |
是 |
支持透明色 |
是 |
是 |
绘制速度 |
慢 |
快 |
支持像素操作 |
是 |
否 |
Drawable在内存占用和绘制速度这两个非常关键的点上胜过Bitmap
5.问题排查
从以下几点进行分析和排查:
1 |
所有不再使用的类对象你置空了吗? |
2 |
被置空的那些对象里的那些申请了内存空间的子对象(比如Bitmap对象)你释放资源以及置空了吗? |
3 |
置空对象后有没有调用System.gc()? |
6.Android框架,每一个进程都有内存限制,可以采用以下方法来躲开这个问题
每一个程序可以通过android.os.Debug.getNativeHeapAllocatedSize()查看进程可以使用的内存。
一种方法是从本机代码分配内存。使用NDK的(本地开发工具包)和JNI,它可以从C级(如的malloc / free或新/删除),这种分配的内存分配,不计入内存的限制。
另一种方法,效果很好,图像,是使用OpenGL纹理 - 纹理内存同样不被计入内存的限制
据说可以达到300M的内存使用,哪位朋友测试了可以告诉我,这个方法的可行性。