文章簡介
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)簡單荆忍,只有三個點
- 有一個內(nèi)部匿名Handler類。
- 有一個私有線程成員,leakThread,線程的runnable來自Runnable實現(xiàn)類 LeakRunnable(代碼后面貼出撤缴,也很簡單)刹枉,并且這個Runnable注入了handler,內(nèi)部持有handler這個引用屈呕。
- 有一個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的推送了碾盟,看圖棚辽!
結(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的推送:
好的扑眉,泄漏了。泄漏的正是第一條引用鏈的整條鏈赖钞。
為什么腰素?因為可達(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呆抑。
至于怎么解決?斷開引用鏈唄汁展。怎么斷鹊碍?方法多的是
- 比如使用弱引用來引用傳進(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();
}
}
}
}
Handler定義為靜態(tài)內(nèi)部類,這樣做handler就不會持有mainActivity的引用铲汪。但這樣的話就不方便我們更新ui树姨。因此可以同樣地傳一個mainActivity的弱引用進(jìn)去。
在mainActivity destroy的時候停止線程的工作并回收線程資源桥状。
解決方法我只提供了思路帽揪,就不細(xì)講了,各位老鐵那么聰明辅斟,思考一下肯定就實現(xiàn)了转晰。到這里測試與分析就結(jié)束啦。