Android常见内存泄露分析总结

一、基础知识

1、什么是内存泄露

java中的内存泄露是指一个无用对象持续占有内存或无用对象的内存得不到及时的释放,从而造成的内存控件的浪费称为内存泄露

2、Java 中的内存分配

静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

3、四种引用类型

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;

软引用(SoftReference):只有在内存空间不足时,才会被回的对象;

弱引用(WeakReference):在 GC(垃圾回收) 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

我们常说的内存泄漏是指new出来的Object无法被GC回收,即为强引用。

4、内部类 静态内部类

内部类:其实就是外部类A的一个对象,所以它会持有一个外部类A的引用,即A.this

静态内部类:静态内部类C,首先是静态的。静态类属于一个类,而不属于类的实例,是多个实例共有的。外部类A不管有多少个实例,都共享同一个C,因此静态内部类不会持有外部类的引用(如果持有的话,它得持有多少个,或者哪一个?!所以是不会持有的。)

二、写在前面的注意事项
1、注意关闭Cursor,InputStream/OutputStream,File文件等;try catch finally, finally里面close,保证一定会走到close();
2、

三、常见内存泄露分析:

内存泄露产生的主要根本原因:
1、该销毁的对象没有销毁;
2、非静态的内部类和匿名类会隐式地持有一个他们外部类的引用;

1、资源未关闭 没有解除注册造成内存泄露

资源性对象比如(Cursor,InputStream/OutputStream,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。

查询数据库时,一定要在finally里面调用cursor.close()保证cursor关闭掉;finally语句之前一定要用catch,要不然不能保证走到finally里面;

需要注册的一定不要忘了解除注册,例如,使用BroadcastReceiver时, registerReceiver()之后要调用unregisterReceiver()进行注销;EventBus,适当的时候解除注册;

webView也要记得销毁:

layout.removeView(mWebView);
mWebView.removeAllViews();
mWebView.destroy();

动画也要及时地停止;

2、Handler内存泄露(非静态内部类造成的泄露)

假如有这样一段代码:

 public class SampleActivity extends Activity {
      private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
          // ... 
        }
      }
    }

尽管不是太明显,这段代码有可能会导致严重的内存泄露。Android Lint也会提示我们:In Android, Handler classes should be static or leaks might occur.

那么为什么要让我们的Handler声明为static的呢,内存泄露产生的原因是什么呢?

简单来说:

上述代码中,Handler不是静态内部类,会持有外部类(Activity)的一个引用。当Handler中有延迟的的任务或是等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被垃圾回收器回收,而导致了内存泄露。引用关系如下:

(主线程的Looper- )延迟的Message - 主线程创建的Handler - Activity

我们来具体分析一下:

Android应用第一次启动时, 系统会在主线程创建Looper对象,Looper实现了一个简单的消息队列,循环处理Message。所有主要的应用层事件(例如Activity的生命周期方法回调、Button点击事件等等)都会包含在Message里,系统会把Message添加到Looper中,然后Looper进行消息循环。主线程的Looper会存在整个应用的生命周期间

当主线程创建Handler对象,会与消息队列Looepr绑定,被分发到消息队列的Message会持有Handler的引用,以便系统在Looper处理到该Message时能调用Handle#handlerMessage(Message)方法。

产生泄露的情形如下:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

当Activity生命周期结束后,延迟的Message在被处理之前会继续停留在主线程中10分钟。该Message持有该Activity的Handler的引用,而Handler持有外部类(SampleActivity )的隐式引用。该引用会继续存在直到Message被处理。所以阻止了Activity和Context的回收,泄漏了所有应用的resources。注意上述代码第15行。非静态类和匿名内部类都会持有外部类的隐式引用,导致了Context泄漏。

为了解决这个问题,可以用新建一个Handler的静态子类。静态内部类不会持有外部类的隐式引用。因此不会导致内存泄漏。如果你需要在Handler子类中调用外部类的方法,可以让Handler持有一个Activity的WeakReference。为了防止内存泄漏,我们新建一个静态的Runnable对象。同时可以在在Activity的onDestory()中调用handler.removeCallbackAndMessages(null)来清理消息。

public class SampleActivity extends Activity {

  /**
   * Instances of static inner classes do not hold an implicit
   * reference to their outer class.
   */
  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

     public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
    }

@Override
public void handleMessage(Message msg) {
  SampleActivity activity = mActivity.get();
  if (activity != null) {
    // ...
  }
}
  }

  private final MyHandler mHandler = new MyHandler(this);

  /**
   * Instances of anonymous classes do not hold an implicit
   * reference to their outer class when they are "static".
   */
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
   protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);

     // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

    // Go back to the previous Activity.
    finish();
  }
}

因此上述Handler的内存泄露,本质上是匿名内部类/非静态内部类持有外部类的引用导致的内存泄露。针对这种内存泄露,我们的解决方案是:

(1)将类声明为静态内部类;
(2)如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;
(3)在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

3、Thread/AsyncTask 线程使用不当导致内存泄露(内部类造成的泄露)

线程导致的内存泄露本质上也是没有使用静态内部类导致的内存泄露,和Handler很像。

内存泄露发生的情形:

