Android内存泄漏定位与解决


问题现象

反复点击被测试的Android App的toolbar界面,然后返回再点击。在此重复过程中,发现到一定次数时,页面打开速度变慢,有时达到5s,十分影响用户体验。该问题涉及app所采用的webview框架的所有界面,影响面大。

初步分析

加载界面慢,一般有2种情况:一是每次都慢,那么与该界面的布局(layout)效率或业务逻辑(主线程动画/同步的业务逻辑)关系更大;另外一种是重复打开几次,会遇到一次变慢,并且循环发生,根据多次内存问题的分析经验,会与内存泄漏关系更大。至于为何有如此的判断依据,下文会进行解释。

该问题属于后者,因此首先从内存的角度进行分析。使用Android官方提供的DDMS工具,选中App所在进程,监控该进程的堆内存状况,如下表所示,随着重复打开界面次数的增加,堆内存一直呈上升趋势,而且,测试过程中,即使点击DDMS的Gause GC(Garbage Collector 内存垃圾回收)来主动触发内存垃圾回收,堆内存也没有下降。


从该图可以判断,无论是系统自发的GC或QA主动触发,对内存都不会下降,说明有相当一部分对象被一直引用着,导致GC时不会去释放这部分对象的内存,而每打开一次界面,又会在堆上为新的对象分配内存,最终结果就是内存泄漏。

对内存泄漏有了初步判断后,下一步是使用MAT工具分析该场景所有对象的内存占用和引用关系,从而定位出具体导致泄漏的类。首先同样地重复打开该界面,在打开第5次、10次、20次的时候,分别dump出当时的hprof文件(相当于所有对象的内存画像),3个文件在MAT的sumary分析中,都指出DynamicBgDrawable这个类的对象存在内存泄漏的风险


于是问题的分析有了下一步的目标。继续使用MAT查看DynamicBgDrawable对象的引用链:


从上图看出,GraphicContent这个类的mContents成员(WeakHashMap类型)是mBackground对象(DynamicBgDrawable类型)的root引用,这说明正是因为GraphicContent中mContents对mBackground的间接引用一直未释放,才导致DynamicBgDrawable对象内存的泄漏,到这里就可以从GraphicContent.java的代码继续寻找问题的原因了。

代码&业务分析


从GraphicContent类的两处代码可以看出,GraphicContent一直持有着View,而WeakHashMap这个结构,如果作为key的View没有被释放,作为value的GraphicContent也不会被释放,而这里的View,在实际运行时,传的就是DynamicBgDrawable对象,所以形成DynamicBgDrawable与GraphicContent循环引用,互不释放的局面,随着重复地调用次数增多,无法释放的对象越来越多,最终导致内存泄漏。

真相大白

Android App是运行在Dalvik虚拟机之上的Java程序,Dalvik是Java虚拟机(JVM)针对Android改造的版本,许多机制沿袭了JVM的设计,包括内存管理。对JVM的内存管理和回收机制进行了解,能更深入地理解该Bug的分析手段和定位过程。

以上是Java虚拟机(JVM)的内存区域划分,Java只能在堆中存放对象,而不能在栈上分配对象,所有运行时产生的对象全部都存放于堆中,包括数组。是一个线程的执行区域, 它保存着一个线程中的方法的调用状态,也可以说,一个Java线程的运行状态,都由一个Java栈来保存。每个线程都会有自己的Java栈, 不会相互访问其他Java栈中的数据。同时,基本数据类型也是在栈中保存,包括boolean、byte、char、short、int、float、long、double。所以在分析这个Bug时,只关心堆内存,而不关心栈内存。因为对象内存的泄漏(溢出)只会发生在堆内存上。

下面解释开头的问题:为什么概率性加载缓慢,更有可能与内存相关。首先需要了解Dalvik虚拟机的内存垃圾回收原理:


Dalvik中会维护一个对象的引用关系图,如上图所示,方块代表一个对象,mark后的数字代表这个对象被持有的引用个数。当Dalvik进行GC时,首先会做“标记”,将每个对象被引用的次数进行标记。上图中,Root是引用关系图的起点,蓝色方块代表该对象被持有了引用,那么它的内存不会被回收。白色方块代表该对象没有或即将不被持有引用。”标记”过程结束后,GC就进入”清除”过程,会把所有mark为0的对象内存释放掉,从而完成一次GC的操作。

 

上图就是GC进行“清除”操作前后的示意图。可以看出,在回收前,连续的可用内存较少,等同于碎片较多,在回收后,连续的可用内存变多了。我们回到bug本身,由于每次打开界面,都会为新的对象分配内存,于是上图中的存活对象方块会会越来越多,连续的未使用区域会越来越少,这时当下一次打开界面时,因为碎片过多,无法分配内存给对象,特别是大对象,就会过早的引起GC。而对象越多,一次GC的时间会越长,从而加大了系统的负载,增加了App界面的调起时间。下一步,当完成GC后,如果有足够的内存可分配,则是较好的情况,如果像该Bug的情况,占用大片内存的对象一直被引用着而不被GC释放,在下一次打开界面时,Android系统就需要为这个App分配更大的堆内存,以保证内存分配成功。当内存泄漏到一定程度,系统无法保证为App分配足够内存时,则内存溢出(Out Of Memory, OOM)就会发生。


上图解释了界面概率性打开缓慢与内存更相关的原因。图中黑色的步骤都会增加界面的加载时间,上面的黑色步骤,是由于内存碎片引起,会随着打开次数增多,发生概率加大。下面的黑色步骤,是根据GC后App所生堆内存大小相关,每次打开界面的情况都会不一样,因此,才会出现概率性的打开缓慢,从而为我们分析这种问题提供思路——就是开头提到的,如果是概率性打开缓慢,可以优先考虑和内存问题相关。

解决方案

将mView改为弱引用,每次垃圾回收(GC)时,都会回收View对象,从而使WeakHashMap中的value(GraphicContent)对象也能自动得到释放。



总结

该Bug的分析和解决过程,具有典型的代表性,在实际项目中,有多个Android内存泄漏的Bug,都是采用上述的定位方法和分析工具进行解决的,是一套通用且有效的Bug定位方案。而相互引用的问题,等价于死锁情况,也是程序中典型的问题场景。弱引用的使用有效地解决业务和内存泄漏的问题,在Android app的内存泄漏和溢出的解决中,经常会被采用,也可以作为Code Review的一个关注点进行推广。


更多干货分享请关注”百度MTC学院“http://mtc.baidu.com/academy/article
展开阅读全文

没有更多推荐了,返回首页