Android Handler 的泄漏测试及分析

文章简介

Android Handler的泄漏算是很有名了,Handler稍有不慎就会造成泄漏。上网一搜就能搜到一大堆解释的文章。但是,大部分其实都在翻译或者解释这篇著名的外文:
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
这篇文章介绍了Handler发送的message以postDelayed的方式驻留在MessageQueue而引起内存泄漏的情况。
配合Handler-Looper-Message机制的理解,看完这篇文章,有一种恍然大悟的激动。

但是!

我们在写handler回发message的时候其实用postDelay的情况也不是占绝大部分,那是不是就不用处理泄漏的情况了呢?
我想啊想,于是想到 线程处理的延时 会不会造成泄漏呢,个人觉得是会的,但是希望求证一下,于是懒得不能自理的我开始在某度和G**gle上搜答案,搜了半天,可能因为上面那篇外文太酷炫,搜出的文章几乎全是讲的是外文中提及的情况。而且在这篇文章中
https://juejin.im/entry/58da161361ff4b0060716f02
作者提及handler泄漏的时候提及 * “只有postDelayed的时候才会有泄露问题,因为delayed的时候activity的引用还保持着,所以只要delayed完了就能回收了,大多数情况下根本不必用加static。” *
这一看我就怂了,因为自己感觉开匿名线程的情况还是挺多,如果线程泄漏的话handler的泄漏还是要处理一下的,可能作者并没有线程不会泄漏的意思,但我这云里雾里的,实在没办法,只好爬起来自己测试一番。于是,这篇文章诞生了。
文章会首先介绍外文提及的泄漏原理及测试,已经熟烂的兄弟姐妹可以直接跳过,后面会介绍线程与handler的配合导致泄漏的原理与测试结果, 大佬们肯定不用测试也心里有数,因此对java回收以及handler机制已经理解透彻的大佬默默地点一下网页右上角的叉叉就好了。
言归正传,本文使用的泄漏测试用的正是你们熟悉的LeakCanary 1.4,那么,现在开始。

Message驻留MessageQueue的泄漏情况

这种情况正是文章开头提到的那篇外文中提及的情况。来看一段代码

public class MainActivity extends AppCompatActivity {
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    private Thread leakThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        leakThread = new Thread(new LeakRunnable(handler));
        leakThread.start();
        Button button = (Button) findViewById(R.id.btn_start);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SecondActivity.StartSecondActivity(MainActivity.this);
                finish();
            }
        });
    }

}

这段代码相当简单,只有三个点

  1. 有一个内部匿名Handler类。
  2. 有一个私有线程成员,leakThread,线程的runnable来自Runnable实现类 LeakRunnable(代码后面贴出,也很简单),并且这个Runnable注入了handler,内部持有handler这个引用。
  3. 有一个button,点击会跳转到别的activity并finish(),这样的话,在正常情况下garbage collector就会在合适的时候回收MainActivity对象。

好,代码看完了,首先明确一点: java的内部类会默认持有外部类的对象引用。在这段代码的表现就是handler会持有MainActivity这个对象的引用。
然后要知道这段代码有两条关键的引用链,
第一条,从这段代码就能看出来的:

mainActivity -(1.1)-> leakThread -(1.2)->  handler -(1.3)-> mainActivity

第二条,从Handler->Looper->MessageQueue机制看出来的:

sMainLooper-(2.1)->mMessageQueue-(2.2)->mMessage-(2.3)->handler-(2.4)->mainActivity

解释一下第二条链是怎么出现的:
主线程拥有一个Looper叫sMainLooper,这个Looper是静态变量,与程序共存亡,而Looper中持有一个MessageQueue的对象,可以看Looper的源码(只贴出了一小部分),里面有个mQueue的成员变量

public final class Looper {
    /*
     * API Implementation Note:
     *
     * This class contains the code required to set up and manage an event loop
     * based on MessageQueue.  APIs that affect the state of the queue should be
     * defined on MessageQueue or Handler rather than on Looper itself.  For example,
     * idle handlers and sync barriers are defined on the queue whereas preparing the
     * thread, looping, and quitting are defined on the looper.
     */

    private static final String TAG = "Looper";

    // sThreadLocal.get() will return null unless you've called prepare().
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    private static Looper sMainLooper;  // guarded by Looper.class

    final MessageQueue mQueue;
}

MessageQueue中持有message对象,同样,源码中有个mMessage的对象

public final class MessageQueue {
    private static final String TAG = "MessageQueue";
    Message mMessages;
}

Message中持有Handler对象, 在handler发送消息时会把持有的handler引用指向发送自己的handler,在源码中这个对象名叫target, 代码就不贴出来啦。
因此出现了上面所说的引用链。

当LeakRunnable的实现是如下图所示的时候,handler发送一个10分钟延迟的消息,造成的就是经典的message驻留在messageQueue引起泄漏的情况。

public class LeakRunnable implements Runnable {
    private Handler handler;
    private Message msg;
    public LeakRunnable(Handler handler){
        this.handler = handler;
        msg = new Message();
    }

