Android oom(out of memory)的疑难问题

来源:互联网 发布:电子相框 知乎 编辑:程序博客网 时间:2024/05/01 13:24

最近遇到一个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的内存泄漏就这么回事.