溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解Handler內(nèi)存泄露

發(fā)布時間:2021-10-21 11:12:47 來源:億速云 閱讀:136 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“如何理解Handler內(nèi)存泄露”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!


這是錯誤的回答

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

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

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

 

內(nèi)存泄漏

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

內(nèi)存泄漏指的就是有些對象(短周期對象)沒有用了,但是卻被其他有用的類(長周期對象)所引用,從而導致無用對象占據(jù)了內(nèi)存空間,形成內(nèi)存泄漏。

所以上面的問題,如果僅僅回答內(nèi)部類持有了外部類的引用,沒有指出內(nèi)部類被誰所引用,那么按道理來說是不會發(fā)生內(nèi)存泄漏的,因為內(nèi)部類和外部類都是無用對象了,是可以被正?;厥?/code>的。

所以這一題的關(guān)鍵在于,內(nèi)部類被引用了?也就是Handler被誰引用了?

一起通過實踐研究下吧~

 

Handler發(fā)生內(nèi)存泄漏的情況

 

1、發(fā)送延遲消息

第一種情況,是通過handler發(fā)送延遲消息:

class MainActivity : AppCompatActivity() {

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

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

class HandlerActivity : AppCompatActivity() {

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

        //發(fā)送延遲消息
        mHandler.sendEmptyMessageDelayed(0, 20000)

        btn2.setOnClickListener {
            finish()
        }
    }

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

我們在HandlerActivity中,發(fā)送一個延遲20s的消息。然后打開HandlerActivity后,馬上finish。看看會不會內(nèi)存泄漏。

 

查看內(nèi)存泄漏并分析

現(xiàn)在查看內(nèi)存泄漏還是蠻方便的了,AndroidStudio自帶對堆轉(zhuǎn)儲(Heap Dump)文件進行分析,并且會把內(nèi)存泄漏點明確標出來。

我們運行項目,點擊Profiler——Memory,就能看到以下圖片了,一個正在運行的內(nèi)存情況實時圖:

如何理解Handler內(nèi)存泄露  
捕獲堆轉(zhuǎn)儲

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

  • 捕獲堆轉(zhuǎn)儲文件按鈕,也就是生成hprof文件,這個文件會展示Java堆的使用情況,點擊這個按鈕后,AndroidStudio會幫我們生成這個堆轉(zhuǎn)儲文件并且進行分析。
  • GC按鈕,一般我們在我們捕獲堆轉(zhuǎn)儲文件之前,點一下GC,就能把一些弱引用給回收,防止給我們分析帶來干擾。

所以我們打開HandlerActivity后,馬上finish,然后點擊GC按鈕,再點擊捕獲堆轉(zhuǎn)儲文件按鈕。AndroidStudio會自動跳轉(zhuǎn)到以下界面:

如何理解Handler內(nèi)存泄露  
分析堆轉(zhuǎn)儲

可以看到左上角有一個Leaks,這就是你內(nèi)存泄漏的點,點擊就能看到內(nèi)存泄漏的類了。右下角就是內(nèi)存泄漏類的引用路徑。

從這張圖可以看到,我們的HandlerActivity發(fā)生了內(nèi)存泄漏,從引用路徑來看,是被匿名內(nèi)部類的實例mHandler持有引用了,而Handler的引用是被Message持有了,Message引用是被MessageQueue持有了...

結(jié)合我們所學的Handler知識和這次引用路徑分析,這次內(nèi)存泄漏完整的引用鏈應(yīng)該是:

主線程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity

所以這次引用的頭頭就是主線程,主線程肯定是不會被回收的,只要是運行中的線程都不會被JVM回收,跟靜態(tài)變量一樣被JVM特殊照顧。

這次內(nèi)存泄漏的原因算是搞清楚了,當然Handler內(nèi)存泄漏的情況不光這一種,看看第二種情況:

 

2、子線程運行沒結(jié)束

第二個實例,是我們常用到的,在子線程中工作,比如請求網(wǎng)絡(luò),然后請求成功后通過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")
        }
    }
}
 

同樣運行后看看內(nèi)存泄漏情況:

如何理解Handler內(nèi)存泄露  
子線程內(nèi)存泄漏

