用户使用移动产品时,App 崩溃现象会严重影响用户体验。而Android 低端机器正是崩溃的重灾区,经常会抛出OOM(out of memory)异常。究其原因多数是App 使用过程中发生内存泄漏,因此为了提高App 的稳定性,测试过程中需要反复操作尝试定位此类问题。本文将针对Android 设备崩溃产生的常见场景和定位工具进行阐述。
一、Android 内存泄漏的常见场景
Android 程序多数基于Java 语言开发。Android 系统在运行时,会给每个应用分配一个Dalvik 虚拟机作为进程运行的基本单位。与Java 中的JVM 虚拟机类似,Dalvik 也实现了虚拟机中的垃圾回收功能,以保证资源的合理使用。内存泄漏行为就是指App 使用过程中没有按正常逻辑被GC 回收的对象。因此Java 程序常见的内存泄漏场景在Android 中同样适用,本文主要描述Android 特有的内存泄漏现象的场景,从而方便测试重现此类问题。
- 页面的销毁与建立
场景:多次关闭、打开同一页面后,触发GC,内存增长,可能存在崩溃。
原因:Android 应用页面之间频繁的切换和绘制,可能会存在某些页面对象无法被GC回收,大量的相关对象停滞在Heap 区,从而导致内存泄漏。 举例来说为了加快页面渲染速度,经常会使用静态变量缓存页面背景。但是如果没有正确处理,在SDK-10 以下的系统上,会导致页面背景对象持有控件对象的引用,使整个Activity 都无法被GC 回收,多次重开同一个页面后,内存就会飙升了,而SDK-15 以上已经通过WeakReference 解决了这一问题。
- 资源对象
场景:
a. Android 中常使用广播broadcast 来监听系统事件,如果注册对象后,未关闭注册,假如监听事件是很频繁的操作(如锁屏等),容易造成OOM。
b. 数据库游标,文件IO 流等没有及时关闭。这种场景需要在大量操作后,才会有明显的内存异常现象,很容易被忽略。
原因:资源对象未及时回收或关闭,从而导致内存泄漏
- 图片
场景:大量图片的加载和切换
原因:图片资源相对于其他资源都较大,虽然系统能够确认Bitmap 分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过堆的限制,直接导致OOM,而崩溃产生的原因有以下2 点:
a. 使用完成后,没有及时的销毁。
b. 总是保留原图大小的对象。如果图片实际的显示区域较小,可以设置合适的采用率保存Bitmap 对象,减少大图的内存占用。
- 横竖屏切换
场景:横竖屏切换
原因:横竖屏切换时,会对页面元素进行重绘,如果处理不当,Handler、Thread 等的数量会随着页面的重建次数增加而增加,内存泄漏问题就很容易被放大。
- 列表的滑动与重载
场景:App 中很多内容都会通过列表的形式来展示,包括图片的浏览等,列表滑动和重载的过程可能会导致内存泄漏,列表中包含大量图片时,不必要的内存开支会导致内存资源不足。
原因:列表在滑动时,不可见的item 对象会被回收,而被用来构造新的item,若在构造每一个item 时,没有使用缓存的convertView,会造成内存垃圾。当快速滑动时,容易给垃圾回收较大压力,如果GC 来不及清理资源,虚拟机不得不分配更多内存来使程序正常运行。
二、Android 内存泄漏分析方法
既然了解了Android 内存泄漏可能出现的场景,那么接下来介绍具体的工具来辅助我们进行内存泄漏分析。Android tools 中自带的DDMS 就是一个很实用的内存检测工具,可以动态查看进程的heap 信息,跟踪内存分配情况以及线程状态,通过与内存分析工具MAT 的结合,可以方便的监控分析潜在内存问题。
- DDMS 跟踪进程的heap信息
DDMS 工具可以在eclipse adt 插件或SDK tools 中打开。通过DDMS 的Devices 面板,可以查看设备运行中的进程,选中进程后,点击工具栏的Update Heap,即可开始该进程的heap 信息监控,如图1。
(Tips:如果使用安卓真机,那么只能看到本地Eclipse 开启Debug 开关编译到真机上的App 进程,要想看到所有的进程,那么刷成开发机吧。)
上文场景中,提到了Bitmap 使用不当时,容易引发内存泄漏,这里就以某产品的漫画功能作为实例。漫画页面占用内存的主要对象即为Bitmap !在DDMS 的Heap 面板里,通过Cause GC 可以触发GC,实时查看到当前的堆情况。图2 展示了一次GC 后的Heap 情况,系统分配的堆控件Heap Size 和应用程序实际占用的内存Allocated 已经增大到异常数量级,其中byte array 占用了大部分的空间。如果观察动态数据,可以看到在漫画翻页的过程中,每次GC 后,堆的大小一直增加,没有明显的回落,因此有内存泄漏的可能性。
当然图2 中Heap 的泄漏可能性比较明显,测试中也可以尝试通过下面两种方法来起到放大泄漏问题的效果:
-
在不同App 间多次切换。可以通过Android 的Home 键,或者历史进程键来切换App,使App 在不同的生命周期内不断变化,通过不断的唤醒onResume 和onPause 暂停页面时,页面对象处理不当引发的泄漏问题,会进一步暴露出来。
-
多次切换横竖屏。横竖屏切换经常可以引发App 中Activity、Context、View 等对象的泄漏,因为横竖屏切换会使页面重新加载。如果App 在其他代码中保持了对上述对象中某一个的引用,系统GC 将无法回收重载过程中本应该回收的资源,Heap 会有明显的增加。
既然监控发现了可能的内存泄漏问题,可以借助hprof 文件来分析泄漏的根源所在。回到Devices 面板,点击Dump HPROF file,即可导出当前GC 后Heap 的详细信息。当然并不是所有内存泄漏都能由OOM 或者Heap Size 的变化来检测,需要进一步分析Heap 的具体使用情况才能明确问题所在。
- MAT 分析内存泄漏原因
HPROF 文件可以使用Java 常用的内存分析工具MAT 来分析,其中Dominator Tree 和Histogram 应该是最有用的工具。在图3 的dominator_tree 列表中,com.netease.x.x.x 直接或间接引用到的Retained Heap 相当大,再逐层查看,发现存在30 多个Bitmap 对象没有被GC 回收。
通过Histogram 也可以看到byte[] 占用了大部分的Shallow Heap(这是由于Android3.1之后,Bitmap 像素数据的内存分配在Dalvik Heap 中), 如图4。选择Path To GC Roots->exclude weak references,可以看到对象的逐层引用关系,找到内存泄漏的根源。
在移动产品Android 端的测试过程中对Heap 和GC 信息的监控是必要的,通过这些宏观的监测数据,发现疑现象,深入分析找到内存泄漏的根源,对于解决崩溃现象,提高测试产品的稳定性,有很大的帮助。