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的时候停止线程的工作并回收线程资源。

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

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

推荐阅读更多精彩内容