最近遇到一个Android oom(out of memory)的疑难问题, 至今仍未有解决. 一路走来, 遇到过多次Android oom. 在此做个总结, 也继续Track目前正在解的问题.
第一步: 发现问题
当我们在把玩自己的AP的时候, 突然它Force Close了. Logcat出来日志以后, 发现是可爱的oom问题.
第二步: 分析问题
通常引起oom的原因据我了解有以下几种情况:
1. 程序中使用了太多自己创建的Bitmap.
这种情况通常是最好解决的. 因为你明白你在哪里使用了这些Bitmap, 在什么时候就不需要了.
大部分情况是因为重复创建bitmap, 而不使用的bitmap没有被及时释放, 导致了 oom. 所以在不使用的时候要将bitmap对象回收bitmap.recycle(), 并将bitmap对象置为null.
还有就是当你一次性使用过多bitmap的时候也会导致oom. 比如使用系统的Gallery组件, 然后在每一项使用自己的图片, 这个时候我们通常自定义Gallery的adapter. 当Gallery需要显示很多图片的时候, 而我们没有做图片缓存机制的话必然会导致oom发生. 所以这个时候需要做图片缓存机制. 例如只保存当前显示项前后3项的图片, 滑动Gallery的时候再根据当前项动态回收前后不需要的图片和加载需要的图片.
2. 程序中一些对象创建的时候需要context的时候.
这种情况, 通常我们会使用create(this); 这个时候引用的时候activity的context, 如果该对象没有被及时回收, activity的引用将被它保留, 从而导致activity不能被及时销毁, 当重复创建activity后, 就会导致activity有多个实例, 从而导致内存泄露. 所以在用到context的时候尽量使用application的context: getApplicationContext().
3. 查询数据库没有关闭游标
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor 后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。所以在使用完游标以后要 cursor.close();
4. 没有释放对对象的引用:
这种情况描述起来比较麻烦,举两个例子进行说明。
示例A:
假设有如下操作
public class DemoActivity extends Activity {
… …
private Handler mHandler = …
private Object obj;
public void operation() {
obj = initObj();
…
[Mark]
mHandler.post(new Runnable() {
public void run() {
useObj(obj);
}
});
}
}
我们有一个成员变量obj,在operation()中我们希望能够将处理obj 实例的操作post 到某个线程的MessageQueue 中。在以上的代码中,即便是mHandler 所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj 还保有这个对象的引用。所以如果在DemoActivity 中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:
… …
public void operation() {
obj = initObj();
…
final Object o = obj;
obj = null;
mHandler.post(new Runnable() {
public void run() {
useObj(o);
}
}
}
… …
示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen 中定义一个PhoneStateListener 的对象,同时将它注册到TelephonyManager 服务中。对于LockScreen 对象,当需要显示锁屏界面的时候就会创建一个LockScreen 对象,而当锁屏界面消失的时候LockScreen 对象就会被释放掉。
但是如果在释放LockScreen 对象的时候忘记取消我们之前注册的PhoneStateListener 对象,则会导致LockScreen 无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen 对象没有办法被回收而引起OutOfMemory,使得system_process 进程挂掉。
总之当一个生命周期较短的对象A,被一个生命周期较长的对象B 保有其引用的情况下,在A 的生命周期结束时,要在B 中清除掉对A 的引用。
5.其它
Android 应用程序中最典型的需要注意释放资源的情况是在Activity 的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity 生命周期的介绍,以明确何时应该释放哪些资源。
还有其它一些疑难的很难找到原因的问题:
这种情况通常是比较折磨人的, 但是你要及时转换一种心态: 哇, 这个问题比较有趣呢, 如果解决掉,岂不是很有成就感呢? 题外话..
排查问题的方法:假设我的程序是A.
1. 如果是A和B程序交互时出现的问题, 那么我就用A和C来交互来重现这个问题, 前提是C和B有些共同点.如果能重现, 说明是A自己的问题, 和B没有关系.
2. 关注A相关的一些回调函数, 打印log, 去找具体的地方.
3. 可以用最小化问题的方法来排除, 叠加屏蔽一些可疑代码, 直到问题消失. 这个是最有效的发现问题的方法.
使用内存分析工具MAT(MemoryAnalyzer Tool)
MAT 是一个Eclipse 插件,同时也有单独的RCP 客户端。官方下载地址、MAT 介绍和详细的使用教程请参见:www.eclipse.org/mat,在此不进行说明了。另外在MAT 安装后的帮助文档里也有完备的使用教程。在此仅举例说明其使用方法。我自己使用的是MAT 的eclipse 插件,使用插件要比RCP 稍微方便一些。
1. 打开DDMS
2.打开程序, 会看到程序的进程.
3.选中程序的进程, 点击Update Heap按钮.
4.按照发生oom的步骤操作, 直到发生oom.
5.点击Dump HPROF file按钮.
6.过一会eclipse会自动用MAT打开生成的.hprof文件.
7.点击文件上面的Histogram会打开一个视图.
8.在class name那一栏最底部, 会有一行Total**** 然后右击选项ExpandAll.
9. 点击Shadow Heap排序, 最顶层的就是泄露最多的地方.
10.右击某个calss选择ListObjects->incoming reference.
11.然后就可以看到在哪些地方有些路, 逐个排查对应的代码.
也可以使用下面的方法:
点击Dominator Tree,并按Package分组,选择自己所定义的Package 类点右键,在弹出菜单中选择List objects->With incomingreferences。这时会列出所有可疑类,右键点击某一项,并选择Path to GC Roots -> exclude weak/soft references,会进一步筛选出跟程序相关的所有有内存泄露的类。据此,可以追踪到代码中的某一个产生泄露的类。
由此发现的一些很搞笑的内存泄漏原因:
最后我的问题解决了, 很可笑的内存泄漏: B引用了A的实例, 我们在释放B的时候没有去释放对A的引用, 那么就会造成对A的泄露. 看下面代码解释.
错误的代码:
public class A extends Activity implments TestListener{
…………….
private B mTestClass
protected void onCreate( ){
mTestClass= new B(this);
testClass.setTestListener(this);//设置了Listener, 引用了Activity.
}
protected void onDestroy(){
mTestClass= null;//只是释放了对象, 实际上Listener还有对Activity的引用
}
…………….
}
public class B{
…………….
private TestListener mTestListener;
public B(Context Context){ …. }
….
public setTestListener( TestListener tl){
mTestListener = tl;
}
…………….
}
修改后的代码:
public class A extends Activity implments TestListener{
…………….
private B mTestClass
protected void onCreate( ){
mTestClass= new B(this);
testClass.setTestListener(this);//设置了Listener, 引用了Activity.
}
protected void onDestroy(){
mTestClass.removeTestListener();//释放引用
mTestClass= null;
}
…………….
}
public class B{
…………….
private TestListener mTestListener;
public B(Context Context){ …. }
….
public setTestListener( TestListener tl){
mTestListener = tl;
}
//加上注销Listener的方法.
public removeTestListener( ){
mTestListener = null;
}
…………….
}
总结:
在这个过程中, 发现了一个很好用的工具:
JvisualVm 这个工具可以打开hprof文件, 查看对象引用关系. 比MAT那个强大啊!!
这个工具就在JDK的根目录的bin文件夹里面.
Android的内存泄漏就这么回事.