透过 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


<<:  【IntelliJ IDEA 入门指南】Java 开发者的神兵利器

>>:  非线性回归-多项式回归 (polynomial regression in r)

D-30-安装 vscode ? dotnet sdk

离实习结束还有30天 实习生小光第一天到新公司实习,什麽都不懂的他到底会遇到什麽事情呢,让大家想想第...

【D9】取得加权指数历史资料

前言 认为整体环境看多,优秀的个股不会太差;大环境不好,优秀的股票也会被拖累。有这个概念後我们取得三...

Day29-实作(地图) (part1)

进入到尾声,范例的最後一片拼图,马上开始吧!!! 地图的部分使用了leaflet JS和OpenSt...

【从实作学习ASP.NET Core】Day12 | 後台 | 资料筛选与分页

前面我们已经完成了 Create 和 Details , 今天就来实作 List 页面 List 页...

DAY4 Python基础教学(二)

DAY4 Python基础教学(二) 前言 今天要介绍一些基础运算式,不过最简单的加减乘除就不赘述了...