经典代码快读之《EventBus》

67 阅读12分钟

一、项目定位与功能

  • EventBus 主要用于简化组件间通信,解耦事件发送者和接收者,适用于 Activity、Fragment 及后台线程等场景。
  • 支持多线程事件分发、订阅者优先级、粘性事件(sticky events)等高级特性。
  • 体积小、速度快,广泛应用于大量实际项目中。

二、实现流程解析

根据源码,EventBus 的通信实现流程如下:


1. 注册订阅者(register)

  • 用户通过 EventBus.getDefault().register(this) 注册订阅者对象。
  • EventBus 会用 SubscriberMethodFinder 查找该对象中所有被 @Subscribe 注解的方法(即订阅方法),这些方法必须是 public、返回值为 void,且参数只有一个(事件类型)。
  • 每个订阅方法会被封装为 SubscriberMethod,再与订阅者对象一起封装为 Subscription。
  • EventBus 会将 Subscription 按事件类型存入 subscriptionsByEventType,并按优先级排序。
  • 同时,记录订阅者与其订阅事件类型的映射,便于后续注销。

2. 发送事件(post)

  • 用户通过 EventBus.getDefault().post(event) 发送事件。
  • EventBus 会根据事件的类型,查找所有订阅了该类型(及其父类/接口,若开启继承)的订阅者。
  • 对每个订阅者,根据其订阅方法的 ThreadMode(线程模式)决定事件分发方式:
  • POSTING:直接在当前线程调用订阅方法。
  • MAIN/MAIN_ORDERED:在主线程(UI线程)调用,若当前不是主线程则通过主线程 Poster 切换。
  • BACKGROUND:在后台线程调用,若当前是主线程则切换到后台线程。
  • ASYNC:始终在独立线程池中异步调用。
  • 事件通过反射调用订阅方法(或通过注解处理器生成的索引,避免反射)。

3. 注销订阅者(unregister)

  • 用户通过 EventBus.getDefault().unregister(this) 注销订阅者。
  • EventBus 会移除该对象所有的订阅关系,防止内存泄漏和重复接收事件。

4. 粘性事件(Sticky Event)

  • 通过 postSticky 发送的事件会被缓存,后注册的订阅者如果声明 sticky=true,会立即收到最近的粘性事件。

5. 事件分发的线程模型

  • 通过 @Subscribe(threadMode = ThreadMode.XXX) 指定事件处理线程,支持多种线程切换,适应不同场景(UI、后台、异步等)。

6. 订阅方法查找机制

  • 默认通过反射查找(findUsingReflection),也支持通过注解处理器在编译期生成索引(findUsingInfo),提升性能。
  • 查找时会递归父类,支持事件和订阅者的继承。

7. 错误与异常处理

  • 如果订阅方法抛出异常,EventBus 会根据配置决定是否抛出、记录日志或发送 SubscriberExceptionEvent。
  • 如果没有订阅者接收事件,可配置是否发送 NoSubscriberEvent。

8. 关键数据结构

  • subscriptionsByEventType:事件类型 -> 订阅者列表(按优先级排序)
  • typesBySubscriber:订阅者对象 -> 事件类型列表
  • stickyEvents:事件类型 -> 最近的粘性事件

EventBus 通过注册/注销订阅者、事件类型与订阅方法的映射、灵活的线程切换和高效的事件分发机制,实现了松耦合、跨线程的组件通信。其核心在于订阅方法的查找与缓存、事件分发的线程模型,以及对粘性事件和异常的支持。

三、实现细节拆分

EventBus 注册流程(register)

1. 用户调用 register

  • 入口:EventBus.getDefault().register(this)
  • 参数:subscriber(订阅者对象)

2. 查找订阅方法

  • 调用 subscriberMethodFinder.findSubscriberMethods(subscriberClass)
  • 优先使用编译期生成的索引(如果有),否则用反射查找。
  • 递归遍历类及其父类,查找所有被 @Subscribe 注解、public、参数唯一的方法。
  • 每个方法被封装为 SubscriberMethod,包含方法、事件类型、线程模式、优先级、是否粘性等信息。
  • 结果缓存到 METHOD_CACHE,提升后续注册效率。

