最近在 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 的限制
其中提到的 Binder 是 Android OS 中負責處理 Process 之間通信的機制(IPC, Inter-Process Communication), 當 Activity、Service、Broadcast Receiver 和 Content 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 發送訊息後才去註冊,是沒辦法拿到資料的
例如:
- A Activity 發送訊息
- A Activity 退到背景,並啟動 B Activity
- B Activity 去註冊要收到 Event
- B Activity 並不會收到
那這是個滿常見的操作,那 EventBus 非常貼心的提供了一個叫作 Sticky Events 的方法,透過這個方法,可以讓比較晚註冊的 Activity 也可以收到,我覺得非常讚!!!
實作
那實作上分成 3 個部份
- 建立訊息類型
- Subscriber 向 EventBus 註冊
- 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 的 Register
和 Unregister
需要由開發者自己控管,依照我的狀況我是在 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 殘害過的人應該都有感...)
最後如果各位大大有更好的解法也歡迎留言分享