Handler中的記憶體洩露
場景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專業版引用鏈。
抽象手動版引用鏈:
每個Message
的target
都會引用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))
}
}
}
這個呢,猜也能猜對Scene2Activity
,mHandler
都會潛在的洩露。???。
我們先來看一下mat
中的引用鏈:
抽象手動版引用鏈:
從MainThread
到Handler
物件(target
),到Scene2Activity
物件都是可達的。因此兩者都會被洩露。
我們修改此問題可以直接把MessageQueue
的mMessage
的引用鏈給咔嚓了,從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是假設的,這裡只是為了方便分析。
在Scene3Activity
中WeakReferenceHandler
建立時直接傳遞匿名的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
的記憶體洩露基本都是傳送延遲訊息導致的。注意恰當的時機移除訊息,就可以避免記憶體洩露了。