    @Override
    public void run() {
       MessageQueue_Message_Leak();
    }
    public void MessageQueue_Message_Leak(){
        msg.what = 0;
        handler.sendMessageDelayed(msg,1000 * 60 * 10);
    }

}

我们可以从代码很容易分析到,当activity需要被回收时,由于message需要在MessageQueue中驻留10分钟,此时第二条引用链无法断开,使得本应该被回收的mainActivity被强引用持有而无法回收。分析到这里,我们运行程序点击start,等几秒就会收到LeakCanary的推送了,看图!

LeakUI.png
handler-message-leak.png

结果正如分析所提到的一样,引用链的(2.2),(2.3),(2.4)节点都出现在了推送上。
这种泄漏情况就分析到这就结束了,还不懂的可以看看链接的外文,文章写得相当清楚,下面进入下一章,分析一个使用handler更新ui的线程在处理耗时操作造成的泄漏情况。

带有耗时操作的线程通过handler更新UI造成泄漏的情况

首先把上一章的引用链再贴一遍,这一章要用到

mainActivity -(1.1)-> leakThread -(1.2)-> handler -(1.3)-> mainActivity
sMainLooper-(2.1)->mMessageQueue-(2.2)->mMessage-(2.3)->handler-(2.4)->mainActivity

测试主界面依然跟上一章一样,不同的是LeakRunnable的run逻辑。

public class LeakRunnable implements Runnable {
    private Handler handler;
    private Message msg;
    public LeakRunnable(Handler handler){
        this.handler = handler;
        msg = new Message();
    }
    @Override
    public void run() {
       Thread_Handler_Leak();
    }
    public void Thread_Handler_Leak(){
        while(true){
            try {
                Thread.sleep(1000 * 10 * 60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这次runnable里面甚至没有使用handler发送消息,仅仅是把主线程的handler注入进来,并且run方法模拟了一个耗时操作。由于没有发送消息,这下跟什么Message,MessageQueue没关系了,也就是(2.2),(2.3)节点断开了。那不会泄漏了吧?
答案当然是否定的。为什么?因为我还没提到过第一条引用链呀。
当handler不发送message的时候第一条引用链还是存在的,试想,如果耗时操作存在,节点(1.2)(1.3)是会长时间存在的。
但聪明的你一定会问:那(1.1)呢?!
没错,(1.1)的存在表明了mainActivity跟leakThread对象的关系有点像循环引用,只是多了个handler作为中间者来桥接,而handler的生命周期在这种情况下完全是依赖于thread或者mainAcitivity的,因此handler对分析泄漏过程不起关键作用。按照现代java gc来说,什么循环引用都是渣渣,我们有可达性算法,标记清除法,不会泄漏!
(关于java垃圾回收这方面不熟悉的可以看看这个
http://www.cnblogs.com/sunniest/p/4575144.html
那么,真的不会泄漏吗?
点击一下界面的start,现在看看LeakCanary的推送:

Thread-Handler-Leak.png

好的,泄漏了。泄漏的正是第一条引用链的整条链。
为什么?因为可达性分析算法依赖定义的GC Root对象,参考java文档
https://www.yourkit.com/docs/java/help/gc_roots.jsp
可知道live Thread是被jvm识别为GC Root的,因此只要leakThread活着,即使activity生命周期已经结束,可达性分析算法会觉得第一条链中整条链的对象均不应该被回收,泄漏就会发生。

这种泄漏应该引起我们注意,因为我们经常都会传入一个handler引用到子线程来通知activity更新ui,而子线程往往都有耗时任务要处理,因此我们写代码的时候很容易就在不知不觉中操作到了内存泄漏的handler。

至于怎么解决?断开引用链呗。怎么断?方法多的是

  1. 比如使用弱引用来引用传进来的handler,这样(1.2)节点就会断开(但这样做需要注意在通知ui更新时对handler的引用判空,不然你的老朋友NullPointException一定会来光顾的,为什么?都有耐心看到这来,你就结合上面说的思考一下呗)。
public class LeakRunnable implements Runnable {
    private WeakReference<Handler> handler;
    private Message msg;
    public LeakRunnable(Handler handler){
        this.handler = new WeakReference<Handler>(handler);
        msg = new Message();
    }

    @Override
    public void run() {
       Thread_Handler_Leak();
    }
    public void Thread_Handler_Leak(){
        while(true){
            try {
                Thread.sleep(1000 * 60 * 10 );
                if(handler.get() != null) {
                    handler.get().sendEmptyMessage(0);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  1. Handler定义为静态内部类,这样做handler就不会持有mainActivity的引用。但这样的话就不方便我们更新ui。因此可以同样地传一个mainActivity的弱引用进去。

  2. 在mainActivity destroy的时候停止线程的工作并回收线程资源。

解决方法我只提供了思路,就不细讲了,各位老铁那么聪明,思考一下肯定就实现了。到这里测试与分析就结束啦。

推荐阅读更多精彩内容