对于程序员来讲,JAVA/Android遇到OOM绝对是让你头疼的问题,作为一名原本是C/C++程序员来说,我一开始觉得很诧异,说好的垃圾回收机制呢???你TM在逗我?
其实不是这样个说法,垃圾回收机制也是要靠你对引用的理解把握的,如果你对你的Activity已经释放完毕,但是你又使用了Activity.this,然后那个引用的变量还一直在,那么抱歉,你的Activity其实并没有释放,如同下面这个例子,首先定义一个单例类:
public class Test {
public List<Context> list = new ArrayList<>();
static Test sIns;
public static Test getIns(){
if(sIns == null){
sIns = new Test();
}
return sIns;
}
}
通过getIns()函数返回一个实例,假设我们定义了这样一个Main2Activity :
public class Main2Activity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
//10次是因为内存效果明显一点
for(int i=0;i<10;i++)
Test.getIns().list.add(this);
}
}
每次创建都把自己加到单例里面的队列里面,问题就来了,退出这个Activity的时候,我们虽然执行了onDestory,但是很遗憾的是,我们的Main2Activity 并没有被回收,OK,内存泄露出来了。
上面只是一个简单的例子,平常我们在开发的时候并没有那么简单或者说不容易发现内存泄露。内存泄露并不一定是我们自己代码的问题,有可能你是从别人那边接手的代码。
下面进入正题,怎么对已经是烂摊子的代码快速定位到问题所在!
先介绍一个检测OOM隐患的代码辅助工具: leakcanary https://github.com/square/leakcanary
我试了下,小Demo确实可以检测,问题是项目一大效果就没了,可能还需要再优化吧。具体怎么使用,同学们可以自己去看github上的说明。
下面我来介绍Android Studio的检测方法。
我是用的Android Studio版本是
我的项目目录是
MainActivity是启动Activity,有一个按钮可以到Main2Activity,Main2Activity和Test上面已经给出代码,这里给出MainActivity的
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivity(new Intent(MainActivity.this,Main2Activity.class));
}
});
}
}
启动调试,然后看下图,我们可以看到我们的App当前的运行内存!
然后我们点击MainActivity的按钮跳转到Main2Activity,再点物理键返回,重复多次后,我们的内存图片就这样了~~~
啊哦,从23.79M彪到27.48M了!!!
好吧,让我们来看看到底发生了什么,见下图操作:
第一步一定要多次点击,因为我们要排除干扰我们的因素,有些变量虽然占着内存,但是可以被GC回收的。
第二步完成后,AndroidStudio会自动打开一个.hprof的Java Heap信息文件
红色箭头解释:
1、是我们看抓取内容的目录
2、我们Dump Java Heap的具体文件
3、Class Name,我们内存中存在的Class都会在这个窗口
4、在这个Instance窗口我们可以看到某个类有哪几个实例
5、这个Reference Tree窗口,我们可以看到具体的引用树
完成以上介绍,那我们就可以开始查问题咯~~
上图那个3的红色箭头我故意指向了Class List View,那我们点击那个按钮,选择 Package Tree View
然后定位到我们自己的包名位置:
我的包名就是com.example.administrator.myapplication,这里我们很容易发现,我的Main2Activity类在内存中已经有31个实例了,那我们继续点一下它
可以很清楚的看到:右边的Instance窗口向我们展示了每个实例的内存地址,我们点击其中一个:
这时候Reference Tree 窗口告诉了我们引用关系,我们这个Main2Activity实例保存在Indices 的190到199的索引中,Indices 又是保存在array中,继续往下查找,我们找到了最后的保存位置,就是我们的Test的sIns中(红色箭头所指),其实就是sIns.list中。
好了,问题找到了,是我们的Test的sIns.list一直Hold了我们的Main2Activity,导致没有被GC回收,所以,我们在Main2Activity中增加代码:
@Override
protected void onDestroy() {
super.onDestroy();
//防止内存泄露
for(int i=0;i<10;i++)
Test.getIns().list.remove(this);
//通知回收内存 - 并没什么卵用。。。
//System.gc();
}
这时候不管我们点击几次,退回,再进去Main2Activity,我们的Main2Activity始终只有一个!!!
好了,打完收工!
千里之堤,溃于蚁穴。我们开发的时候需要多注意!
下面是我觉得应该在Android OOM开发方面应该注意的地方
1. 少用Activity的Context,除了必须要用的如Dialog等 可以使用 Application的Context
2. 对于用到的Context需要及时释放置为null
3. 线程的无节制使用也会耗尽内存,尽量使用 线程池Executors。
4. 对于占有内存较大的变量,我们需要在Activity销毁的时候把它置为null
5. 在一个Activity的基类上定义一个static final Handler,以后都用这个Handler,而不是无节制的new Handler
6. 对于一些经常用到的类,我们可以把它声明到Application中的静态变量,这样不会引发无节制的生成新的,占用空间
7. 对于一些需要单独创建的单列类,我们需要对其中引用到的的Context、View等大内存对象及时释放,不然内存会一泄千里