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内存泄漏终极解决篇(下)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • 一、基础知识 1、什么是内存泄露 java中的内存泄露是指一个无用对象持续占有内存或无用对象的内存得不到及时的释放...
    LiveMoment阅读 4,417评论 0 20
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,619评论 0 8
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 789评论 0 5