3. 注册每个订阅方法

  • 遍历所有 SubscriberMethod,对每个方法调用 subscribe(subscriber, subscriberMethod):
  • 以事件类型为 key,获取或新建 subscriptionsByEventType(事件类型 -> 订阅者列表)。
  • 检查是否已注册,防止重复注册。
  • 按优先级插入到订阅者列表(CopyOnWriteArrayList)。
  • 记录订阅者与事件类型的映射到 typesBySubscriber(订阅者 -> 事件类型列表)。
  • 如果该方法声明为 sticky,则立即检查是否有对应的粘性事件,若有则直接分发一次。

4. 线程安全

  • 注册过程用 synchronized 保证线程安全,防止并发注册导致数据结构混乱。

5. 注册完成

  • 订阅者对象的所有订阅方法都已被 EventBus 记录,后续只要有对应事件发布,就会自动分发到这些方法。

关键数据结构

  • subscriptionsByEventType:事件类型 -> 订阅者列表(按优先级排序)
  • typesBySubscriber:订阅者对象 -> 事件类型列表
  • METHOD_CACHE:订阅者类 -> 订阅方法列表(加速查找)

注册流程时序图(简化)

register(subscriber)

    └─> findSubscriberMethods(subscriberClass)

        └─> 反射/索引查找所有@Subscribe方法

    └─> for each SubscriberMethod

        └─> subscribe(subscriber, subscriberMethod)

            └─> 订阅关系写入数据结构

            └─> 检查并分发粘性事件(如有)

注册流程的核心是查找订阅方法和建立事件类型与订阅者的映射,为后续事件分发打下基础。整个过程兼顾了性能(缓存/索引)、灵活性(支持继承、粘性事件)、线程安全和易用性。

EventBus 事件发送(post)流程

1. 用户调用 post

  • 入口:EventBus.getDefault().post(event)
  • 参数:event(事件对象)

2. 事件入队与线程状态管理

  • 获取当前线程的 PostingThreadState(线程本地变量,保证线程安全)。
  • 将事件加入 eventQueue 队列。
  • 如果当前没有正在分发事件(isPosting == false),则设置 isPosting = true,并记录当前是否主线程。

3. 事件分发主循环

  • 进入循环,依次处理 eventQueue 中的每个事件,调用 postSingleEvent(event, postingState)。

4. 事件类型查找与继承支持

  • 判断是否开启事件继承(eventInheritance)。
  • 若开启,则查找事件的所有父类和接口类型,依次分发。
  • 否则只分发给事件本身类型的订阅者。

5. 分发给每个订阅者

  • 对每个事件类型,查找 subscriptionsByEventType 中的订阅者列表。
  • 遍历每个 Subscription,设置当前分发状态(event、subscription)。
  • 调用 postToSubscription(subscription, event, isMainThread) 进行实际分发。

6. 线程模型分发

  • postToSubscription 根据订阅方法的 ThreadMode 决定分发方式:
  • POSTING:直接在当前线程调用。
  • MAIN/MAIN_ORDERED:主线程调用,必要时切换到主线程。
  • BACKGROUND:后台线程调用,必要时切换到后台线程。
  • ASYNC:始终在独立线程池异步调用。
  • 线程切换通过 mainThreadPoster、backgroundPoster、asyncPoster 实现。

7. 反射调用订阅方法

  • 最终通过 invokeSubscriber(subscription, event) 反射调用订阅方法。
  • 若方法抛出异常,根据配置决定是否抛出、记录日志或发送 SubscriberExceptionEvent。

8. 事件分发终止

  • 如果订阅方法调用了 cancelEventDelivery,则本次事件不会继续分发给后续订阅者。

9. 无订阅者处理

  • 如果没有任何订阅者接收该事件,根据配置可发送 NoSubscriberEvent。

10. 状态恢复

  • 事件分发结束后,重置 isPosting、isMainThread 等状态。

