EventBus 是什么?
EventBus 是一个基于发布/订阅(Publish/Subscribe) 模式的开源库(主要由 greenrobot 开发维护)。它的核心目的是简化 Android 应用中不同组件(如 Activity, Fragment, Service, Thread 等)之间的通信。它通过一个中央事件总线(Central Event Bus)来传递事件(Event
对象),允许组件订阅它们关心的事件类型,并在事件发生时自动接收通知。这显著降低了组件间的耦合度。
核心概念
- 事件 (
Event
): 一个普通的 Java 对象(POJO),代表需要传递的消息或通知。事件本身不包含逻辑,只是数据的载体。可以是任何类,比如MessageEvent
,DataUpdateEvent
,UserLoggedInEvent
等。 - 发布者 (
Publisher
): 任何需要通知其他组件发生了某事的对象。它创建一个事件对象并通过 EventBus 实例post(event)
发布该事件到总线上。 - 订阅者 (
Subscriber
): 对特定类型事件感兴趣的对象。它包含一个或多个用@Subscribe
注解标记的方法(称为事件处理方法)。这些方法定义了当特定事件被发布时应该执行的逻辑。 - 事件总线 (
EventBus
): 单例(通常通过EventBus.getDefault()
获取)或自定义实例。它负责:- 维护所有订阅者及其感兴趣的事件类型的注册表。
- 接收发布者发送的事件。
- 根据事件类型查找所有匹配的订阅者。
- 在正确的线程(根据订阅方法指定的 ThreadMode)上调用订阅者的事件处理方法。
一、 使用方法 (非常详细)
1. 添加依赖 (以 Gradle 为例)
在 app 模块的 build.gradle
文件中添加最新版本的 EventBus 依赖(请查看 Maven Central 获取最新版本):
gradle
dependencies {
implementation 'org.greenrobot:eventbus:3.3.1' // 检查最新版本
}
2. 定义事件 (Event)
创建一个简单的 Java 类来表示你想要传递的数据或通知。
java
public class MessageEvent {
public final String message;
public MessageEvent(String message) {
this.message = message;
}
}
3. 准备订阅者 (Subscriber)
在需要接收事件的组件(如 Activity、Fragment、Service 或任何普通对象)中:
- 注册/注销: 组件必须在开始接收事件前向 EventBus 注册自己,并在不再需要接收事件(或生命周期结束时)注销自己,以避免内存泄漏和无效调用。
- 注册: 通常在
onStart()
或onResume()
中进行。 - 注销: 通常在
onStop()
或onPause()
中进行(选择与注册对称的生命周期方法)。对于 Fragment,onCreateView
/onDestroyView
也是常见选择。
- 注册: 通常在
java
public class MyActivity extends AppCompatActivity {
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().register(this); // 注册当前 Activity 作为订阅者
}
@Override
protected void onStop() {
super.onStop();
EventBus.getDefault().unregister(this); // 注销当前 Activity
}
// ... 其他代码 ...
}
- 声明事件处理方法: 使用
@Subscribe
注解标记一个公共方法(方法名任意)。该方法的参数类型 决定了它订阅哪种事件。注解可以指定threadMode
和sticky
属性。threadMode
(必选): 指定事件处理方法在哪个线程执行。这是 EventBus 的核心优势之一。ThreadMode.POSTING
(默认): 在事件发布所在的线程调用。最快 ,避免线程切换开销。小心:如果发布者在主线程发布,你在这里不能执行耗时操作;如果发布者在后台线程发布,你在这里不能更新UI。ThreadMode.MAIN
: 在 Android 的主线程 (UI 线程) 调用。安全更新 UI。如果事件是在主线程发布的,方法会立即执行;否则,事件会被放入主线程队列等待处理。ThreadMode.MAIN_ORDERED
: 类似于MAIN
,但事件在队列中有序执行 (MAIN
可能在某些情况下插队)。通常使用MAIN
即可。ThreadMode.BACKGROUND
: 在后台线程 调用。如果事件是在非主线程发布的,就在该线程直接调用;如果是在主线程发布的,EventBus 会使用一个单一线程的后台线程池 来调用该方法。适合执行轻量级后台操作。ThreadMode.ASYNC
: 在独立的、非主线程、非发布者线程 调用。EventBus 使用一个线程池 来调用这些方法。适合执行耗时操作(如网络请求、数据库大查询),不会阻塞发布线程或主线程。
sticky
(可选,默认false
): 是否处理粘性事件 (Sticky Event) 。如果设为true
,那么即使事件是在该订阅者注册之前发布的,只要该粘性事件还在总线上,订阅者注册后会立即收到该事件的最新一次发布。非常适用于需要获取"最新状态"的场景(如当前登录用户信息、网络状态)。
java
// 示例1:在主线程处理 MessageEvent
@Subscribe(threadMode = ThreadMode.MAIN)
public void onMessageEvent(MessageEvent event) {
// 安全更新UI
textView.setText(event.message);
}
// 示例2:在后台线程处理 DataLoadedEvent
@Subscribe(threadMode = ThreadMode.BACKGROUND)
public void handleDataLoaded(DataLoadedEvent event) {
// 处理数据,比如保存到数据库(轻量级操作)
saveDataToDatabase(event.data);
}
// 示例3:处理粘性事件 UserLoggedInEvent
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onUserLoggedIn(UserLoggedInEvent event) {
// 更新UI显示当前登录用户信息
updateUserProfile(event.user);
}
4. 发布事件 (Publisher)
在任何需要通知其他组件的地方(任何类中),获取 EventBus 实例并调用 post(Object event)
方法。event
对象就是你要传递的事件实例。
java
// 在某个按钮点击事件或网络请求回调中
public void someMethodThatTriggersEvent() {
// 创建事件对象
MessageEvent event = new MessageEvent("Hello EventBus!");
// 发布事件到总线
EventBus.getDefault().post(event);
}
// 发布粘性事件 (会一直保留在总线上直到被覆盖或手动移除)
public void postStickyEvent() {
UserLoggedInEvent event = new UserLoggedInEvent(currentUser);
EventBus.getDefault().postSticky(event);
}
5. 处理粘性事件 (Sticky Events) 的额外操作
- 获取最新粘性事件: 使用
EventBus.getStickyEvent(Class<T> eventType)
可以在订阅者注册前或任何地方手动获取特定类型的最新粘性事件。 - 移除粘性事件: 使用
EventBus.removeStickyEvent(T event)
或EventBus.removeStickyEvent(Class<T> eventType)
手动移除粘性事件。
java
// 在注册订阅者之前,检查是否有最新的登录信息
UserLoggedInEvent stickyEvent = EventBus.getDefault().getStickyEvent(UserLoggedInEvent.class);
if (stickyEvent != null) {
// 立即使用最新用户信息更新UI或状态
updateUserProfile(stickyEvent.user);
}
6. 事件继承
EventBus 支持事件继承。如果一个订阅者订阅了父类事件类型(如 BaseEvent
),那么当发布者发布任何该父类的子类事件(如 SpecificEvent extends BaseEvent
)时,该订阅者也会接收到通知。这可以用于创建更通用的事件处理器。
7. 优先级 (priority
)
在 @Subscribe
注解中可以设置 priority
属性(整数,默认 0)。数值越大 ,优先级越高 。优先级高的订阅者方法会在优先级低的之前接收到事件。如果优先级相同,顺序不确定。注意: 只有在同一个 ThreadMode 下 ,优先级才生效。优先级通常用于拦截或修改事件(高优先级订阅者可以调用 EventBus.cancelEventDelivery(event)
来阻止事件继续传递给低优先级订阅者)。
java
@Subscribe(threadMode = ThreadMode.MAIN, priority = 1)
public void onHighPriorityEvent(MyEvent event) {
// 高优先级处理
if (shouldCancel(event)) {
EventBus.getDefault().cancelEventDelivery(event); // 取消事件传递
}
}
8. 混淆配置 (Proguard/R8)
如果使用代码混淆,需要在 Proguard 规则文件 (proguard-rules.pro
) 中添加以下配置,以确保 @Subscribe
注解方法在运行时能被正确找到(如果使用索引加速则可能不需要,但加上更保险):
pro
-keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# 如果使用了索引加速 (EventBusAnnotationProcessor)
-keep class org.greenrobot.eventbus.** { *; }
9. 索引加速 (EventBusAnnotationProcessor) - 推荐
EventBus 3 引入了索引加速,在编译时 通过 APT (Annotation Processing Tool) 生成一个索引类,列出所有 @Subscribe
方法及其信息(事件类型、线程模式、优先级等)。这避免了在 App 首次运行时使用反射扫描所有类查找订阅方法,大大提高了注册速度和启动性能,并减少了运行时方法查找的开销。
启用步骤:
-
添加注解处理器依赖:
gradledependencies { implementation 'org.greenrobot:eventbus:3.3.1' annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1' // 与 eventbus 版本一致 }
-
配置索引选项 (可选但推荐): 在 app 模块的
build.gradle
中指定生成的索引类名:gradleandroid { defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [ eventBusIndex : 'com.yourpackage.MyEventBusIndex' ] } } } }
-
在 Application 中设置索引: 在自定义
Application
类的onCreate()
中配置 EventBus 使用生成的索引:javapublic class MyApp extends Application { @Override public void onCreate() { super.onCreate(); // 使用索引构建 EventBus 实例 (替代默认反射查找) EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus(); // 现在 EventBus.getDefault() 已经使用了索引 } }
或者在需要使用 EventBus 的地方手动创建带索引的实例:
javaEventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();
二、 应用场景
EventBus 特别适合解决以下通信难题:
- Fragment 间通信: 两个 Fragment 之间没有直接引用。Fragment A 发布事件,Fragment B 订阅并处理。比通过 Activity 中转或接口回调更简洁。
- Activity 与 Service 通信: Service 后台完成任务(如下载完成、播放状态改变)后发布事件,Activity 订阅并更新UI。
- 后台线程与 UI 线程通信: 在后台线程(如网络请求、数据库操作)中完成任务后发布事件,订阅者指定
ThreadMode.MAIN
安全更新UI。 - 跨层通信: 深度嵌套的组件(如 Adapter 中的 ViewHolder)需要通知顶层的 Activity 或 Fragment 执行某些操作(如打开新界面)。
- 广播全局状态变化: 用户登录/登出、网络连接状态变化、语言切换、主题更改等全局状态变更。使用粘性事件尤其方便,新启动的组件能立即获取最新状态。
- 替代部分 Intent/BroadcastReceiver: 对于应用内部通信,EventBus 比系统广播更轻量、更快速、类型更安全(避免 Intent 的 key 字符串硬编码)。
- 解耦业务逻辑与 UI 更新: 业务逻辑模块(如 Presenter, ViewModel, Interactor)处理完逻辑后发布事件,UI 层(Activity/Fragment)订阅事件并只负责展示,实现更好的关注点分离。
- 组件化/模块化通信: 不同模块之间通过定义和发布/订阅公共事件接口进行通信,减少模块间的直接依赖。
三、 底层原理 (深入解析)
EventBus 的核心在于高效地管理订阅关系和在正确线程上派发事件。以下是其核心机制:
-
订阅者注册 (
register(Object subscriber)
):- 方法查找 (运行时 / 编译时):
- 运行时 (默认,无索引): 使用反射遍历
subscriber
对象的所有方法,查找所有被@Subscribe
注解标记的公共方法。提取方法参数类型(即订阅的事件类型)、ThreadMode、优先级、是否粘性等信息。 - 编译时 (使用索引): 编译期间
EventBusAnnotationProcessor
扫描所有类,找出所有@Subscribe
方法,生成一个索引类(如MyEventBusIndex
)。注册时直接从这个索引类中查找subscriber
的类名对应的所有订阅方法信息,避免了耗时的运行时反射扫描。
- 运行时 (默认,无索引): 使用反射遍历
- 构建订阅关系映射: EventBus 内部维护一个核心数据结构:
Map<Class<?>, CopyOnWriteArrayList<Subscription>>
。- Key (
Class<?>
): 事件类型(event.getClass()
)。 - Value (
CopyOnWriteArrayList<Subscription>
): 一个线程安全的列表,存储了所有订阅了该事件类型的Subscription
对象。 Subscription
对象封装了:订阅者对象 (subscriber
)、订阅者方法 (SubscriberMethod
- 包含方法对象、ThreadMode、优先级、是否粘性)。
- Key (
- 对于找到的每个订阅方法,根据其订阅的事件类型,将封装好的
Subscription
对象添加到上述映射表中对应事件类型的列表中。列表根据Subscription
的优先级(priority
)排序。
- 方法查找 (运行时 / 编译时):
-
事件发布 (
post(Object event)
):- 获取当前线程状态:
post()
方法首先获取当前线程的PostingThreadState
对象(一个 ThreadLocal 变量)。PostingThreadState
包含:eventQueue
:当前线程待处理的事件队列。isPosting
:标识当前是否正在派发事件。isMainThread
:标识当前线程是否是主线程。subscription
:当前正在处理的事件对应的订阅信息(用于cancelEventDelivery
)。
- 入队: 将传入的
event
对象加入到当前线程的eventQueue
中。 - 事件派发循环: 如果当前线程不在派发过程中 (
!isPosting
),则开始处理队列:- 设置
isPosting = true
。 - 循环从队列头部取出事件。
- 调用
postSingleEvent(event, postingState)
处理单个事件。 - 处理完队列所有事件后,重置状态
isPosting = false
。
- 设置
- 获取当前线程状态:
-
单个事件处理 (
postSingleEvent
):- 查找订阅者: 根据事件的运行时类 (
event.getClass()
),从订阅关系映射表中查找对应的Subscription
列表。 - 处理事件继承 (可选): 如果启用了事件继承(默认启用),还会查找该事件类的所有父类和接口对应的订阅者列表,并将所有找到的订阅者合并到一个总的列表中(去重)。
- 遍历订阅者并派发 (
postToSubscription
): 对于找到的每一个Subscription
:- 检查订阅者是否已注销(弱引用失效)。
- 根据 ThreadMode 决定执行方式:
POSTING
:直接调用。在当前发布线程直接通过反射(或 MethodHandle)调用订阅者方法。最快,但需注意线程安全。MAIN
/MAIN_ORDERED
:- 如果当前是主线程 :直接调用(
MAIN_ORDERED
会确保按入队顺序)。 - 如果当前不是主线程 :将调用包装成一个
Runnable
,通过mainThreadPoster
(通常是HandlerPoster
,它内部持有一个关联到主线程 Looper 的Handler
)发送到主线程的消息队列中排队执行。
- 如果当前是主线程 :直接调用(
BACKGROUND
:- 如果当前不是主线程:直接调用(在当前后台线程)。
- 如果当前是主线程 :将调用包装成
Runnable
,通过backgroundPoster
(通常是BackgroundPoster
,它内部使用一个单线程的线程池ExecutorService
)提交到后台线程执行。
ASYNC
:总是 将调用包装成Runnable
,通过asyncPoster
(通常是AsyncPoster
,它内部使用一个通用的线程池ExecutorService
)提交执行,与发布线程和当前线程无关。
- 优先级处理: 在同一个 ThreadMode 下,列表已经按优先级排序,高优先级的
Subscription
会先被处理。高优先级订阅者可以通过cancelEventDelivery(event)
取消事件,阻止后续低优先级订阅者收到该事件。cancelEventDelivery
只能在POSTING
模式下调用,因为它需要直接操作当前派发流程。
- 查找订阅者: 根据事件的运行时类 (
-
粘性事件 (
postSticky(Object event)
):- 存储: EventBus 内部维护一个
Map<Class<?>, Object>
用于存储粘性事件。Key 是事件类型,Value 是该类型最新的粘性事件对象。发布粘性事件时,会先更新这个 Map(覆盖旧事件)。 - 发布: 然后像普通事件一样调用
post(event)
进行派发。 - 新订阅者处理: 当一个新的订阅者调用
register()
时,如果它声明了sticky=true
的事件处理方法:- 注册过程完成后,EventBus 会检查粘性事件 Map。
- 对于该订阅者每个声明为
sticky=true
的事件类型,从 Map 中取出最新的粘性事件(如果存在)。 - 然后按照正常的派发逻辑(根据 ThreadMode)调用该订阅者的对应方法,就好像这个事件刚刚发布一样。
- 存储: EventBus 内部维护一个
-
注销 (
unregister(Object subscriber)
):- 遍历内部订阅关系映射表 (
Map<Class<?>, CopyOnWriteArrayList<Subscription>>
)。 - 对于每个事件类型的订阅者列表,移除所有
subscription.subscriber
等于传入的subscriber
的Subscription
对象。 - 移除后,该订阅者对象将不再接收任何事件。
- 遍历内部订阅关系映射表 (
-
线程安全:
- 核心数据结构
Map<Class<?>, CopyOnWriteArrayList<Subscription>>
使用ConcurrentHashMap
或其变体保证键的并发访问安全。CopyOnWriteArrayList
保证了单个事件类型订阅者列表的线程安全(读无锁,写复制)。 - 使用 ThreadLocal (
PostingThreadState
) 管理每个线程的派发状态。 - 不同 ThreadMode 的派发器 (
HandlerPoster
,BackgroundPoster
,AsyncPoster
) 内部使用队列和锁/Handler/线程池来保证任务的有序执行和线程安全。 - 注册/注销操作通常是同步的(内部有锁),应尽量在主线程或确保线程安全的环境下调用。
- 核心数据结构
关键优化点:
- 索引加速: 极大提升注册速度,避免首次运行时反射扫描。
CopyOnWriteArrayList
: 读多写少场景(事件派发是高频读,注册/注销是相对低频写)性能好,读操作完全无锁。- 线程局部变量 (
PostingThreadState
): 高效管理每个线程的事件队列和派发状态。 - 按需线程切换: 只在需要时才将任务派发到其他线程(通过 Handler 或 Executor)。
- 弱引用:
Subscription
持有订阅者对象的弱引用 (WeakReference
) 。这非常重要!它确保如果订阅者对象(如 Activity)被垃圾回收(例如用户关闭了 Activity 但忘记调用unregister()
),Subscription
中的弱引用会自动失效。EventBus 在派发事件时会检查弱引用是否有效,无效则跳过或移除该Subscription
,从而自动防止了因忘记注销而导致的内存泄漏 。这是 EventBus 设计中的一个关键安全机制。(注意:虽然弱引用提供了保护,但最佳实践仍是显式unregister()
以保持代码清晰和及时释放资源)。
总结:
EventBus 通过高效的发布/订阅机制、强大的线程模式支持、粘性事件特性以及底层精心的设计(映射表、ThreadLocal、弱引用、线程池/Handler),为 Android 开发提供了一种极其便捷、灵活且相对安全的组件间通信方式。它特别擅长解耦跨组件、跨线程的通信需求。理解其底层原理(尤其是订阅关系管理、线程派发逻辑和弱引用机制)对于正确、高效地使用 EventBus 至关重要。务必遵循注册/注销的生命周期管理,合理选择 ThreadMode,并在大型项目中使用索引加速以获得最佳性能。