最近在 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), 当 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 ActivityB Activity 去注册要收到 EventB Activity 并不会收到
那这是个满常见的操作,那 EventBus 非常贴心的提供了一个叫作 Sticky Events 的方法,透过这个方法,可以让比较晚注册的 Activity 也可以收到,我觉得非常讚!!!
实作
那实作上分成 3 个部份
建立讯息类型Subscriber 向 EventBus 注册Publisher 向 EventBus 发送讯息1. 建立讯息类型
我是建立了一个叫作 MessageEvent
的 sealed class,并在其中建立多个 data class 来区分讯息的类型
// MessageEvent.ktsealed 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
指定要收到哪种讯息,以及收到之后行为
// 要收讯息的 Activityclass 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 注册的喔~
// 要发讯息的 Activityclass 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 残害过的人应该都有感...)
最后如果各位大大有更好的解法也欢迎留言分享喔~