Android程序的内存泄漏与规避方法
扫描二维码
随时随地手机看文章
引言
Android应用程序中内存使用的问题经常容易被忽视,在传统的编程语言中(例如C语言),回收内存的任务是由程序本身来完成的,程序可以显式分配和释放变量所占用的内存。Android[1]应用程序采用Java编程语言编写,而Java区别于其他语言的一个重要优点就是它通过垃圾收集器(Garbage Collection,GC) 自动管理内存的回收,Java程序员只需通过内存分配操作创建对象,而无须关心对象占用的空间是如何被收回的。因此很多程序员认为在Java中不必担心内存泄漏的问题,然而实际并非如此,Java中仍然存在着内存泄漏。Android应用程序运行在嵌入式系统中,而嵌入式系统中内存的总量非常有限,因此如何合理地规避“内存泄露”问题也就显得十分关键。
1 造成Android应用程序内存泄漏的原因
1.1 引用没释放造成的内存泄露
(1) 注册没有取消造成的内存泄漏
这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。
(2) 集合中对象没有关闭造成的内存泄漏
通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,慢慢地这个集合就会越来越大。如果这个集合是静态的话,那情况就会更严重。
1.2 资源对象没有关闭造成的内存泄漏
资源对象比如Cursor、File文件等往往都用了一些缓冲,在不使用的时候应该及时关闭它们,以便它们的缓冲及时回收内存。这些缓冲不仅存在于Java虚拟机内,还存在于Java虚拟机外,如果仅仅是把它的引用设置为空,而不关闭它们,那么往往会造成内存泄漏。
一些不良代码造成的内存压力原因如下:
◆ Bitmap没有调用recycle( );
◆ 构造Adapter时,没有使用缓存的convertView;
◆ ThreadLocal使用不当;
◆ 其他。
2 内存泄漏的检测及定位
2.1 内存泄漏的检测
Android应用程序是基于虚拟机的,其内存管理都是由Dalvik[2]代为管理,GC的回收不是及时的。一个正常的应用程序在其运行稳定后其内存的占用量是基本稳定的,不应该是无限制的增长。同样,对任何一个类的对象的使用个数也有一个相对稳定的上限,不应该是持续增长的。当我们持续地观察某个应用程序运行过程中使用内存的大小和各实例的个数时,如果内存的大小持续增长,则说明系统存在内存泄漏的问题;如果特定类的实例对象个数随时间而增长,则说明这个类的实例可能存在泄漏情况。比如一个Activity被关掉之后,其内存的引用对象会在下次GC回收[3]的时候通过回收算法计算,如果这部分内存已经属于可回收的对象,那么这些垃圾对象会被一并回收,内存未泄漏趋势图如图1所示。
图1 内存未泄漏趋势图
内存泄漏趋势图如图2所示。在重复打开关闭某个应用程序的时候,内存一直在向上爬升,也就是说每次关闭这个Activity的时候,有些应该释放的内存并没有被释放掉。由此我们可以确定这个应用程序存在着内存泄漏的问题。
图2 内存泄漏趋势图
2.2 内存泄漏的位置定位
查找内存泄漏一种比较彻底的方法就是代码走查,我们可以一行一行地分析对象的创建去留等等[4],但会很耗时间也比较迷茫。这里可以通过Eclipse Memory Analyzer Tool(MAT)工具来定位内存泄漏的位置,该方法只适用于Java层的查找,对C/C++没用,也就是说只针对于被虚拟机来管理的进程和内存。MAT的解析文件是.hprof文件,这个文件里面存放了某进程的内存快照,MAT通过解析.hprof文件就会自动生成一个内存泄漏推测报告,通过分析这个报告就可以准确定位到有可能存在内存泄漏的具体位置。
然而,还有一些内存泄漏通过MAT是查不出来的,比如native的代码,对C/C++是无能为力的,对于这些问题本文并没有做过多的研究。
3 规避内存泄漏的方法
在编写应用程序的过程中,对于BraodcastReceiver、ContentObserver、FileObserver在Activity onDestory或者某类声明周期结束之后一定要注销掉,否则这个Activity类会被系统强引用,不会被内存回收。
在定义成员变量时,不要直接对Activity进行引用而作为成员变量。如果不得不这么做,那么可以用private Weak Reference
在应用程序中,很多内存泄漏是由于循环引用而造成的,比如a中包含了b,b中包含了c,c中又包含a,这样只要一个对象存在,那么其他对象肯定会一直常驻内存。因此,在编写应用程序时要从逻辑上来分析是否需要这样的设计。
Bitmap对象不再使用时,调用recycle()方法释放内存。如果一个Bitmap对象比较占内存,当它不再被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,这个不是必须的,可视情况而定。
还要注意释放对象的引用。当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。
4 内存监测工具DDMS和内存分析工具MAT
4.1 内存监测工具DDMS
我们在开发Android应用程序时,很容易造成内存的泄漏,这时需要一些工具来帮助我们检查代码中是否存在会造成内存泄漏的不良代码。在Android tools的DDMS里面带有一个内存监测工具Heap,用它来监测应用程序使用内存的情况,这里需要和Eclipse配合使用。利用 Heap工具监测应用进程使用内存情况的方法如下:
① 首先启动Eclipse,切换到DDMS透视图,并确认Devices视图、Heap视图都已打开。将要测试的设备(比如手机)通过USB数据线连接到电脑上,连接成功后,会在DDMS的Devices视图界面中显示手机设备的序列号,以及设备中正在运行的部分进程信息。[!--empirenews.page--]
② 然后选中想要监测的进程,比如system_process进程,进而选中Devices视图界面中最上方一排图标中的“Update Heap”图标,点击Heap视图中的“Cause GC”按钮,此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。
③ Heap视图界面会定时地刷新正在监测的进程内存使用情况,通过不断地操作被监测的应用程序来观察内存使用的变化。
那么如何才能知道被监测的应用程序是否存在内存泄漏呢?这里需要注意一个值:Heap视图中有一个类型值叫做data object,即数据对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量。一般情况下,这个值的大小决定了应用程序是否存在内存泄漏。我们不断地操作当前应用,同时注意观察data object的Total Size值,正常情况下Total Size的值都会稳定在一个有限的范围内,也就是说由于程序中的代码良好,没有造成对象不被垃圾回收的情况,内存占用量保持在了一个相对稳定的水平;反之,如果代码中存在没有释放对象引用的情况,则data object的Total Size值会随着操作次数的增多越来越大,直到到达一个上限后导致进程被杀掉。
通过上面的分析,使用DDMS的Heap视图工具可以很方便地确认应用程序是否存在内存泄漏的问题。
4.2 内存分析工具MAT
通过DDMS工具可以判断应用程序中是否存在内存泄漏的问题,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?内存分析工具MAT Memory Analyzer Tool解决了这一难题。MAT工具是一个Eclipse 插件,同时也有单独的RCP 客户端,MAT工具的解析文件是.hprof,这个文件存放了某进程的内存快照。MAT工具定位内存泄漏具体位置的方法如下:
① 生成.hprof文件。Eclipse中生成.hprof文件的方法有很多,不同Android版本中生成.hprof的方式也稍有差别,但它们整体思路是一样的。我们在DDMS界面选中想要分析的应用进程,在Devices视图界面上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮,这时DDMS将会自动生成当前选中进程的.hprof文件。
② 将.hprof 文件导入到MAT工具中,MAT工具会自动解析并生成报告,点击“Dominator Tree”按钮,并按包分组,选择已定义的包类点右键,在弹出的菜单中选择List objects?﹥With incoming references,这时会列出所有可疑的类。右键点击某一项,并选择Path to GC Roots?﹥exclude weak/soft references,MAT工具会进一步筛选出跟程序相关的所有内存泄漏的类。这样就可以追踪到某一个产生内存泄漏的类的具体代码中。
使用MAT内存分析工具查找内存泄漏的根本思路是找到哪个类的对象的引用没有被释放,然后分析没有被释放的原因,最终定位到代码中哪些片段存在着内存泄漏。
结语
Android应用程序中内存泄漏是一个特别重要但又难以解决的问题,不再有用的对象被其他依然有用的对象所引用是导致内存泄漏的主要原因。程序员良好的编程风格和专业的检测工具可以有效地减少内存泄漏的问题,比如上面介绍的内存监测工具DDMS和内存分析工具MAT。但如何更好地避免甚至消除内存泄漏、如何解决因内存泄漏而导致的Android应用程序系统性能下降问题,将是内存泄漏相关领域的重要研究方向。