Android Handler 的泄漏測試及分析

文章簡介

Android Handler的泄漏算是很有名了枕稀,Handler稍有不慎就會造成泄漏杀赢。上網(wǎng)一搜就能搜到一大堆解釋的文章辨萍。但是棋恼,大部分其實都在翻譯或者解釋這篇著名的外文:
http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
這篇文章介紹了Handler發(fā)送的message以postDelayed的方式駐留在MessageQueue而引起內(nèi)存泄漏的情況。
配合Handler-Looper-Message機制的理解分瘦,看完這篇文章,有一種恍然大悟的激動琉苇。

但是嘲玫!

我們在寫handler回發(fā)message的時候其實用postDelay的情況也不是占絕大部分,那是不是就不用處理泄漏的情況了呢并扇?
我想啊想去团,于是想到 線程處理的延時 會不會造成泄漏呢,個人覺得是會的,但是希望求證一下土陪,于是懶得不能自理的我開始在某度和G**gle上搜答案昼汗,搜了半天,可能因為上面那篇外文太酷炫鬼雀,搜出的文章幾乎全是講的是外文中提及的情況顷窒。而且在這篇文章中
https://juejin.im/entry/58da161361ff4b0060716f02
作者提及handler泄漏的時候提及 * “只有postDelayed的時候才會有泄露問題,因為delayed的時候activity的引用還保持著源哩,所以只要delayed完了就能回收了鞋吉,大多數(shù)情況下根本不必用加static±常” *
這一看我就慫了谓着,因為自己感覺開匿名線程的情況還是挺多,如果線程泄漏的話handler的泄漏還是要處理一下的坛掠,可能作者并沒有線程不會泄漏的意思赊锚,但我這云里霧里的,實在沒辦法屉栓,只好爬起來自己測試一番舷蒲。于是,這篇文章誕生了系瓢。
文章會首先介紹外文提及的泄漏原理及測試阿纤,已經(jīng)熟爛的兄弟姐妹可以直接跳過,后面會介紹線程與handler的配合導(dǎo)致泄漏的原理與測試結(jié)果, 大佬們肯定不用測試也心里有數(shù)夷陋,因此對java回收以及handler機制已經(jīng)理解透徹的大佬默默地點一下網(wǎng)頁右上角的叉叉就好了欠拾。
言歸正傳,本文使用的泄漏測試用的正是你們熟悉的LeakCanary 1.4骗绕,那么藐窄,現(xiàn)在開始。

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();
            }
        });
    }

}

這段代碼相當(dāng)簡單荆忍,只有三個點

  1. 有一個內(nèi)部匿名Handler類。
  2. 有一個私有線程成員,leakThread,線程的runnable來自Runnable實現(xiàn)類 LeakRunnable(代碼后面貼出撤缴,也很簡單)刹枉,并且這個Runnable注入了handler,內(nèi)部持有handler這個引用屈呕。
  3. 有一個button微宝,點擊會跳轉(zhuǎn)到別的activity并finish(),這樣的話虎眨,在正常情況下garbage collector就會在合適的時候回收MainActivity對象蟋软。

好镶摘,代碼看完了,首先明確一點: java的內(nèi)部類會默認(rèn)持有外部類的對象引用岳守。在這段代碼的表現(xiàn)就是handler會持有MainActivity這個對象的引用凄敢。
然后要知道這段代碼有兩條關(guān)鍵的引用鏈,
第一條湿痢,從這段代碼就能看出來的:

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

解釋一下第二條鏈?zhǔn)窃趺闯霈F(xiàn)的:
主線程擁有一個Looper叫sMainLooper,這個Looper是靜態(tài)變量,與程序共存亡蒙袍,而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發(fā)送消息時會把持有的handler引用指向發(fā)送自己的handler,在源碼中這個對象名叫target, 代碼就不貼出來啦以现。
因此出現(xiàn)了上面所說的引用鏈狠怨。

當(dāng)LeakRunnable的實現(xiàn)是如下圖所示的時候,handler發(fā)送一個10分鐘延遲的消息邑遏,造成的就是經(jīng)典的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);
    }

}

我們可以從代碼很容易分析到,當(dāng)activity需要被回收時记盒,由于message需要在MessageQueue中駐留10分鐘憎蛤,此時第二條引用鏈無法斷開,使得本應(yīng)該被回收的mainActivity被強引用持有而無法回收纪吮。分析到這里俩檬,我們運行程序點擊start,等幾秒就會收到LeakCanary的推送了碾盟,看圖棚辽!

LeakUI.png
handler-message-leak.png