可以發(fā)現(xiàn),這里的內(nèi)存泄漏主要的原因是因為這個運行中的子線程,由于子線程這個匿名內(nèi)部類持有了外部類的引用,而子線程本身是一直在運行的,剛才說過運行中的線程是不會被回收的,所以這里內(nèi)存泄漏的引用鏈應(yīng)該是:

運行中的子線程 —> Activity

當然,這里的Handler也是持有了Activity的引用的,但主要引起內(nèi)存泄漏的原因還是在于子線程本身,就算子線程中不用Handler,而是調(diào)用Activity的其他變量或者方法還是會發(fā)生內(nèi)存泄漏。

所以這種情況我覺得不能看作Handler引起內(nèi)存泄漏的情況,其根本原因是因為子線程引起的,如果解決了子線程的內(nèi)存泄漏,比如在Activity銷毀的時候停止子線程,那么Activity就能正常被回收,那么也不存在Handler的問題了。

 

延伸問題1:內(nèi)部類為什么會持有外部類的引用

這是因為內(nèi)部類雖然和外部類寫在同一個文件中,但是編譯后還是會生成不同的class文件,其中內(nèi)部類的構(gòu)造函數(shù)中會傳入外部類的實例,然后就可以通過this$0訪問外部類的成員。

其實也挺好理解的吧,因為在內(nèi)部類中可以調(diào)用外部類的方法,變量等等,所以肯定會持有外部類的引用的。

貼一段內(nèi)部類在編譯后用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中的內(nèi)部類與Java有什么不一樣嗎

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

btn2.setText("2222")
 

這是因為在kotlin中的匿名內(nèi)部類分為兩種情況:

  • 在Kotlin中,匿名內(nèi)部類如果沒有使用到外部類的對象引用時候,是不會持有外部類的對象引用的,此時的匿名內(nèi)部類其實就是個     靜態(tài)匿名內(nèi)部類,也就不會發(fā)生內(nèi)存泄漏。
  • 在Kotlin中,匿名內(nèi)部類如果使用了對外部類的引用,像我剛才使用了     btn2,這時候就會持有外部類的引用了,就會需要考慮     內(nèi)存泄漏的問題。

所以我特意加了這一句,讓匿名內(nèi)部類持有外部類的引用,復(fù)現(xiàn)內(nèi)存泄漏問題。

同樣kotlin中對于內(nèi)部類也是和Java有區(qū)別的:

  • Kotlin中所有的內(nèi)部類都是默認靜態(tài)的,也就都是     靜態(tài)內(nèi)部類
  • 如果需要調(diào)用外部的對象方法,就需要用     inner修飾,改成和Java一樣的內(nèi)部類,并且會持有外部類的引用,需要考慮內(nèi)存泄漏問題。 

解決內(nèi)存泄漏

說了這么多,那么該怎么解決內(nèi)存泄漏問題呢?其實所有內(nèi)存泄漏的解決辦法都大同小異,主要有以下幾種:

  • 不要讓     長生命周期對象持有     短生命周期對象的引用,而是用     長生命周期對象持有     長生命周期對象的引用。

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

  • 將對象的強引用改成     弱引用

強引用就是對象被強引用后,無論如何都不會被回收。
弱引用就是在垃圾回收時,如果這個對象只被弱引用關(guān)聯(lián)(沒有任何強引用關(guān)聯(lián)他),那么這個對象就會被回收。
軟引用就是在系統(tǒng)將發(fā)生內(nèi)存溢出的時候,回進行回收。
虛引用是對象完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來獲取對象實例,用的比較少。

所以我們將對象改成弱引用,就能保證在垃圾回收時被正?;厥眨热?code>Handler中傳入Activity的弱引用實例:

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

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

跟上面Hanlder情況一樣,有時候內(nèi)部類被不正當使用,容易發(fā)生內(nèi)存泄漏,解決辦法就是寫成外部類或者靜態(tài)內(nèi)部類。

  • 在短周期結(jié)束的時候?qū)⒖赡馨l(fā)生內(nèi)存泄漏的地方移除

比如Handler延遲消息,資源沒關(guān)閉,集合沒清理等等引起的內(nèi)存泄漏,只要在Activity關(guān)閉的時候進行消除即可:

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

“如何理解Handler內(nèi)存泄露”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI