一、为什么需要 EventBus?
开发 Android 应用时,经常会遇到组件之间传递消息的需求。
例如,一个商城应用中,商品页面负责“加入购物车”,而页面顶部的工具栏负责显示购物车数量。加入成功后,如何通知工具栏更新?
我们可以定义接口,通过回调传递结果。但随着组件增多,接口可能需要经过多层传递,维护起来越来越麻烦。
EventBus 提供了另一种方式:发布者发送事件,订阅者接收事件,由事件总线负责分发。
整个过程可以理解为:
商品 Fragment │ │ 发布“购物车数量变化”事件 ▼ EventBus │ │ 分发给订阅者 ▼ 宿主 Activity 更新工具栏二、先理解三个核心概念
使用 EventBus,只需要先掌握三个角色。
| 角色 | 作用 | 示例 |
|---|---|---|
| 事件 Event | 描述发生了什么,以及相关数据 | 购物车数量变为 3 |
| 发布者 Publisher | 发送事件 | 商品 Fragment |
| 订阅者 Subscriber | 接收并处理事件 | 显示购物车数量的 Activity |
事件通常是一个普通的数据类:
data class CartCountChangedEvent( val count: Int )建议事件名称表达“已经发生的事情”,例如CartCountChangedEvent,比UpdateEvent更容易理解。
同时,事件中尽量只携带必要数据,避免放入 Activity、View 或 Context 等对象,以免延长这些对象的生命周期。
三、用一个例子完成消息传递
下面以“商品 Fragment 添加购物车,宿主 Activity 更新数量”为例。
这两个组件处于同一个前台页面中,Activity 接收事件时仍然可见。
1. 添加依赖
在应用模块的build.gradle.kts中添加:
dependencies { implementation("org.greenrobot:eventbus:3.3.1") }2. 定义事件
新建一个 Kotlin 数据类:
data class CartCountChangedEvent( val count: Int )这个类负责传递购物车的最新数量。
3. 注册订阅者
布局中假设已经存在 ID 为cartCountText的 TextView。
import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode class MainActivity : AppCompatActivity() { override fun onStart() { super.onStart() EventBus.getDefault().register(this) } @Subscribe(threadMode = ThreadMode.MAIN) fun onCartCountChanged(event: CartCountChangedEvent) { findViewById<TextView>(R.id.cartCountText).text = "购物车:${event.count}" } override fun onStop() { EventBus.getDefault().unregister(this) super.onStop() } }这里有三个关键点:
register(this):让当前 Activity 开始接收事件。@Subscribe:标记用于接收事件的方法。unregister(this):停止接收事件,并释放注册关系。
注意点:
1. 订阅方法需要是public、返回Unit,并且只有一个参数
2. 参数类型决定它接收哪一种事件,方法名称本身不决定事件匹配。也就是说,把方法改名为handleCartEvent,仍然能够收到CartCountChangedEvent。
4. 发布事件
在商品 Fragment 中,加入购物车成功后发送事件:
import org.greenrobot.eventbus.EventBus // 在“加入购物车成功”的业务回调中执行。 // latestCount 应来自业务层返回或查询到的最新数量。 fun onAddToCartSuccess(latestCount: Int) { EventBus.getDefault().post( CartCountChangedEvent(count = latestCount) ) }执行post()后,EventBus 会寻找匹配的订阅方法,并按照配置的线程模式进行调用。
四、ThreadMode:订阅方法运行在哪个线程?
Android 开发中,更新 UI 必须在主线程执行,因此线程模式是 EventBus 最需要理解的部分。
常见模式如下:
| 模式 | 执行规则 | 适用场景 |
|---|---|---|
POSTING | 在发布事件的线程执行,是默认模式 | 简短、无 UI 操作的处理 |
MAIN | 主线程发布时直接执行;其他线程发布时切到主线程 | 更新界面 |
MAIN_ORDERED | 始终排队到主线程执行 | 希望避免主线程内立即嵌套调用 |
BACKGROUND | 主线程发布时交给后台线程;后台线程发布时直接执行 | 非 UI 工作 |
ASYNC | 交给线程池执行 | 需要异步执行的独立任务。全面外包 |
有两个地方很容易理解错。
第一,post()不代表自动切换到后台线程。
如果使用默认的POSTING,主线程发送事件,订阅方法就会在主线程执行。此时如果处理大量计算,就可能造成卡顿。
第二,BACKGROUND不保证每次都切换线程。
如果事件本来就在后台线程发布,它会直接在当前线程执行。
对于前面的购物车示例,订阅方法需要修改 TextView,因此选择:
@Subscribe(threadMode = ThreadMode.MAIN)即使业务回调来自后台线程,界面更新也会在主线程进行。
不过,MAIN 只解决线程位置问题。订阅方法中仍然不能执行耗时操作,可能会导致ANR
五、粘性事件
普通事件有一个特点:
只有发送时已经注册的订阅者,才有机会收到事件。
例如:
先 post 事件 → Activity 后 register → 收不到之前的普通事件如果业务确实需要“先发送,后订阅”,可以使用粘性事件。
发布方式:
EventBus.getDefault().postSticky( CartCountChangedEvent(count = 3) )订阅方式:
@Subscribe( threadMode = ThreadMode.MAIN, sticky = true ) fun onCartCountChanged(event: CartCountChangedEvent) { // 更新界面 }EventBus 会保存对应事件类型最近一次发布的粘性事件。新的粘性订阅者注册时,可以收到这份缓存。
这里要注意:
- 同一种事件类型通常只保留最近一次粘性事件,不会保存完整历史。
- 缓存在内存中,进程结束后会丢失。
- 粘性事件不会在被接收后自动删除。
不再需要时,可以主动移除:
EventBus.getDefault().removeStickyEvent( CartCountChangedEvent::class.java )六、生命周期与原理:理解后才能少踩坑
前面的示例使用onStart()注册,onStop()注销,意味着 Activity 只在可见期间接收事件。
这也带来一个需要留意的问题:
如果首页已经进入onStop(),用户在另一个 Activity 修改了数据,此时首页无法收到普通事件。返回首页后,它也不会自动补收。
因此,注册位置需要根据业务决定:
| 注册与注销位置 | 接收范围 |
|---|---|
onStart()/onStop() | 页面可见期间 |
onCreate()/onDestroy() | 页面实例存活期间 |
还有需要注意的是,如果订阅者需要操作 Fragment 的视图,还需要考虑 Fragment 和视图的生命周期不同,避免访问已经销毁的 View。
还有两个常见问题需要记住。
忘记注销可能导致内存泄漏。EventBus 会持有已注册订阅者的引用,页面退出后如果仍然注册,就可能无法及时释放。
EventBus 默认不能跨进程通信。Android 中不同进程拥有各自的内存和 EventBus 实例。跨进程通信应使用 Binder、AIDL 等机制。
七、什么时候适合使用 EventBus?
EventBus 适合组件之间关系比较松散、需要广播通知的场景,例如多个模块监听某个业务操作完成。
但它也有代价:消息调用链变得不直观。查看一处post(),通常无法直接看出所有接收方,需要继续搜索订阅代码。
因此,选择通信方式时,可以参考下面的思路:
| 场景 | 可以优先考虑 |
|---|---|
| 两个对象之间有明确调用关系 | 接口回调 |
| 同一 Activity 内多个 Fragment 共享状态 | 共享 ViewModel |
| 页面观察持续变化的状态 | StateFlow / LiveData |
| 组件之间进行松散的事件通知 | EventBus |
| 跨进程通信 | Binder / AIDL |
EventBus 的价值是降低发布者和订阅者之间的直接依赖。使用时要同时明确三件事:事件由谁发送、订阅者何时有效、处理逻辑运行在哪个线程。
掌握这三点,再配合正确的注册与注销,就能完成 Android 中常见的事件通信需求。