結(jié)果正如分析所提到的一樣,引用鏈的(2.2)冰肴,(2.3)屈藐,(2.4)節(jié)點都出現(xiàn)在了推送上。
這種泄漏情況就分析到這就結(jié)束了熙尉,還不懂的可以看看鏈接的外文联逻,文章寫得相當(dāng)清楚,下面進(jìn)入下一章检痰,分析一個使用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發(fā)送消息,僅僅是把主線程的handler注入進(jìn)來谭贪,并且run方法模擬了一個耗時操作境钟。由于沒有發(fā)送消息,這下跟什么Message,MessageQueue沒關(guān)系了俭识,也就是(2.2),(2.3)節(jié)點斷開了慨削。那不會泄漏了吧?
答案當(dāng)然是否定的套媚。為什么缚态?因為我還沒提到過第一條引用鏈呀。
當(dāng)handler不發(fā)送message的時候第一條引用鏈還是存在的堤瘤,試想玫芦,如果耗時操作存在,節(jié)點(1.2)(1.3)是會長時間存在的本辐。
但聰明的你一定會問:那(1.1)呢桥帆?!
沒錯慎皱,(1.1)的存在表明了mainActivity跟leakThread對象的關(guān)系有點像循環(huán)引用老虫,只是多了個handler作為中間者來橋接,而handler的生命周期在這種情況下完全是依賴于thread或者mainAcitivity的茫多,因此handler對分析泄漏過程不起關(guān)鍵作用祈匙。按照現(xiàn)代java gc來說,什么循環(huán)引用都是渣渣天揖,我們有可達(dá)性算法夺欲,標(biāo)記清除法,不會泄漏宝剖!
(關(guān)于java垃圾回收這方面不熟悉的可以看看這個
http://www.cnblogs.com/sunniest/p/4575144.html
那么洁闰,真的不會泄漏嗎?
點擊一下界面的start万细,現(xiàn)在看看LeakCanary的推送:

Thread-Handler-Leak.png

好的扑眉,泄漏了。泄漏的正是第一條引用鏈的整條鏈赖钞。
為什么腰素?因為可達(dá)性分析算法依賴定義的GC Root對象,參考java文檔
https://www.yourkit.com/docs/java/help/gc_roots.jsp
可知道live Thread是被jvm識別為GC Root的雪营,因此只要leakThread活著弓千,即使activity生命周期已經(jīng)結(jié)束,可達(dá)性分析算法會覺得第一條鏈中整條鏈的對象均不應(yīng)該被回收献起,泄漏就會發(fā)生洋访。

這種泄漏應(yīng)該引起我們注意镣陕,因為我們經(jīng)常都會傳入一個handler引用到子線程來通知activity更新ui,而子線程往往都有耗時任務(wù)要處理姻政,因此我們寫代碼的時候很容易就在不知不覺中操作到了內(nèi)存泄漏的handler呆抑。

至于怎么解決?斷開引用鏈唄汁展。怎么斷鹊碍?方法多的是

  1. 比如使用弱引用來引用傳進(jìn)來的handler,這樣(1.2)節(jié)點就會斷開(但這樣做需要注意在通知ui更新時對handler的引用判空食绿,不然你的老朋友NullPointException一定會來光顧的侈咕,為什么?都有耐心看到這來器紧,你就結(jié)合上面說的思考一下唄)耀销。
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定義為靜態(tài)內(nèi)部類,這樣做handler就不會持有mainActivity的引用铲汪。但這樣的話就不方便我們更新ui树姨。因此可以同樣地傳一個mainActivity的弱引用進(jìn)去。

  2. 在mainActivity destroy的時候停止線程的工作并回收線程資源桥状。

解決方法我只提供了思路帽揪,就不細(xì)講了,各位老鐵那么聰明辅斟,思考一下肯定就實現(xiàn)了转晰。到這里測試與分析就結(jié)束啦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末士飒,一起剝皮案震驚了整個濱河市查邢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酵幕,老刑警劉巖扰藕,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異芳撒,居然都是意外死亡邓深,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門笔刹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來芥备,“玉大人,你說我怎么就攤上這事舌菜∶瓤牵” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長袱瓮。 經(jīng)常有香客問我缤骨,道長,這世上最難降的妖魔是什么尺借? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任荷憋,我火速辦了婚禮,結(jié)果婚禮上褐望,老公的妹妹穿的比我還像新娘。我一直安慰自己串前,他們只是感情好瘫里,可當(dāng)我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荡碾,像睡著了一般谨读。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坛吁,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天劳殖,我揣著相機與錄音,去河邊找鬼拨脉。 笑死哆姻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的玫膀。 我是一名探鬼主播矛缨,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼帖旨!你這毒婦竟也來了箕昭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤解阅,失蹤者是張志新(化名)和其女友劉穎落竹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體货抄,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡述召,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蟹地。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片桨武。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖锈津,靈堂內(nèi)的尸體忽然破棺而出呀酸,到底是詐尸還是另有隱情,我是刑警寧澤琼梆,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布性誉,位于F島的核電站窿吩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏错览。R本人自食惡果不足惜纫雁,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望倾哺。 院中可真熱鬧轧邪,春花似錦、人聲如沸羞海。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽却邓。三九已至硕糊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腊徙,已是汗流浹背简十。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留撬腾,地道東北人螟蝙。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像民傻,于是被迫代替她去往敵國和親胶逢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容