天天看點

Android安全且無洩露Handler

作者:莫大叔雜談

Handler引發的洩露(通常發生在Activity、Fragment等容器)、crash是Android開發中常見的問題,也是面試時非常容易被問到的技術點。關于Handler為何會引起容器洩露,網上有很多的文章,這裡就簡單提一下引用鍊:

Thread->ThreadLocal->Looper->MessageQueue->Message->Handler->Activity

Handler産生的洩露一般是暫時的,當消息成功排程後,從消息隊列中移除,上面的引用鍊也便就不存在了,在下次gc時,Activity便可以正常釋放。是以大多情況下,Handler引起的洩露問題并不可怕(極端情況另說),可怕的是引起crash。下面重點讨論下Handler如何引起crash,看個僞代碼:

class TestFragment: Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Toast.makeText(activity, "AAA", Toast.LENGTH_SHORT).show()
        Handler().postDelayed({
            Toast.makeText(activity, "BBB", Toast.LENGTH_SHORT).show()
        }, 5000)
        parentFragmentManager.beginTransaction().run {
            remove(this@TestFragment)
            commitAllowingStateLoss()
        }
    }
}           

啟動TestFragment後,會先看到一條"AAA"的吐司,然後在logcat中看到如下crash日志:

java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getPackageName()' on a null object reference

at android.widget.Toast.<init>(Toast.java:121)

at android.widget.Toast.makeText(Toast.java:286)

at android.widget.Toast.makeText(Toast.java:276)

at com.ada.test_app.TestFragment.onCreate$lambda-0(MainActivity.kt:136)

at com.ada.test_app.TestFragment.$r8$lambda$ZvBB3ieIi-neYI0Ok2qP--pCPEg(Unknown Source:0)

at com.ada.test_app.TestFragment$ExternalSyntheticLambda0.run(Unknown Source:2)

at android.os.Handler.handleCallback(Handler.java:883)

at android.os.Handler.dispatchMessage(Handler.java:100)

at android.os.Looper.loop(Looper.java:238)

at android.app.ActivityThread.main(ActivityThread.java:7798)

at java.lang.reflect.Method.invoke(Native Method)

at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:995)

原因是當Fragment生命周期結束時,會将activity對象置為null,等消息延遲排程時,取得的便是一個空的activity,是以出現了空指針異常。解決的辦法很簡單,加個空判斷就好了。然而現實中的場景會複雜很多,而且開發人員素質參差不齊,沒法保證所有場景都正确處理了,我們希望能有一套通用的解決方案。以下是筆者寫的SafeHandler,在實際項目中已經廣泛使用,是一個小而美的元件:

class SafeHandler(owner: LifecycleOwner, looper: Looper = Looper.getMainLooper()): Handler(looper), LifecycleObserver {
    private val host = WeakReference(owner)

    init {
        owner.lifecycle.addObserver(this)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        removeCallbacksAndMessages(null)
    }

    override fun dispatchMessage(msg: Message) {
        val owner = host.get()
        if (owner != null && owner.lifecycle.currentState != Lifecycle.State.DESTROYED) {
            super.dispatchMessage(msg)
        }
    }
}

fun handlerOf(owner: LifecycleOwner, looper: Looper = Looper.getMainLooper()): Handler {
    return SafeHandler(owner, looper)
}

fun LifecycleOwner.newHandler(looper: Looper = Looper.getMainLooper()): Handler {
    return handlerOf(this, looper)
}           

分析下代碼:

  • 這裡looper預設為主線程looper,而建構系統的Handler在沒有設定looper時,預設是擷取目前線程looper。從Handler的通用性來說這樣設計沒有問題,但從業務的角度來說,我們使用的Handler絕大多數是位于主線程中,是以這樣設計會更安全一些,避免一些開發者因為對Handler的機制不夠了解而使用預設建構方法建構出了錯誤的Handler。
  • LifecycleOwner使用弱引用存儲,SafeHandler本身就是為了解決記憶體洩露及crash,當然不能因為自身的缺陷導緻另外的洩露了。
  • 監聽LifecycleOwner的銷毀,在銷毀時清除所有消息。
  • 在排程消息時判斷LifecycleOwner的狀态,如果已經銷毀,就不允許執行。既然已經在onDestroy時清空消息了,為什麼還要做這步操作呢?這是因為外部有可能在onDestroy後依然使用Handler去發送消息。

繼續閱讀