1. 程式人生 > >Handler中的記憶體洩露

Handler中的記憶體洩露

zzdjs

場景1

看碼識錯誤1:

class Scene1Activity : AppCompatActivity() {
    private val mHandler = Handler()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //延遲10s
        val msg = Message.obtain()
        msg.what = WHAT_MSG
        mHandler.sendMessageDelayed
(msg, 10000) } companion object { private const val WHAT_MSG = 1 fun startActivity(activity: Activity){ activity.startActivity(Intent(activity, Scene1Activity::class.java)) } } }

程式碼很簡單,只是建立一個mHandler變數,並在onCreate中傳送一個延遲訊息。

那問題來了,這種寫法有沒有潛在的記憶體洩露?換一種說法Scene1Activity

會不會洩露,mHandler會不會洩露?

答案是Scene1Activity沒有洩露,mHandler會有潛在的洩露。

Scene1Activity沒有洩露很好理解,退出Scene1Activity後沒有物件引用它。

那問題又來了,而mHandler為什麼會洩露呢?

我們先看一下mat專業版引用鏈。 在這裡插入圖片描述

抽象手動版引用鏈: 在這裡插入圖片描述

每個Messagetarget都會引用Handler物件,用於處理訊息。Scene1Activity中的mHandler會被髮送的Message物件引用。因此從MainThread(GCRoot)到Handler物件可達。mHandler會被洩露。只有當延遲的訊息被處理以後才會釋放mHandler

物件。

Notice : 預設的Handler方法會獲取當前的執行緒的Looper,Scene1Activity中的mHandler會持有主執行緒的Looper,因此傳送訊息的時候,也是向主執行緒的Looper的MessageQueue新增訊息。

修改此問題可以直接把MessageQueue的到mMessage的引用鏈給咔嚓了,從MainThread(GcRoot)就到Handler物件不可達,自然就不會存在洩漏了。 在這裡插入圖片描述

也就是onDestroy中通過mHandler來移除訊息。

修改後的程式碼:

class Scene1Activity : AppCompatActivity() {
    private val mHandler = Handler()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //延遲10s
        val msg = Message.obtain()
        msg.what = WHAT_MSG
        mHandler.sendMessageDelayed(msg, 10000)
    }
    override fun onDestroy() {
        mHandler.removeMessages(WHAT_MSG)
        super.onDestroy()
    }
    companion object {
        private const val WHAT_MSG = 1
        fun startActivity(activity: Activity){
            activity.startActivity(Intent(activity, Scene1Activity::class.java))
        }
    }
}

Notice:其實Scene1Activity中的mHandler最好在伴生物件中宣告為常量,避免每次進入Scene1Activity中都會建立mHandler,避免記憶體抖動。

場景2

看碼識錯誤2:

class Scene2Activity : AppCompatActivity() {

    private val mHandler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            Toast.makeText(this@Scene2Activity, "haha", Toast.LENGTH_LONG).show()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val msg = Message.obtain()
        msg.what = WHAT_MSG
        mHandler.sendMessageDelayed(msg, 100_000)
    }

    companion object {
        private const val WHAT_MSG = 2
        fun startActivity(activity: Activity) {
            activity.startActivity(Intent(activity, Scene2Activity::class.java))
        }
    }
}

這個呢,猜也能猜對Scene2ActivitymHandler都會潛在的洩露。???。

我們先來看一下mat中的引用鏈: 在這裡插入圖片描述 抽象手動版引用鏈: 在這裡插入圖片描述

MainThreadHandler物件(target),到Scene2Activity物件都是可達的。因此兩者都會被洩露。 我們修改此問題可以直接把MessageQueuemMessage的引用鏈給咔嚓了,從MainThread(GcRoot)到Handler物件,到Scene2Activity物件不可達,自然也就不會存在洩漏了。 在這裡插入圖片描述 也就是在退出時onDestroy中移除Message。 修改後的部分程式碼:

class Scene2Activity : AppCompatActivity() {
	······
    override fun onDestroy() {
        mHandler.removeMessages(WHAT_MSG)
        super.onDestroy()
    }
    ······
}

場景3

在場景2的基礎上有個想法,能不能把Handler物件到Scene2Activity的引用鏈給咔嚓了呢?從而實現讓Scene2Activity可回收呢。 在這裡插入圖片描述

於是乎寫了一個帶有弱引用回撥的Handler

class WeakReferenceHandler<T : WeakReferenceHandler.Callback> : Handler {
    private var mWeakReference: WeakReference<T>

    constructor(callback: T) : super() {
        mWeakReference = WeakReference(callback)
    }

    override fun handleMessage(msg: Message) {
        val callback = mWeakReference.get()
        if (callback != null) {
            callback.handleMessage(msg)
        } else {
            Log.e(TAG, "mWeakReference.get() is null ")
        }
    }

    class Callback {
        fun handleMessage(msg: Message) {}
    }

    companion object {
        private const val TAG = "WeakReferenceHandler"
    }
}

看碼識錯誤3:

class Scene3Activity : AppCompatActivity() {
    private val mHandler = WeakReferenceHandler(object : WeakReferenceHandler.Callback {
        override fun handleMessage(msg: Message) {
            Toast.makeText(this@Scene3Activity, "haha", Toast.LENGTH_LONG).show()
        }
    })

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val msg = Message.obtain()
        msg.what = WHAT_MSG
        mHandler.sendMessageDelayed(msg, 10_000)
    }

    companion object {
        private const val WHAT_MSG = 3
        fun startActivity(activity: Activity) {
            activity.startActivity(Intent(activity, Scene3Activity::class.java))
        }
    }
}

這段程式碼看起來非常完美。???。

只是看起來完美罷了。有個隱藏的炸彈?。我們來分析一下引用鏈。

在這裡插入圖片描述

ActivityThread作為GCRoot是假設的,這裡只是為了方便分析。

Scene3ActivityWeakReferenceHandler建立時直接傳遞匿名的Callback物件。而此Callback物件僅僅被mHandler持有弱引用。只有弱引用的物件只能存活到下次GC之前,一旦GC,只有弱引用的物件就會被回收。因此一旦GC,我們的Toast就不會彈了。???。驚不驚喜,意不意外。

修改的話讓Scene3Activity中建立一個內部的變數來強引用匿名的Callback物件。

修改後的部分程式碼:

class Scene3Activity : AppCompatActivity() {
	private val mCallback = object : WeakReferenceHandler.Callback {
        override fun handleMessage(msg: Message) {
            Toast.makeText(this@Scene3Activity, "haha", Toast.LENGTH_LONG).show()
        }
    }
    private val mHandler = WeakReferenceHandler(mCallback)
    ······
}

修改後的應用鏈:

在這裡插入圖片描述

由於Scene3Activity強引用了Callback的匿名物件。因此可以正常了。

Notice:這一節也就分析分析就可以了,還是要正確的使用Handler。像這樣的弱引用的方式其實沒必要。

總結

Handler的記憶體洩露基本都是傳送延遲訊息導致的。注意恰當的時機移除訊息,就可以避免記憶體洩露了。