一切從android的handler說起(七)之內存泄露

閱讀本文大概需要 7 分鐘。

 

 

作爲一個客戶端,UI無疑是非常重要的,因此主線程承載了非常多的任務,例如生命週期,View操作,包括Toast,View繪製,動畫,等等,而這些的實現,都依賴於Android的消息機制模型。

 

可見Handler在Android的地位是非常核心的,在源碼中隨處可見它的存在。另一方面,在開發中Handler也可以作爲線程間通信的重要手段,比如在子線程進行邏輯計算,通過向主線程handler發送message,從而達到更新UI的目的。

 

因此,對於Handler的使用,也是我作爲面試官經常考察候選人的經典面試題。

 

我:在開發過程中我們經常在Activity中使用匿名handler對象來接收和處理線程中handler拋出來的message,但是Android Studio會提示有內存泄露的風險,你有碰到嗎?

 

小張:是的,我最開始學習Android的時候就經常碰到這樣的提示,一開始沒當一會兒事,提示的次數多了,就覺得很奇怪IDE爲何如此智能。

 

 

我:嗯,那你知道Android Studio爲什麼會有這樣的提示嗎?

 

小張回答道:因爲匿名內部類默認會持有外部類Activity的引用,這樣當Activity被銷燬時,由於被匿名handler對象所持有而不能被釋放,Activity所佔用的內存就會泄露。

 

我:在聊內存泄露之前,我先問一下,你知道爲什麼匿名內部類默認會持有外部類的引用嗎?

小張楞了一下:這個說多了就熟爛於心了,至於爲什麼,倒是沒怎麼想過呢...

 

我提示道:你有看過包含匿名內部類的java類編譯過後的.class文件嗎?

小張搖了搖頭。

 

我說道:如果你看過的話,你會發現編譯器爲匿名內部類也單獨生成了一份.class文件,而且其類名爲Outer$1,併爲其構造函數添加了一個參數,這個參數就是Outer類的實例,這就是爲什麼說匿名內部類默認會持有外部類的引用。

 

小張說道:哦,原來如此。那非靜態內部類也默認會持有外部類的引用,是不是也是這個原因?

我說道:沒錯,但是靜態內部類就不是這樣了,可以認爲它是一個獨立的類,只不過寫在了Outer類裏,表示這2個類的關係非常緊密。

 

小張說道:看來每天掛在嘴邊的常識,常常容易忽略背後的原因呢。

 

我繼續說道:好了,回到之前的正題來,你說因爲handler這個匿名內部類持有外部Activity的引用,導致Activity銷燬時無法釋放其內存是嗎?

小張答道:是的。

 

我又問:那爲什麼Activity被引用了就無法釋放Activity的內存呢?

 

小張見我繼續問下去,只好答下去:因爲匿名handler實例引用了Activity,handler又被其messsage.target所引用,如果當這個message是以sendMessageDelayed的方式放入message queue的,那麼這個message可能在queue裏存活較長時間,而此期間內如果用戶銷燬了Activity,但由於Activity一直被message引用鏈所引用而得不到釋放。

 

我再問:如果不是用sendMessageDelayed,而是用postMessageDelayed呢?

 

小張答道:一樣,不過這種情況下,不僅messsage.target會持有Activity的引用,同時匿名runnable還會直接引用Activity,而runnable又被message.callback所引用,因此無論用哪種情況下,message都會間接持有Activity的引用。

 

其實對於小張能回答到這一步,在衆多面試者裏已經算是不錯的表現了,不過我並不打算就此輕易放過。

 

我繼續反問道:這樣爲什麼就不能被釋放呢?

小張被問得有點發懵,好像問題答到這裏算是結束了,就說道:我好像不是很理解你想問什麼...

 

基本上在我多年的面試當中,到了這一步還能夠回答得完美的少之又少,可以說不到5%!

 

我見勢,就提示道:java裏的對象之間的引用非常常見,難道只要被引用的對象都不能被內存回收嗎?

小張這才回過神來:哦,你是想問JVM的垃圾回收機制。

 

我說道:沒錯,你還記得JVM是在什麼情況下纔不能釋放一個對象的內存嗎?

小張答道:如果GC Root到這個對象是引用鏈可達的話,那麼此時就不能被GC垃圾回收掉此對象的內存。

 

我說道:嗯,是的,那你覺得在這個案例裏,GC Root的引用鏈能夠到達Activity嗎?你先儘量把上游的引用鏈給列出來。

小張聽我如此一說,開始捋了起來:Activity -> 匿名handler/runnable對象 -> message -> mQueue -> sMainLooper -> sThreadLocal -> 活着的UI線程。

 

我又問道:JVM裏能夠當GC Root的有哪幾種對象?

小張聽後,想了想,最後還是靦腆的說道:我記得有好幾種對象是可以當GC Root的,不過現在只記得方法區裏的類靜態屬性所引用的對象好像可以,其他的情況實在是想不起來了。

 

我說道:嗯,你剛纔說的算一種,其它的情況還有:

1). 虛擬機棧/本地方法棧中JNI中的引用的對象。

2). System Class Loader/Boot Class Loader加載的類對象。

3). 激活狀態的Thread 線程。

4). 方法區中的常量引用的對象,等等。

 

我繼續問道:那你覺得剛纔你列的引用鏈裏面哪個可以當GC Root?

小張回答道:我怎麼感覺好幾都可以當GC Root。sThreadLocal可以當GC Root,因爲它是Looper類的類靜態屬性。UI線程永生,也可以當GC Root,因此這條引用鏈上的所有對象都不能被GC內存回收掉。

 

我說道:我們還是利用leakCanary來分析一下內存泄露,看看真實的引用鏈到底是什麼樣的吧。

 

leakCanary分析結果

 

同時還可以用專業版的MAT或者Android Studio的Profiler來dump heap進行分析和佐證!

 

MAT分析結果

 

Profiler分析結果

 

我說道:你看分析工具都是 Activity -> handler -> message -> queue -> UI線程作爲GC Root引用鏈,是不是和你想象的不一樣?

 

小張很吃驚:真是沒想到結果居然是這樣!

 

我又問道:那根據這個理論,如果我們現在的handler關聯的looper是子線程而非UI線程的話,你覺得還會有內存泄露風險嗎?

 

小張想了一下,說道:那就應該不會了,因爲隨着子線程運行完畢,子線程的Looper和Message queue,handler對象也隨之消亡,這條引用鏈也就斷裂了,Activity銷燬後就可以被GC回收掉!

 

我笑道:嗯,實際上你在IDE裏這麼寫的話,就不會有內存泄露風險的提示出現了。

 

 

我:好了,現在原因我們搞清楚了,那要防止在UI線程使用匿名handler的內存泄露有什麼辦法嗎?

小張答道:可以把Handler聲明爲靜態內部類,這樣就不會默認含有Activity的引用了。

 

我繼續問道:那這樣的話,你如何在其handleMessage裏更新UI呢?

小張答道:哦,看來還是需要持有Activity的引用才行,在Hanlder的構造函數裏傳遞進去。

 

我又繼續問道:這樣豈不是問題又回來了嗎?現在的問題是在Activity銷燬時GC無法回收,這一點上是否可以有什麼辦法,讓其可以被GC強制回收?

小張回答道:咱們可以在Handler裏不直接強引用Activity,而改爲弱引用,這樣在GC時就會釋放掉Activity。

 

我問道:那還有沒有更好的辦法呢?

小張說道:最簡單的方法就是在Activity銷燬時,也立即斷掉這條引用鏈。

 

我追問道:怎麼說?

小張回答道:我們可以在Activity onDestroy()時調用handler.removeCallbacksAndMessages(null),這樣就把queue裏所有的message都remove掉了,之前說過message被message pool回收掉會reset,因此不會再引用handler,這條引用鏈就斷掉了。

 

我說道:嗯,很好,這種方式是最直接的。你終於把來龍去脈全部搞清楚了。我看天色已晚,要不你先回去,後面等我的反饋?

小張有點意猶未盡,便說道:和你一起能學到太多東西了,我能加一下你的微信嗎?

 

我哈哈大笑:個人微信聊起來有些費事兒,如果你想定期學一些乾貨,我會經常在我個人公衆號上分享的,有什麼問題也可以給我私信或者留言。

小張於是問道:好啊,那我掃一下你的二維碼,關注一下你的公衆號,跟你一起學習成長!

 

 

一切從android的handler說起(一)之message

一切從android的handler說起(二)之threadLocal

一切從android的handler說起(三)之UI線程不卡頓

一切從android的handler說起(四)之postDelay原理

一切從android的handler說起(五)之觸摸事件模型

一切從android的handler說起(六)之生命週期來源

 


 

進入公衆號,回覆“程序員“可以領取一份計算機技術電子書福利合集

歡迎轉發,關注公衆號 肖暉

每天幾分鐘,掌握一個硬核面試知識點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章