Android中Handler内存泄漏分析及解决

1.什么是内存泄漏?

Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收。也就是说,一个对象不被任何引用所指向,则该对象会在被GC发现的时候被回收;另外,如果一组对象中只包含互相的引用,而没有来自它们外部的引用(例如有两个对象A和B互相持有引用,但没有任何外部对象持有指向A或B的引用),这仍然属于不可到达,同样会被GC回收。

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

2.Android中使用Handler造成内存泄露原因分析

(1)Handler使用方法

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
};

在使用handler时,这是一段很常见的代码。但是,它却会造成严重的内存泄漏问题。在实际编写中,我们往往会得到如下警告:

This Handler class should be static or leaks might occur (anonymous android.os.Handler)

Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected. If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue. If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows: Declare the Handler as a static class; In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler; Make all references to members of the outer class using the WeakReference object.

(2)内部类mHandler

简单的内部类如下:

class OuterClass { 
    class InnerClass{ 
    }
}

以上代码mHandler让人并不觉得是内部类,它并不像InnerClass那样形象,但是其实以下句柄实现一个继承Handler的类,也就是自定义了一个类,那么明显它就是一个内部类。其实它是属于内部类一种:匿名内部类Anonymous Inner Class

{
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
}

(3)Handler造成内存泄漏分析

当Android应用程序启动时,Framework会为该应用程序的主线程创建一个Looper对象。这个Looper对象包含一个简单的消息队列Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序Framework事件,例如Activity生命周期方法调用、点击事件等,这些消息都会被添加到消息队列中并被逐个处理。另外,主线程的Looper对象会伴随该应用程序的整个生命周期。

当在主线程中初始化Handler时,该Handler就会自动和主线程Looper的消息队列关联起来。所有发送到消息队列的消息Message都会拥有一个对Handler的引用,所以当Looper来处理消息时,会据此回调Handler的handleMessage(Message)方法来分发处理该消息。

在Java里,非静态内部类和匿名内部类都会潜在的引用它们所属的外部类。但是,静态内部类不会引用外部类对象。

当使用内部类(包括匿名内部类)来创建Handler的时候,Handler对象会持有外部类对象(通常是一个Activity)的引用(不然怎么通过Handler来操作Activity中的View?)。而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。

如果执行了Handler的postDelayed()方法,该方法会将Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致Activity被持有引用而无法被GC回收。

(4)内存泄漏的危害

虚拟机占用内存过高,导致OOM(内存溢出),程序出错。对于Android应用来说,就是用户打开一个Activity,使用完之后关闭它,内存泄露;又打开,又关闭,又泄露;几次之后,程序占用内存超过系统限制。

3.Handler导致内存泄漏解决方法

方法一:通过程序逻辑进行保护

1.在关闭Activity的时候停掉后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被GC回收。

2.如果Handler是被delay的Message持有了引用,那么使用Handler的removeCallbacks()方法,把消息对象从消息队列移除就行了。

方法二:将Handler声明为静态类

静态类不持有外部类的对象,所以Activity可以随意被回收。代码如下:

private static class MyHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
    }
}

但其实没这么简单。使用了以上代码之后,你会发现,由于Handler不再持有外部类对象的引用,导致程序不允许你在Handler中操作Activity中的对象了。所以你需要在Handler中增加一个对Activity的弱引用(WeakReference):

private final MyHandler mHandler = new MyHandler(this);
private static class MyHandler extends Handler {
    private final WeakReference<Activity> mActivity;
    public MyHandler(Activity activity) {
      mActivity = new WeakReference<Activity>(activity);
    }

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

对于匿名类Runnable,同样可以将其设置为静态类:

private static final Runnable mRunnable = new Runnable() {
      @Override
      public void run() { 
          // doSomething
      }
  };

4.什么是WeakReference?

WeakReference弱引用,与强引用(即我们常说的引用)相对,它的特点是,GC在回收时会忽略掉弱引用,即就算有弱引用指向某对象,但只要该对象没有被强引用指向(实际上多数时候还要求没有软引用,但此处软引用的概念可以忽略),该对象就会在被GC检查到时回收掉。对于上面的代码,用户在关闭Activity之后,就算后台线程还没结束,但由于仅有一条来自Handler的弱引用指向Activity,所以GC仍然会在检查的时候把Activity回收掉。这样,内存泄露的问题就不会出现了。

推荐阅读更多精彩内容