前段时间公司项目一直出现莫名其妙的bug,看了日志发现有很多内存溢出的问题。引发的原因也不是很清晰,因为平时写代码并没有过于关注过内存泄露的问题,关于内存管理更多是很平常的那种,图片压缩啊什么的。后来查资料的时候看到内存泄露也可能造成内存溢出。就在项目中导入了LeakCanary,不测不知道,一侧吓一跳!卧槽,一个主页面就有5、6个内存泄露。虽然说项目中的内存泄露有很多地方,但是改过之后发现最长出现的也就那么几个,比如说Context引起的内存泄露,非静态内部类引起的内存泄露等。索性在这里总结一下,方便以后查看。
查看了好多文章,这个还是比较详细的,刚开始都是从这里边一个个对照着来的。
1.什么是内存泄露?
简单来说,内存泄露就是该被释放的资源由于被其他实例引用而不能够被回收。轻则导致卡顿,重则直接crash。
2.怎么检测是否有内存泄露?
常见的内存泄露分析工具有LeakCanary和MAT,两个都可以。LeakCanary用起来更简单点,看起来更直观。可以直接在手机上查看,而且报错信息也很明确。一些简单问题都可以直接定位的。MAT功能比LeakCanary更加强大,更全面,但是使用起来会麻烦一点,需要掌握一定的分析技巧。当然也不是很难。网上有很多的教程,感兴趣的可以看看,这里只介绍LeakCanary的使用方法。
3.LeakCanary的使用方法:
其实这不需要解释的,直接在github上搜一下,里边就有详细的使用说明,这里大体讲一下步骤好了。
1. 在项目的build.gradle的dependencies语句块中添加LeakCanary 的依赖
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
这里可以有三种模式下的依赖,可以把不需要的注视掉都可以。
2. 在Application中添加如下代码:
if (LeakCanary.isInAnalyzerProcess(this)) {
return;
}
LeakCanary.install(this);//内存泄露检测
通过上述两个步骤直接启动项目就可以使用LeakCanary检测内存泄露了。出现内存泄露的时候,手机上会出现一个leak的图标,可以点开查看泄露的列表,以及一些详细的报错信息,根据这些就可以分析出造成内存泄露的原因和发生的位置。
3.常见的内存泄露:
0. 上下文环境Cotext乱用造成内存泄露
这里一定要搞明白ApplicationContext,context,Activity的区别:
- Activity是android的四大组件之一是一种具化的东西,而ApplicationContext,Context是一种抽象的概念,标识的是上下文环境。
- ApplicationContext的生命周期和app是一致的,而Activity的Context生命周期和Activity相同的,随着Activity的销毁,这个Context也就不复存在了。
因此,我们不能让长生命周期的对象引用Activity的Context。
比如说:如果我们在单例模式中需要传入Context,我们应该用哪个呢?我们可以分析一下,因为单例模式创建的实例是静态的,生命周期和app的生命周期是一样的,如果我们使用Activity的Context就会引起内存泄露。所以这种情况可以使用getApplicationContext。
1. 单例引起的内存泄露
原来我们的项目中创建Fragment一直都是用的单例模式,这是仿照原来同事的做法写的。开始觉得挺方便的,也没多想(毕竟经验有限)。现在一测。。。全是内存泄露。没办法,改吧!
//这是原来使用单例模式创建Fragment的代码,会造成内存泄露!
public static HomePageFragment instance;
public static HomePageFragment getInstance() {
if (instance == null) {
instance = new HomePageFragment();
}
return instance;
}
原因分析:
通过单例模式创建的实例,由于该实例是被static修饰的,所以它的生命周期是和app的生命周期一致的,而Fragment肯定是要持有Activity的实例的,如果在Fragment中有耗时操作,关闭掉页面的时候还没有执行完。就会导致Activity不能被正常销毁,从而造成内存泄露。
解决办法:
既然不能通过单例创建Fragment对象,那就改用直接创建好了。
//如果Fragment为空,就去new一个
if (mHomePageFragment == null) {
mHomePageFragment = new HomePageFragment();
}
通过这样的创建方式,就解决了这个内存溢出的问题,其实这样的创建方式和使用单例创建唯一的区别就是,单例创建的是个static修饰的实例,就是生命周期不一样。
2. 非静态内部类创建静态实例引起的内存泄露
项目中很多地方都是用了内部类的方式来实现某些功能,常见的是通过内部类实现一些监听。
public class MainActivity extends AppCompatActivity {
private static MemoryText mMemoryText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (mMemoryText==null){
mMemoryText=new MemoryText();
}
}
class MemoryText{
//非静态内部类会自动持有外部类对象
}
}
因为非静态内部类会自动持有外部类对象,而mMemoryText是个静态的实例,那么他的生命周期就是app的生命周期了。因此会造成内存泄露。正确的做法为:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext 。
3. 匿名内部类引起的内存泄露
匿名内部类的写法在java或者android中时非常常见的方式,比如说在设置监听事件的时候,我们都习惯使用匿名内部类,然是由于匿名内部类会自动持有外部类的引用,如果进行耗时操作的话,会导致外部类不能正常销毁,从而引发内存泄露。
4. Handler引起的内存泄露
public class MainActivity extends AppCompatActivity {
private Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
//...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
}
private void initData() {
mHandler.sendEmptyMessage(0);
}
}
由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,
public class MainActivity extends AppCompatActivity {
private TextView id_content;
private MHanlder mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView() {
mHandler=new MHanlder(this);
id_content= (TextView) findViewById(R.id.id_content);
}
private void initData() {
mHandler.sendEmptyMessage(0);
}
//声明为静态内部类,就不会持有外部类对象了
static class MHanlder extends Handler{
private WeakReference<Context> mContext;
public MHanlder(Context context) {
//如果需要用到外部类对象,就通过弱引用的方式引入
mContext=new WeakReference<Context>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity= (MainActivity) mContext.get();
if (activity!=null){
activity.id_content.setText(".....");
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//在回收Activity时,移除没有处理完成的对象
mHandler.removeCallbacksAndMessages(null);
}
}
声明一个静态Handler内部类,这样就不会持有外部类对象了,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,但是在回收Handler持有的外部类对象时,Looper 线程消息队列可能还有未处理完的对象,这里需要在onDestory中移除.
5. 资源未关闭造成的内存泄露
使用BoradcastReceiver,ContentResolver,File,Cursor,Stream,Bitmap的时候,一定要在Activity销毁的时候及时反注册或者关闭资源。否则这些资源不会被回收,从而造成内存泄露。