小題大做 | Handler記憶體洩露全面分析

jimuzz發表於2020-12-25

前言

嗨,大家好,問大家一個“簡單”的問題:

Handler記憶體洩露的原因是什麼?

你會怎麼答呢?

這是錯誤的回答

有的朋友看到這個題表示,就這?太簡單了吧。

"內部類持有了外部類的引用,也就是Hanlder持有了Activity的引用,從而導致無法被回收唄。"

其實這樣回答是錯誤的,或者說沒回答到點子上。

記憶體洩漏

Java虛擬機器中使用可達性分析的演算法來決定物件是否可以被回收。即通過GCRoot物件為起始點,向下搜尋走過的路徑(引用鏈),如果發現某個物件或者物件組為不可達狀態,則將其進行回收。

記憶體洩漏指的就是有些物件(短週期物件)沒有用了,但是卻被其他有用的類(長週期物件)所引用,從而導致無用物件佔據了記憶體空間,形成記憶體洩漏。

所以上面的問題,如果僅僅回答內部類持有了外部類的引用,沒有指出內部類被誰所引用,那麼按道理來說是不會發生記憶體洩漏的,因為內部類和外部類都是無用物件了,是可以被正常回收的。

所以這一題的關鍵在於,內部類被引用了?也就是Handler被誰引用了?

一起通過實踐研究下吧~

Handler發生記憶體洩漏的情況

1、傳送延遲訊息

第一種情況,是通過handler傳送延遲訊息:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_handler)

        btn.setOnClickListener {
        	//跳轉到HandlerActivity
            startActivity(Intent(this, HandlerActivity::class.java))
        }
    }
}

class HandlerActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_handler2)

        //傳送延遲訊息
        mHandler.sendEmptyMessageDelayed(0, 20000)

        btn2.setOnClickListener {
            finish()
        }
    }

    val mHandler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            btn2.setText("2222")
        }
    }
}

我們在HandlerActivity中,傳送一個延遲20s的訊息。然後開啟HandlerActivity後,馬上finish。看看會不會記憶體洩漏。

檢視記憶體洩漏並分析

現在檢視記憶體洩漏還是蠻方便的了,AndroidStudio自帶對堆轉儲(Heap Dump)檔案進行分析,並且會把記憶體洩漏點明確標出來。

我們執行專案,點選Profiler——Memory,就能看到以下圖片了,一個正在執行的記憶體情況實時圖:

捕獲堆轉儲

可以看到圖片中有兩個按鈕我標出來了:

  • 捕獲堆轉儲檔案按鈕,也就是生成hprof檔案,這個檔案會展示Java堆的使用情況,點選這個按鈕後,AndroidStudio會幫我們生成這個堆轉儲檔案並且進行分析。
  • GC按鈕,一般我們在我們捕獲堆轉儲檔案之前,點一下GC,就能把一些弱引用給回收,防止給我們分析帶來干擾。

所以我們開啟HandlerActivity後,馬上finish,然後點選GC按鈕,再點選捕獲堆轉儲檔案按鈕。AndroidStudio會自動跳轉到以下介面:

分析堆轉儲

可以看到左上角有一個Leaks,這就是你記憶體洩漏的點,點選就能看到記憶體洩漏的類了。右下角就是記憶體洩漏類的引用路徑。

從這張圖可以看到,我們的HandlerActivity發生了記憶體洩漏,從引用路徑來看,是被匿名內部類的例項mHandler持有引用了,而Handler的引用是被Message持有了,Message引用是被MessageQueue持有了...

結合我們所學的Handler知識和這次引用路徑分析,這次記憶體洩漏完整的引用鏈應該是:

主執行緒 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

所以這次引用的頭頭就是主執行緒,主執行緒肯定是不會被回收的,只要是執行中的執行緒都不會被JVM回收,跟靜態變數一樣被JVM特殊照顧。

這次記憶體洩漏的原因算是搞清楚了,當然Handler記憶體洩漏的情況不光這一種,看看第二種情況:

2、子執行緒執行沒結束

第二個例項,是我們常用到的,在子執行緒中工作,比如請求網路,然後請求成功後通過Handler進行UI更新。

class HandlerActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_handler2)

        //執行中的子執行緒
        thread {
            Thread.sleep(20000)
            mHandler.sendEmptyMessage(0)
        }

        btn2.setOnClickListener {
            finish()
        }
    }

    val mHandler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            btn2.setText("2222")
        }
    }
}

同樣執行後看看記憶體洩漏情況:

子執行緒記憶體洩漏

可以發現,這裡的記憶體洩漏主要的原因是因為這個執行中的子執行緒,由於子執行緒這個匿名內部類持有了外部類的引用,而子執行緒本身是一直在執行的,剛才說過執行中的執行緒是不會被回收的,所以這裡記憶體洩漏的引用鏈應該是:

執行中的子執行緒 —> Activity

當然,這裡的Handler也是持有了Activity的引用的,但主要引起記憶體洩漏的原因還是在於子執行緒本身,就運算元執行緒中不用Handler,而是呼叫Activity的其他變數或者方法還是會發生記憶體洩漏。

所以這種情況我覺得不能看作Handler引起記憶體洩漏的情況,其根本原因是因為子執行緒引起的,如果解決了子執行緒的記憶體洩漏,比如在Activity銷燬的時候停止子執行緒,那麼Activity就能正常被回收,那麼也不存在Handler的問題了。

