小題大作 | Handler內存泄露全面分析

點擊上方藍字關注我,天天一見,給你力量html


前言

嗨,你們好,問你們一個「簡單」的問題:java

Handler內存泄露的緣由是什麼?web

你會怎麼答呢?面試

這是錯誤的回答

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

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

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

內存泄漏

Java虛擬機中使用可達性分析的算法來決定對象是否能夠被回收。即經過GCRoot對象爲起始點,向下搜索走過的路徑(引用鏈),若是發現某個對象或者對象組爲不可達狀態,則將其進行回收。編輯器

內存泄漏指的就是有些對象(短週期對象)沒有用了,可是卻被其餘有用的類(長週期對象)所引用,從而致使無用對象佔據了內存空間,造成內存泄漏。ide

因此上面的問題,若是僅僅回答內部類持有了外部類的引用,沒有指出內部類被誰所引用,那麼按道理來講是不會發生內存泄漏的,由於內部類和外部類都是無用對象了,是能夠被正常回收的。函數

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

一塊兒經過實踐研究下吧~

Handler發生內存泄漏的狀況

一、發送延遲消息

第一種狀況,是經過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(020000)

        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內存泄漏的狀況不光這一種,看看第二種狀況:

二、子線程運行沒結束

第二個實例,是咱們經常使用到的,在子線程中工做,好比請求網絡,而後請求成功後經過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(020000)

    //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都不能算是罪魁禍首,罪魁禍首(根本緣由)都是他們的頭頭——線程

參考

https://www.cnblogs.com/shoshana-kong/p/10449648.html
https://www.jianshu.com/p/825cca41d962
https://www.jianshu.com/p/0ee88812d73e

拜拜

感謝你們的閱讀,有一塊兒學習的小夥伴能夠關注下公衆號—碼上積木❤️

每日三問知識點/面試題,聚沙成塔。



在看你最好看


本文分享自微信公衆號 - 碼上積木(Lzjimu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索