当我们的线程的run函数中执行一个很耗时的操作,当我们开启该线程后,改变配置(比如转换屏幕),这时会重新创建Activity,并把旧Activity交给垃圾回收机制进行回收。如果我们的线程被声明为非静态的内部类或者匿名类,那么它会保持有旧Activity的隐式引用。当我们的线程的run函数没有结束时, 线程是不会销毁的,因此导致它所引用的旧的Activity也不会被销毁,并且与该Activity相关的所有资源文件也不会被回收,因此造成严重的内存泄露。

因此总结来看, 线程产生内存泄露的主要原因有两点:
1、线程生命周期的不可控;Activity中的Thread和AsyncTask并不会Activity销毁而销毁;Thread会一直执行run()方法直到该方法结束,AsyncTask的doInBackground()方法同理;
2、非静态的内部类和匿名类会隐式地持有一个他们外部类的引用;

Thread只有在run函数不结束时才出现这种内存泄露,而AsyncTask内部的实现机制是运用了ThreadPoolExcutor,
该类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,
因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。

例1、非静态的内部类造成内存泄露:

public class ThreadTestActivity extends Activity {  
    public void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        new MyThread().start();  
    }  

private class MyThread extends Thread {  
    @Override  
    public void run() {  
        super.run();  
        doSomthing();   
    }  
 }  
}  

例2、匿名内部类造成内存泄露:

new Thread(new Runnable() {
        @Override
        public void run() {
            dosomthing(); 
        }
    }).start();

例3、AsyncTask内存泄露

  new AsyncTask<Void, Void, Void>() {
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
    }.execute();

正确做法示例:前面加static

将线程静态后的优缺点:

前面我们说了可以将线程做成静态的,加上利用弱引用实现线程和Activity的解耦。而静态后出现的一些问题也是需要考虑的:

1)静态类中使用的变量需要静态,有何隐患:
静态类是一直在内存中存在的,占用一定的内存,所以静态变量和类不能过多,否则会导致内存的不足。 但必要时也是不可避免的。

2)静态线程的开启方式,以及当线程需要外部传参的实现:
*线程的启动: 利用创建实例的方式:一个AsyncTask实例只能被执行一次;采用此方式那么传参可以直接通过线程的构造函数即可。

4、单例内存泄露

单例的特性导致它和应用的生命周期一样长。一般泄露发生都是因为传入了一个Activity的Context。

如果需要使用Context,不要传入Activity的Context, 正确的做法是使用Application的Context;

5、Context使用不当导致内存泄露

private static Drawable sBackground;   //注意,static类型的Drawable对象
  @Override  
  protected void onCreate(Bundle state) {  
  super.onCreate(state);        
  TextView label = new TextView(this);  
  label.setText("Leaks are bad");      
  if (sBackground == null) {  
    sBackground = getDrawable(R.drawable.large_bitmap);  
  }  
  label.setBackgroundDrawable(sBackground);      
  setContentView(label);  

在这段代码中,我们使用了一个static的Drawable对象。
这通常发生在我们需要经常调用一个Drawable,而其加载又比较耗时,不希望每次加载Activity都去创建这个Drawable的情况。
此时,使用static无疑是最快的代码编写方式,但是其也非常的糟糕。
当一个Drawable被附加到View时,这个View会被设置为这个Drawable的callback (通过调用Drawable.setCallback()实现)。
这就意味着,这个Drawable拥有一个TextView的引用,而TextView又拥有一个Activity的引用。
这就会导致Activity在销毁后,内存不会被释放。

6、集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

解决方案:

在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。

三、内存泄露总结:

1、对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

2、尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

3、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:

4、将内部类改为静态内部类

5、静态内部类中使用弱引用来引用外部类的成员变量

6、Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.

7、在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。

8、正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

9、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。


References:

http://mp.weixin.qq.com/s?__biz=MzI0MjE3OTYwMg==&mid=2649548387&idx=1&sn=8b826d2d29e688d813d010568ff99d63&chksm=f1180d1ec66f8408b1c7b5899b83767f0ec1c9641114e060d98524ee2ee5df31a43bfbe924bf&scene=21#wechat_redirect

[内存泄露总结]
http://mp.weixin.qq.com/s?__biz=MzI0MjE3OTYwMg==&mid=2649547511&idx=1&sn=8df658f8fc4d4fe7ddc6d11dbc4effb2&scene=21#wechat_redirect

[到底handler为什么会导致内存泄露?]
http://www.jianshu.com/p/63aead89f3b9
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html

[Android App 内存泄露之Thread]
http://blog.csdn.net/zhuanglonghai/article/details/37909553

[android non-static内部类导致的内存泄露]
http://www.jianshu.com/p/6a362ea4dfd8

[Android中糟糕的AsyncTask]
http://www.open-open.com/lib/view/open1417955629527.html

[细话Java:"失效"的private修饰符]
http://droidyue.com/blog/2014/10/02/the-private-modifier-in-java/

《Android内存泄漏的八种可能》

可检测app内存泄露的项目:LeakCanary(可以检测app的内存泄露)

推荐阅读更多精彩内容

  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 1,045评论 0 10
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 881评论 0 7
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 135,738评论 17 577
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 527评论 2 6
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 259评论 0 4