延伸問題1:內部類為什麼會持有外部類的引用

這是因為內部類雖然和外部類寫在同一個檔案中,但是編譯後還是會生成不同的class檔案,其中內部類的建構函式中會傳入外部類的例項,然後就可以通過this$0訪問外部類的成員。

其實也挺好理解的吧,因為在內部類中可以呼叫外部類的方法,變數等等,所以肯定會持有外部類的引用的。

貼一段內部類在編譯後用JD-GUI檢視的class程式碼,也許你能更好的理解:


//原始碼
class InnerClassOutClass{

    class InnerUser {
       private int age = 20;
    }
}

//class程式碼
class InnerClassOutClass$InnerUser {
    private int age;
    InnerClassOutClass$InnerUser(InnerClassOutClass var1) {
        this.this$0 = var1;
        this.age = 20;
     }
}


延伸問題2:kotlin中的內部類與Java有什麼不一樣嗎

其實可以看到,在上述的程式碼中,我都加了一句

btn2.setText("2222")

這是因為在kotlin中的匿名內部類分為兩種情況:

  • 在Kotlin中,匿名內部類如果沒有使用到外部類的物件引用時候,是不會持有外部類的物件引用的,此時的匿名內部類其實就是個靜態匿名內部類,也就不會發生記憶體洩漏。
  • 在Kotlin中,匿名內部類如果使用了對外部類的引用,像我剛才使用了btn2,這時候就會持有外部類的引用了,就會需要考慮記憶體洩漏的問題。

所以我特意加了這一句,讓匿名內部類持有外部類的引用,復現記憶體洩漏問題。

同樣kotlin中對於內部類也是和Java有區別的:

  • Kotlin中所有的內部類都是預設靜態的,也就都是靜態內部類
  • 如果需要呼叫外部的物件方法,就需要用inner修飾,改成和Java一樣的內部類,並且會持有外部類的引用,需要考慮記憶體洩漏問題。

解決記憶體洩漏

說了這麼多,那麼該怎麼解決記憶體洩漏問題呢?其實所有記憶體洩漏的解決辦法都大同小異,主要有以下幾種:

  • 不要讓長生命週期物件持有短生命週期物件的引用,而是用長生命週期物件持有長生命週期物件的引用。

比如Glide使用的時候傳的上下文不要用Activity而改用Application的上下文。還有單例模式不要傳入Activity上下文。

  • 將物件的強引用改成弱引用

強引用就是物件被強引用後,無論如何都不會被回收。
弱引用就是在垃圾回收時,如果這個物件只被弱引用關聯(沒有任何強引用關聯他),那麼這個物件就會被回收。
軟引用就是在系統將發生記憶體溢位的時候,回進行回收。
虛引用是物件完全不會對其生存時間構成影響,也無法通過虛引用來獲取物件例項,用的比較少。

所以我們將物件改成弱引用,就能保證在垃圾回收時被正常回收,比如Handler中傳入Activity的弱引用例項:

    MyHandler(WeakReference(this)).sendEmptyMessageDelayed(0, 20000)

    //kotlin中內部類預設為靜態內部類
    class MyHandler(var mActivity: WeakReference<HandlerActivity>):Handler(){
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            mActivity.get()?.changeBtn()
        }
    }
  • 內部類寫成靜態類或者外部類

跟上面Hanlder情況一樣,有時候內部類被不正當使用,容易發生記憶體洩漏,解決辦法就是寫成外部類或者靜態內部類。

  • 在短週期結束的時候將可能發生記憶體洩漏的地方移除

比如Handler延遲訊息,資源沒關閉,集合沒清理等等引起的記憶體洩漏,只要在Activity關閉的時候進行消除即可:

@Override
protected void onDestroy() {
  //移除handler所有訊息
  if(mHanlder != null){
		mHandler.removeCallbacksAndMessages(null)
  }
  super.onDestroy();
}

總結

Handler記憶體洩露的原因是什麼?

Handler導致記憶體洩漏一般發生在傳送延遲訊息的時候,當Activity關閉之後,延遲訊息還沒發出,那麼主執行緒中的MessageQueue就會持有這個訊息的引用,而這個訊息是持有Handler的引用,而handler作為匿名內部類持有了Activity的引用,所以就有了以下的一條引用鏈。

主執行緒 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

根本原因是因為這條引用鏈的頭頭,也就是主執行緒,是不會被回收的,所以導致Activity無法被回收,出現記憶體洩漏,其中Handler只能算是導火索。

而我們平時用到的子執行緒通過Handler更新UI,其原因是因為執行中的子執行緒不會被回收,而子執行緒持有了Actiivty的引用(不然也無法呼叫ActivityHandler),所以就導致記憶體洩漏了,但是這個情況的主要原因還是在於子執行緒本身。

所以綜合兩種情況,在發生記憶體洩漏的情況中,Handler都不能算是罪魁禍首,罪魁禍首(根本原因)都是他們的頭頭——執行緒

參考

Handler 記憶體洩露
引用介紹
Kotlin 記憶體洩漏

拜拜

有一起學習的小夥伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回覆111可獲得面試題《思考與解答》以往期刊。

相關文章