iT邦幫忙

1

透過 EventBus 解決 TransactionTooLargeException 問題

最近在 App 裡面加上了紀錄使用者登山軌跡的功能後,上線的第一個週末 Firebase Crashlytics 就冒出來 20 個以上的 crash log,尿都快嚇出來了...

看了一下 log 後發現雖然引發的位置不同,但是全部都指向同一個問題

Caused by android.os.TransactionTooLargeException: data parcel size 2764576 bytes
       at android.os.BinderProxy.transactNative(BinderProxy.java)
       at android.os.BinderProxy.transact(BinderProxy.java:510)
       ... 以下省略

TransactionTooLargeException 的原因

官方在 TransactionTooLargeException 的文檔中有提到 Binder transaction buffer 有 1MB 的限制


[color=#79bf18]

其中提到的 Binder 是 Android OS 中負責處理 Process 之間通信的機制(IPC, Inter-Process Communication), 當 ActivityServiceBroadcast ReceiverContent Provide 需要溝通時,就會需要 Binder 參與到其中

而我所碰到的狀況是紀錄了太多的軌跡點,當他需要切換到其他 Activity 時,我會透過 Intent + Parcelable 來傳遞軌跡點資料,而當 Intent 內容超過 1MB 時, OS 就會丟出 TransactionTooLargeException,因此必須找個方法讓資料可以完整的傳遞,同時不會引起 TransactionTooLargeException 的方法

解法

而這個問題其實分析過後就是捨棄 Intent 傳遞資料,只需要有一個地方可以暫存這些資料,而需要的 Activity 可以取得,那這樣就可以解決上述的問題

而最多人使用的是 EventBus 來解,其中包括阿里巴巴的開發手冊都推荐使用 EventBus 來解大量數據傳遞的問題,因此應該可以很放心的使用 EventBus 來解吧...

EventBus 是啥?

其實在這之前我都沒有用過 EventBus 的經驗,但在了解過後發現如果只是用基礎功能,那其實還滿好上手的

在我看完 EventBus 的介紹後,很直覺的就是想到 LiveData 或只是 RxJava 之類的工具,他有Publisher 以及 Subscriber,Subscriber 和 Publisher 之間不用知道彼此的存在,只要 Subscriber 先跟 EventBus Manager 註冊要收哪類型的訊息,當 Publisher 發送相同類型的訊息到 EventBus Manager 時, Manager 就會負責轉交給那些有註冊的 Subscriber,而這個過程是不用 Binder 的介入的

用 EventBus 怎麼解?

根據上面的基礎,我們只要在 Activity 之間設定 Publisher 以及 Subscriber 就可以了,但在使用時有一個地方要注意,在這種 Event Base 的架構下,如果 Subcriber 在 Publisher 發送訊息後才去註冊,是沒辦法拿到資料的

例如:

  1. A Activity 發送訊息
  2. A Activity 退到背景,並啟動 B Activity
  3. B Activity 去註冊要收到 Event
  4. B Activity 並不會收到

那這是個滿常見的操作,那 EventBus 非常貼心的提供了一個叫作 Sticky Events 的方法,透過這個方法,可以讓比較晚註冊的 Activity 也可以收到,我覺得非常讚!!!

實作

那實作上分成 3 個部份

  1. 建立訊息類型
  2. Subscriber 向 EventBus 註冊
  3. Publisher 向 EventBus 發送訊息

1. 建立訊息類型

我是建立了一個叫作 MessageEvent 的 sealed class,並在其中建立多個 data class 來區分訊息的類型

// MessageEvent.kt
sealed class MessageEvent {
    data class MessageTrack(val track: Track) : MessageEvent()
    data class MessageSearch(val text: String) : MessageEvent()
}

這邊可以看到我建立了兩個 data class,分別表示兩種類型的訊息

2. Subscriber 向 EventBus 註冊

那 Subscriber 就可以像 EventBus 註冊需要收到上面哪些類型的訊息,那這邊有一個重點需要注意, Subscriber 的 RegisterUnregister 需要由開發者自己控管,依照我的狀況我是在 onStart() 的時候 Register 並在 onStop() 的時候 Unregister

在需要收 Event 的 Method 需要加上 @Subscribe 告訴 EventBus,並且加上 sticky = true 才能收到已經被發過的 event ,而需要在 Method 中透 when 指定要收到哪種訊息,以及收到之後行為

// 要收訊息的 Activity
class HikeStatisticsActivity : AppCompatActivity() {

    ...

    override fun onStart() {
        super.onStart()
        // 向 EventBus 註冊
        EventBus.getDefault().register(this)
    }

    override fun onStop() {
        super.onStop()
        // 結束註冊
        EventBus.getDefault().unregister(this)
    }

    // 告訴 EventBus 可以收到之前發出來的訊息,以及跑在 MAIN theread 上
    @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
    fun onResultReceived(event: MessageEvent) {
        when (event) {
            is MessageEvent.MessageHike -> {
                // 指定收到的訊息以及之後的行為
                ...
            }
        }
    }

    ...

}

3. Publisher 向 EventBus 發送訊息

最後就可以透過 Publisher 發送消息了,透過 postSticky 可以讓發送被保留住,因此在發送訊息後才啟動的 Activity 也可以收到 event 喔!

另外 Publisher 的 Activity 如果沒有要接收其他 Activity 的資料,是不需要在 onStart() 以及 onStop() 中向 EventBus 註冊的喔~

// 要發訊息的 Activity
class TrackingActivity : AppCompatActivity() {

    ...

    private inner class StatisticsClickListener : View.OnClickListener {
        override fun onClick(v: View?) {
            val intent = Intent(context, HikeStatisticsActivity::class.java)
            EventBus.getDefault().postSticky(MessageEvent.MessageHike(trackingData))
            startActivity(intent)
        }
    }

    ...

}

結論

透過 EventBus 這樣的機制,在程式撰寫上就可以做到

  • 一個 Publisher 讓多個 Subscriber 收到 Event
  • 多個 Publisher 讓一個 Subscriber 收到 Event

對於應用上也是有很大的彈性,雖然很方便,但這種 Event Base 撰寫上還是要做到儘量單一一點,否則一堆 Event 互相觸發、打來打去,在之後 Debug 也會感覺很困擾的!(曾經被 LiveData 循環觸發 Event 殘害過的人應該都有感...)

最後如果各位大大有更好的解法也歡迎留言分享喔~

Reference


尚未有邦友留言

立即登入留言