关键点总结

  • 事件队列:支持嵌套/递归 post,保证事件顺序。
  • 事件类型继承:支持事件分发到父类/接口的订阅者。
  • 线程模型:灵活支持主线程、后台线程、异步线程分发。
  • 异常处理:可配置是否抛出、记录或分发异常事件。
  • 分发终止:高优先级订阅者可中断事件继续分发。

post 流程时序图(简化)


post(event)

    └─> eventQueue.add(event)

    └─> while (!eventQueue.isEmpty())

        └─> postSingleEvent(event, postingState)

            └─> for each eventType (含继承)

                └─> for each Subscription

                    └─> postToSubscription(subscription, event, isMainThread)

                        └─> (根据ThreadMode切换线程)

                        └─> invokeSubscriber(subscription, event)


四、常见提问

1、EventBus的优点有哪些,如果不使用EventBus如何解决对应的问题?

EventBus 的优点

  1. 解耦
  • 事件发送者和接收者完全解耦,无需直接引用对方,降低模块间依赖。
  1. 简化代码
  • 只需注册/注销和注解订阅方法,无需手动维护回调、接口、广播等繁琐逻辑。
  1. 线程切换灵活
  • 支持主线程、后台线程、异步线程等多种分发模式,适应 UI、IO、计算等不同场景。
  1. 性能高
  • 采用索引+缓存机制,事件分发效率高,适合高频通信。
  1. 支持粘性事件
  • 粘性事件可让后注册的订阅者也能收到最近一次事件,适合如“全局状态通知”等场景。
  1. 继承支持
  • 事件类型和订阅者都支持继承,灵活性强。
  1. 生命周期友好
  • 适合 Android Activity/Fragment 生命周期,注册/注销简单安全。
  1. 广泛应用与社区支持
  • 经过大量项目验证,文档丰富,社区活跃。

如果不用 EventBus,常见的替代方案

  1. 接口回调(Callback)
  • 发送方持有接收方接口引用,直接调用方法。
  • 缺点:强耦合,难以跨模块/跨层传递,代码冗长。
  1. 观察者模式(Observer/Listener)
  • 典型如 Java 的 Observer、Android 的 OnClickListener。
  • 缺点:需要手动维护注册/注销,跨线程处理复杂。
  1. Handler/Message(Android)
  • 用于线程间通信,主线程与子线程交互。
  • 缺点:只能用于 Handler 所在线程,API繁琐,难以全局广播。
  1. BroadcastReceiver(Android)
  • 用于全局广播事件。
  • 缺点:系统级广播开销大,数据类型受限,注册/注销繁琐。
  1. LiveData(Android Jetpack)
  • 支持生命周期感知的事件分发,适合 MVVM 架构。
  • 缺点:主要用于数据驱动 UI,跨模块/全局事件不如 EventBus 灵活。
  1. RxJava/RxBus
  • 用响应式流实现事件总线。
  • 缺点:学习曲线陡峭,API复杂,容易内存泄漏。

总结

  • 不用 EventBus 时,事件通信往往需要手动维护对象引用、接口、注册表、线程切换等,代码复杂且易出错。
  • EventBus 通过统一的事件总线、注解、线程模型等机制,大大简化了事件通信,提升了开发效率和代码可维护性。

2、EventBus支持事件取消机制吗?

是的,EventBus 支持事件的取消机制。

具体机制

  • 在订阅方法中,可以调用 EventBus.getDefault().cancelEventDelivery(event) 来中断当前事件的后续分发。
  • 这样,优先级较高的订阅者可以“拦截”事件,阻止事件继续传递给其他订阅者。

使用限制

  1. 只能在事件处理方法内部调用,且必须是 ThreadMode.POSTING(即事件在发送线程直接分发时)。
  1. 只能取消当前正在处理的事件,不能取消其他事件。
  1. 如果订阅方法的 ThreadMode 不是 POSTING,调用会抛出异常。

示例代码


Apply to SubscriberMe...

@Subscribe(threadMode = ThreadMode.POSTING, priority = 10)

public void onEvent(MyEvent event) {

    // 某些条件下不希望后续订阅者再收到该事件

    EventBus.getDefault().cancelEventDelivery(event);

}

