Android内存泄露及解决方法总结

1. 概述

Java内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到gc roots导致无法被GC回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。

2. 常见泄露类型

2.1. 集合类泄露

如果集合类仅仅有添加元素,而没有相应的删除机制,会导致内存被占用。当将集合中元素置空,但是集合因为持有对元素的引用,导致内存回收不,而发生内存泄露。解决方法是,可先删除元素然后置空,或者直接将集合置空。

Android 中常见的集合类内存泄露有ValueAnimator调用addUpdateListener,EditText调用addTextChangedListener()而未注销监听导致内存泄露,他们代码如下


     //ValueAnimator
   public void addUpdateListener(AnimatorUpdateListener listener) {
        if (mUpdateListeners == null) {
            mUpdateListeners = new ArrayList<AnimatorUpdateListener>();
        }
        mUpdateListeners.add(listener);
    }
    
    //EditText
    public void addTextChangedListener(TextWatcher watcher) {
        if (mListeners == null) {
            mListeners = new ArrayList<TextWatcher>();
        }

        mListeners.add(watcher);
    }

2.2 静态变量引起的内存泄漏

在java中静态变量的生命周期是在类加载时开始,类卸载时结束。换句话说,在android中其生命周期是在进程启动时开始,进程死亡时结束。所以在程序的运行期间,如果进程没有被杀死,静态变量就会一直存在,不会被回收掉。如果静态变量强引用了某个Activity中变量,那么这个Activity就同样也不会被释放,即便是该Activity执行了onDestroy(不要将执行onDestroy和被回收划等号)。这类问题的解决方案为:

  1. 寻找与该静态变量生命周期差不多的替代对象。
  2. 若找不到,将强引用方式改成弱引用

2.2.1 单例引起的Context内存泄漏

由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。如果单例中引用activity类型Context而非ApplicationContext,会导致ondestroy后不能被回收

public class IMManager {
  private Context context;
  private static IMManager mInstance;
 
  public static IMManager getInstance(Context context) {
    if (mInstance == null) {
      synchronized (IMManager.class) {
        if (mInstance == null)
          mInstance = new IMManager(context);
      }
    }
    return mInstance;
  }
 
  private IMManager(Context context) {
    this.context = context;
  }
 
}

可以让传入的context 转化为ApplicationContext。

2.2.2 静态Activity和View,drawable

静态变量Activity和View会导致内存泄漏,在下面这段代码中对Activity的Context和TextView设置为静态对象,从而产生内存泄漏。

public class MainActivity extends AppCompatActivity {
 
    private static Context context;
    private static TextView textView;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = this;
        textView = new TextView(this);
    }
}

静态变量drawable 也类似,但android 4.0的Drawable.Java对setCallback的实现进行了软引用,避免了内存泄露:

  public final void setCallback(Callback cb){

        mCallback = newWeakReference<Callback> (cb);

}

2.2.3 非静态内部类创建静态实例

public class MainActivity extends AppCompatActivity {
    private static TestResource mResource = null;
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mManager == null){
            mManager = new TestResource();
        }
            //...
    }
   
   class TestResource {
    //...
   }
}

因为非静态内部类默认会持有外部类的引用,而该非静态内部类TestResource又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

2.3. 线程造成内存泄露

android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类/非静态内部类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

2.3.1 多线程

如果多线程中含有外部activity的引用,当activity销毁后,而多线程任务还未执行完,因为线程持有对activity的引用而导致activity不能被回收掉

2.3.2 内部线程造成内存泄露

public class LeakActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        leakFun();
    }
 
    private void leakFun(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

可以将leakFun()加上static 改成静态方法,让匿名内部类不会持有LeakActivity.this应用。

2.3.1 handler

public class LeakAty extends Activity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.aty_leak);
    fetchData();
 
  }
 
  private Handler mHandler = new Handler() {
    public void handleMessage(android.os.Message msg) {
      switch (msg.what) {
      case 0:
        // 刷新数据
        break;
      default:
        break;
      }
 
    };
  };
 
  private void fetchData() {
    //获取数据
    mHandler.sendEmptyMessage(0);
  }
}

mHandler 为匿名内部类实例,会引用外围对象LeakAty.this,如果该Handler在Activity退出时依然还有消息需要处理,那么这个Activity就不会被回收。

如果当Handler为非静态内部类也会导致内存泄露,因为非静态内部类也会持有外部类的引用,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

2.3.2 AsyncTask

AsyncTask 情况类似handler,都有由于非静态内部类,或者匿名内部类持有activity的引用,当activity退出时任务还未完成继续持有对activity的引用,导致activity不能回收。

2.4. 动画导致的内存泄露

Android在动画使用过程如果不注意也会经常导致内存泄露。

例如属性动画onDestroy中未停止动画,这时候Activity会被View所持有,从而导致Activity无法被释放。解决方法onDestroy去去调用objectAnimator.cancel()来停止动画。

public class LeakActivity extends AppCompatActivity {

   private TextView textView;
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_leak);
       textView = (TextView)findViewById(R.id.text_view);
       ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView,"rotation",0,360);
       objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
       objectAnimator.start();
   }
}

2.5 资源未关闭或注销引用内存泄露

常见由于资源未关闭/注销导致内存泄露有

  • BraodcastReceiver
  • ContentObserver
  • File
  • 游标 Cursor
  • Stream
  • Bitmap

这些资源应该打开,用完后马上关闭,或者在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

2.6. 不良代码

  • 构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView
  • EventBus,RxJava等一些第三开源框架的使用,若是在Activity销毁之前没有进行解除订阅将会导致内存泄漏。
    ···

3. 内存泄露解决措施

3.1. 工具方面:

  1. LeakCanary 检测 Android 的内存泄漏
  2. Android Studio 的Monitor监测内存使用情况
  3. MAT分析heap的总内存占用大小来初步判断是否存在泄露

3.2 代码方面

  1. 引入弱引用
  2. 使用合适的Context,尽量使用ApplicationContext
  3. 在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列 MessageQueue 中的消息。
  4. 尽量避免使用 static成员变量
  5. 使用的资源及时关闭、注销
  6. 避免非静态内部类,可改成静态内部类
  7. 避免匿名内部类

4. 参考:

  1. Android 内存泄漏总结
  2. Android内存泄漏终极解决篇(下)
    原文作者:Jinwong
    原文地址: https://www.jianshu.com/p/bf4546ac1a6a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