解讀在Activity中使用Handler的內(nèi)存泄漏問(wèn)題


在開(kāi)發(fā)過(guò)程中,我們經(jīng)常會(huì)遇到這樣一種情況,當(dāng)在Activity中使用handler時(shí),直接創(chuàng)建匿名內(nèi)部類,會(huì)得到一個(gè)警告,意思是可能出現(xiàn)內(nèi)存泄漏,推薦使用靜態(tài)內(nèi)部類。這也是面試時(shí)經(jīng)常被問(wèn)的一個(gè)問(wèn)題,現(xiàn)在,我們就來(lái)解讀一下為什么會(huì)出現(xiàn)這個(gè)警告,以及如何改進(jìn)。

我們知道,Handler在使用時(shí),通過(guò)post或者send的方式,可以把消息發(fā)送到MessageQueue隊(duì)列中,期間Looper循環(huán)取出消息去交給對(duì)應(yīng)的handler所在的線程去處理,有些消息還加上了延時(shí)發(fā)送,這些原因就可能會(huì)導(dǎo)致一個(gè)問(wèn)題:當(dāng)Activity銷毀了,而主線程中一直運(yùn)行的Looper會(huì)持有handler的引用,而我們?cè)趧?chuàng)建handler的時(shí)候用的是非靜態(tài)匿名內(nèi)部類,所以此handler會(huì)持有Activity的引用,導(dǎo)致Activity不會(huì)被回收,出現(xiàn)了內(nèi)存泄漏。因此,編譯器才會(huì)給我們一個(gè)這樣的警告。

可能有些朋友會(huì)疑惑為什么Looper會(huì)持有handler的引用,這就要看源碼了:

不管我們調(diào)用post還是send方式發(fā)送消息,最終調(diào)用的都是sendMessageAtTime方法,而在這個(gè)方法中最后調(diào)用了enqueueMessage方法,我們來(lái)看一下Handler中enqueueMessage的源碼:

可以看到msg.target被賦值給本類對(duì)象,也就是我們?cè)赼ctivity中創(chuàng)建的handler。也就是說(shuō)我們?cè)诎l(fā)送Message時(shí),這個(gè)消息就把handler給封裝進(jìn)了自己的內(nèi)部,只要Message對(duì)象不被銷毀,此handler對(duì)象就會(huì)一直存在,而此Message會(huì)一直存儲(chǔ)在MessageQueue消息隊(duì)列中,直到Looper取出交給handler處理??梢钥吹絃ooper的loop()方法的源碼如下:

交給handler處理完后會(huì)回收該消息:

這樣Looper才能斷開(kāi)與Handler對(duì)象的引用,直到這個(gè)時(shí)候,外部的Activity才可能被回收。因此,在使用handler的時(shí)候,不建議再這么使用了。

那么我們?cè)撛鯓诱_的使用呢?

可以將Handler聲明為Activity里的一個(gè)靜態(tài)內(nèi)部類,內(nèi)部創(chuàng)建一個(gè)WeakReference,將Activity實(shí)例用弱引用存起來(lái),這樣就可以了。如果為了方便復(fù)用,也可以將Handler單獨(dú)提出來(lái)作為一個(gè)類,原理相同。如下:

采用靜態(tài)內(nèi)部類或者單獨(dú)提出來(lái)的作用是讓handler對(duì)象不再持有activity對(duì)象的引用,而對(duì)外部activity的使用都是采用弱引用的方式,當(dāng)垃圾回收器發(fā)現(xiàn)此activity只有弱引用時(shí),就會(huì)回收它,這樣就不會(huì)導(dǎo)致內(nèi)存泄漏了。

好了,這個(gè)問(wèn)題就解讀到這里。下面我們?cè)賮?lái)看看一些相關(guān)的題外知識(shí)。

我們平常在使用Looper的時(shí)候,必須先調(diào)用Looper.prepare()方法,因?yàn)榇朔椒〞?huì)初始化一個(gè)Looper(并初始化一個(gè)MessageQueue)并和當(dāng)前線程綁定,只有這些初始化工作完成后,后邊我們才能使用這些東西;然后需要調(diào)用Looper.loop()把MessageQueue消息隊(duì)列在這個(gè)線程中運(yùn)行起來(lái)。下面是prepare方法和創(chuàng)建looper實(shí)例的源碼,其中sThreadLocal.set(new Looper(quitAllowed)實(shí)現(xiàn)了當(dāng)前線程和looper的綁定。

.

但是平時(shí)我們?cè)贏ctivity中使用時(shí),并未先調(diào)用prepare然后調(diào)用loop方法,為什么呢?我們來(lái)看看ActivityThread的main方法,此方法是應(yīng)用程序的入口,源碼如下:

根據(jù)源碼得出,原來(lái)APP在啟動(dòng)的時(shí)候,系統(tǒng)已經(jīng)自動(dòng)為我們做了相關(guān)的初始化工作,這里的ActivityThread就是主線程(UI線程),這樣我們?cè)贏ctivity中就可以直接創(chuàng)建handler使用了。當(dāng)然我們也可以創(chuàng)建自己的Looper,下面我們來(lái)演示子線程中的Handler和主線程中的Handler的使用。

老樣子,我們使用靜態(tài)內(nèi)部類,提供兩個(gè)構(gòu)造函數(shù),無(wú)參的給主線程創(chuàng)建handler,有參數(shù)的給子線程用,只需提供一個(gè)在子線程中初始化的Loope即可。在handleMessage回調(diào)方法里打印出當(dāng)前線程。

然后在主線程和子線程中分別創(chuàng)建各自的實(shí)例:

然后在另外一個(gè)子線程中使用兩個(gè)handler發(fā)送消息:

可以看到打印的日志:

由此可以看出不管handler是在哪個(gè)線程中使用,發(fā)送的消息都會(huì)存儲(chǔ)在創(chuàng)建此handler時(shí)對(duì)應(yīng)的Looper所屬的線程中(MessageQueue中),并最后交給這個(gè)handler的handleMessage方法進(jìn)行處理,這樣實(shí)現(xiàn)了線程之間的通信。


最后總結(jié):

1.在Activity中直接使用非靜態(tài)內(nèi)部類的Handler,提示會(huì)出現(xiàn)內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類對(duì)象會(huì)持有外部類對(duì)象的引用,如果此內(nèi)部對(duì)象一直被引用著,就會(huì)導(dǎo)致外部類對(duì)象不會(huì)被回收。要么聲明為靜態(tài),要么提出去單獨(dú)作為一個(gè)文件類。并對(duì)Activity采用弱引用的方式來(lái)使用。

2.應(yīng)用程序在啟動(dòng)階段已經(jīng)替我們做了Looper的初始化工作,我們?cè)贏ctivity中可以直接使用Handler。但是如果想在子線程中創(chuàng)建handler,必須先調(diào)用Looper.prepare()方法,然后調(diào)用Looper.loop(),這樣才能使用handler。

3.線程--Looper--MessqgeQueue是一一對(duì)應(yīng)的,由msg.target可以得出一個(gè)線程中可以創(chuàng)建多個(gè)handler,handler的使用(發(fā)送消息)可以在任何線程中,但是消息最終都是在創(chuàng)建此handler時(shí)對(duì)應(yīng)的Looper所屬線程中被處理的。這樣也就實(shí)現(xiàn)了線程間通信。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容