源码依据

  • EventBus 的 cancelEventDelivery(Object event) 方法会设置当前线程的 PostingThreadState.canceled = true,后续分发检测到后会中断循环。

典型应用场景

  • 事件被高优先级订阅者消费后,不希望再被其他订阅者处理(如“拦截”某些全局事件)。

总结:EventBus 支持事件取消,但仅限于 ThreadMode.POSTING 的订阅方法中使用,适合实现事件的“拦截”或“优先消费”机制。

3、EventBus如何保证事件的有序性的?

EventBus 保证事件有序性的核心机制如下:


1. 事件队列(eventQueue)顺序处理

  • EventBus 内部为每个线程维护了一个 PostingThreadState,其中包含一个 eventQueue(事件队列)。
  • 当你调用 post(event) 时,事件会被按顺序加入队列。
  • 事件分发时,始终是先进先出(FIFO)地从队列取出并处理,保证了事件的发送顺序和处理顺序一致。

2. 单线程串行分发

  • 在同一个线程内,事件的分发是串行的。
  • 只有当前事件分发完成后,才会处理下一个事件,避免了并发导致的顺序错乱。

3. 订阅者优先级与顺序

  • 对于同一事件类型的多个订阅者,EventBus 会根据 @Subscribe(priority=...) 的优先级从高到低依次分发。
  • 同优先级的订阅者,按照注册顺序分发。

4. 线程模型影响

  • ThreadMode.POSTING、ThreadMode.MAIN、ThreadMode.BACKGROUND、ThreadMode.ASYNC 等线程模式会影响事件在哪个线程被处理。
  • 同一线程模式下,事件分发顺序依然严格按照事件发送顺序和订阅者优先级。
  • 不同线程模式下,事件可能在不同线程并发处理,但每个线程内部依然保证顺序。

5. 嵌套/递归 post 也有序

  • 如果在订阅方法中再次调用 post(),新事件会被加入当前线程的 eventQueue,依然保证顺序处理。

结论

 EventBus 通过每线程独立的事件队列和串行分发机制,保证了事件的有序性。只要事件在同一线程、同一事件类型下,分发顺序就是发送顺序。

4、EventBus是如何处理粘性事件的?

1. 粘性事件的定义

  • 粘性事件是指:事件发送后会被缓存,即使订阅者在事件发送之后才注册,也能立刻收到最近一次的该类型事件。

2. 发送粘性事件

  • 通过 postSticky(Object event) 方法发送粘性事件。
  • 实现方式:
  1. 事件会被存入 stickyEvents(一个 Map,key 为事件类型,value 为事件对象)。
  1. 然后像普通事件一样分发给当前已注册的订阅者。

java

Apply to SubscriberMe...

  EventBus.getDefault().postSticky(new MyEvent());


3. 注册粘性事件订阅者

  • 订阅方法需加 @Subscribe(sticky = true) 注解。
  • 当订阅者注册时,EventBus 会检查 stickyEvents,如果有对应类型的粘性事件,会立即调用订阅方法,让订阅者收到最近的事件。

java

Apply to SubscriberMe...

  @Subscribe(sticky = true)

  public void onStickyEvent(MyEvent event) {

      // 处理粘性事件

  }


4. 获取/移除粘性事件

  • 获取:getStickyEvent(Class eventType) 可获取某类型的粘性事件。
  • 移除:removeStickyEvent(Class eventType) 或 removeAllStickyEvents() 可移除单个或全部粘性事件。

5. 粘性事件的典型应用场景

  • 全局状态通知(如登录状态、配置变更等)。
  • 需要“补发”给后注册订阅者的事件。

6. 源码关键点

  • 粘性事件存储在 stickyEvents(ConcurrentHashMap<Class<?>, Object>)。
  • 注册时如果订阅方法声明 sticky = true,会自动查找并分发粘性事件。

总结

 EventBus 通过 postSticky 发送并缓存事件,注册时自动补发给 sticky 订阅者,从而实现了粘性事件机制。