EventBus 消息的线程切换模型与实现原理
一. 序
EventBus 是一个基于观察者模式的事件订阅/发布框架,利用 EventBus 可以在不同模块之间,实现低耦合的消息通信。
EventBus 因为其使用简单且稳定,被广泛应用在一些生产项目中。
通常我们就是使用 EventBus 分发一些消息给消息的订阅者,除此之外我们还可以通过 EventBus 将消息传递到不同的线程中去执行,处理消息。这其中还涉及到一些线程切换问题、线程池的问题,在使用的过程中,还有一些配置的选择,此时我们需要根据不同的业务场景,来选择不同的线程切换方式。
本文就 EventBus 的几种线程切换方式,以及内部的实现原来,来分析如何使用 EventBus 来切换消息线程。
二. EventBus 的线程切换
2.1 EventBus 切换线程
EventBus 是一个基于观察者模式的事件订阅/发布框架。利用 EventBus 可以在不同模块之间,实现低耦合的消息通信。

EventBus 诞生以来这么多年,在很多生产项目中都可以看到它的身影。而从更新日志可以看到,除了体积小,它还很稳定,这两年就没更新过,最后一次更新也只是因为支持所有的 JVM,让其使用范围不仅仅局限在 Android 上。
可谓是非常的稳定,稳定到让人有一种感觉,要是你使用 EventBus 出现了什么问题,那一定是你使用的方式不对。
EventBus 的使用方式,对于 Android 老司机来说,必然是不陌生的,相关资料太多,这里就不再赘述了。
在 Android 下,线程的切换是一个很常用而且很必须的操作,EventBus 除了可以订阅和发送消息之外,它还可以指定接受消息处理消息的线程。
也就是说,无论你 post() 消息时处在什么线程中,EventBus 都可以将消息分发到你指定的线程上去,听上去就感觉非常的方便。
不过无论怎么切换,无外乎几种情况:
- UI 线程切子线程。
- 子线程切 UI 线程。
- 子线程切其他子线程。
在我们使用 EventBus 注册消息的时候,可以通过 @Subscribe 注解来完成注册事件, @Subscribe 中可以通过参数 threadMode 来指定使用那个线程来接收消息。
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventTest(event:TestEvent){
// 处理事件
}
threadMode 是一个 enum,有多种模式可供选择:
- POSTING,默认值,那个线程发就是那个线程收。
- MAIN,切换至主线程接收事件。
- MAIN_ORDERED,v3.1.1 中新增的属性,也是切换至主线程接收事件,但是和 MAIN 有些许区别,后面详细讲。
- BACKGROUND,确保在子线程中接收事件。细节就是,如果是主线程发送的消息,会切换到子线程接收,而如果事件本身就是由子线程发出,会直接使用发送事件消息的线程处理消息。
- ASYNC,确保在子线程中接收事件,但是和 BACKGROUND 的区别在于,它不会区分发送线程是否是子线程,而是每次都在不同的线程中接收事件。
EventBus 的线程切换,主要涉及的方法就是 EventBus 的 postToSubscription() 方法。
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}
可以看到,在 postToSubscription() 方法中,对我们配置的 threadMode 值进行了处理。
这端代码逻辑非常的简单,接下来我们看看它们执行的细节。
2.2 切换至主线程接收事件
想在主线程接收消息,需要配置 threadMode 为 MAIN。
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
这一段的逻辑很清晰,判断是主线程就直接处理事件,如果是非主线程,就是用 mainThreadPoster 处理事件。
追踪 mainThreadPoster 的代码,具体的逻辑代码都在 HandlerPoster 类中,它实现了 Poster 接口,这就是一个普通的 Handler,只是它的 Looper 使用的是主线程的 「Main Looper」,可以将消息分发到主线程中。
为了提高效率,EventBus 在这里还做了一些小优化,值得我们借鉴学习。

为了避免频繁的向主线程 sendMessage(),EventBus 的做法是在一个消息里尽可能多的处理更多的消息事件,所以使用了 while 循环,持续从消息队列 queue 中获取消息。
同时为了避免长期占有主线程,间隔 10ms (maxMillisInsideHandleMessage = 10ms)会重新发送 sendMessage(),用于让出主线程的执行权,避免造成 UI 卡顿和 ANR。
MAIN 可以确保事件的接收,在主线程中,需要注意的是,如果事件就是在主线程中发送的,则使用 MAIN 会直接执行。为了让开发和可配置的成都更高,在 EventBus v3.1.1 新增了 MAIN_ORDERED,它不会区分当前线程,而是通通使用 mainThreadPoster 来处理,也就是必然会走一遍 Handler 的消息分发。
当事件需要在主线程中处理的时候,要求不能执行耗时操作,这没什么好说的,另外对于 MAIN 或者 MAIN_ORDERED 的选择,就看具体的业务要求了。
2.3 切换至子线程执行
想要让消息在子线程中处理,可以配置 threadMode 为 BACKGROUND 或者 AYSNC,他们都可以实现,但是也有一些区别。
先来看看 BACKGROUND,通过 postToSubscription() 中的逻辑可以看到,BACKGROUND 会区分当前发生事件的线程,是否是主线程,非主线程这直接分发事件,如果是主线程,则 backgroundPoster 来分发事件。
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
BackgroundPoster 也实现了 Poster 接口,其中也维护了一个用链表实现的消息队列 PendingPostQueue,
在一些编码规范里就提到,不要直接创建线程,而是需要使用线程池。EventBus 也遵循这个规范,在 BackgroundPoster 中,就使用了 EventBus 的 executorService 线程池对象去执行。
为了提高效率,EventBus 在处理 BackgroundPoster 时,也有一些小技巧值得我们学习。

可以看到,在 BackgroundPoster 中,处理主线程抛出的事件时,同一时刻只会存在一个线程,去循环从队列中,获取事件处理事件。
通过 synchronized 同步锁来保证队列数据的线程安全,同时利用 volatile 标识的 executorRunning 来保证不同线程下看到的执行状态是可见的。
既然 BACKGROUND 在处理任务的时候,只会使用一个线程,但是 EventBus 却用到了线程池,看似有点浪费。但是再继续了解 ASYNC 的实现,才知道怎么样是对线程池的充分利用。
和前面介绍的 threadMode 一样,大多数都对应了一个 Poster,而 ASYNC 对应的 Poster 是 AsyncPoster,其中并没有做任何特殊的处理,所有的事件,都是无脑的抛给 EventBus 的 executorService 这个线程池去处理,这也就保证了,无论如何发生事件的线程,和接收事件的线程,必然是不同的,也保证了一定会在子线程中处理事件。
public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
queue.enqueue(pendingPost);
eventBus.getExecutorService().execute(this);
}
到这里应该就理解了 BACKGROUND 和 ASYNC ,虽然都可以保证在子线程中接收处理事件,但是内部实现是不同的。
BACKGROUND 同一时间,只会利用一个子线程,来循环从事件队列中获取事件并进行处理,也就是前面的事件的执行效率,会影响后续事件的执行。例如你分发了一个事件,使用的是 BACKGROUND 但是队列前面还有一个耗时操作,那你分发的这个事件,也必须等待队列前面的事件都处理完成才可以继续执行。所以如果你追求执行的效率,立刻马上就要执行的事件,可以使用 ASYNC。
那是不是都用 ASYNC 就好了?当然这种一揽子的决定都不会好,具体问题具体分析,ASYNC 也有它自己的问题。
ASYNC 会无脑的向线程池 executorService 发送任务,而这个线程池,如果你不配置的话,默认情况下使用的是 Executors 的 newCachedThreadPool() 创建的。
这里我又要说到编码规范了,不推荐使用 Executors 直接创建线程,之所以这样,其中一个原因在于线程池对任务的拒绝策略。 newCachedThreadPool 则会创建一个无界队列,来存放线程池暂时无法处理的任务,说到无界队列,拍脑袋就能想到,当任务(事件)过多时,会出现的 OOM。
这也确实是 EventBus 在使用 ASYNC 时,真实存在的问题。

但是其实这里让开发者自己去配置,也很难配置一个合理的线程池的拒绝策略,拒绝时必然会放弃一些任务,也就是会放弃掉一些事件,任何放弃策略都是不合适的,这在 EventBus 的使用中,表现出来就是出现逻辑错误,该收到的事件,收不到了。所以你看,这里无界队列不合适,但是不用它呢也不合适,唯一的办法就是合理的使用 ASYNC,只在必要且合理的情况下,才去使用它。
三. 小结时刻
到这里基本上 EventBus 在分发事件时的线程切换,就讲清除了,很多资料里其实都写了他们可以切换线程,但是对于一些使用的细节,描述的并不清除,正好借此文,把 EventBus 的线程切换的直接讲清除。
EventBus 也是简历上比较常见的高频词,我在面试的过程中,也经常会问面试者,关于它是如何做到线程切换的问题。但是正因为它简单易用,其实很多时候我们都忽略了它的实现细节。
今天就到这里,小结一下:
1. EventBus 可以通过 threadMode 来配置接收事件的线程。
2. MAIN 和 MAIN_ORDERED 都会在主线程接收事件,区别在于是否区分,发生事件的线程是否是主线程。
3. BACKGROUND 确保在子线程中接收线程,它会通过线程池,使用一个线程循环处理所有的事件。所以事件的执行时机,会受到事件队列前面的事件处理效率的影响。
4. ASYNC 确保在子线程中接收事件,区别于 BACKGROUND,ASYNC 会每次向线程池中发送任务,通过线程池的调度去执行。但是因为线程池采用的是无界队列,会导致 ASYNC 待处理的事件太多时,会导致 OOM。
本文就到这里,本文对你有帮助吗?留言、转发、收藏是最大的支持,谢谢!
本文首发自公众号「承香墨影」,欢迎关注获取最新的原创文章。公众号后台回复成长『成长』,将会得到我准备的学习资料,。

EventBus 消息的线程切换模型与实现原理的更多相关文章
- Java线程内存模型-JVM-底层原理
public class Demo1 { private static boolean initFlag=false; public static void main(String[] args) t ...
- EventBus 线程切换原理
主要问题其实只有两个,其一:如何判断当前发送事件的线程是否是主线程:其二:如何在接收事件时指定线程并执行: 一个一个来看. 1.如何判断是否在主线程发送 EventBus在初始化的时候会初始化一个Ma ...
- c++之初级的消息队列及线程池模型
1.最近项目不是很忙,结合之前看的一些开源代码(skynet及其他github代码)及项目代码,抽空写了一个简单的任务队列当做练习. 2.介绍: 1)全局队列中锁的使用:多线程下,全局队列需要加锁,本 ...
- 优化技术专题-线程间的高性能消息框架-深入浅出Disruptor的使用和原理
前提概要 简单回顾 jdk 里的队列: 阻塞队列: ArrayBlockingQueue主要通过:数组(Object[])+ 计数器(count)+ ReetrantLock的Condition (n ...
- JavaScript 学习笔记之线程异步模型
核心的javascript程序语言并没有包含任何的线程机制,客户端javascript程序也没有任何关于线程的定义,事件驱动模式下的javascript语言并不能实现同时执行,即不能同时执行两个及以上 ...
- 基于java.util.logging实现轻量级日志记录库(增加根据当前类class初始化,修复线程池模型(javaEE)下的堆栈轨迹顺序与当前调用方法不一致问题)
前言: 本章介绍自己写的基于java.util.logging的轻量级日志记录库(baseLog). 该版本的日志记录库犹如其名,baseLog,是个实现日志记录基本功能的小库,适合小型项目使用,方便 ...
- Day035--Python--管道, Manager, 进程池, 线程切换
管道 #创建管道的类: Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process ...
- [并发并行]_[线程模型]_[Pthread线程使用模型之二 工作组work crew]
Pthread线程使用模型之二工作组(Work crew) 场景 1.一些耗时的任务,比如分析多个类型的数据, 是独立的任务, 并不像 pipeline那样有序的依赖关系, 这时候pipeline就显 ...
- Android的Handler线程切换原理
Handler是我们在开发中经常会接触到的类,因为在Android中,子线程一般是不能更新UI的. 所以我们会使用Handler切换到主线程来更新UI,那Handler是如何做到实现不同线程之间的切换 ...
随机推荐
- 带你剖析WebGis的世界奥秘----瓦片式加载地图
WebGIS应用程序的页面能够通过HTML.JSP.ASP或任何任何类型的Web页文件构成,其特殊之处在于,它的请求提交的方法并不是通过常用的 "超链接"形式,而是使用鼠标与Web ...
- SQLServer数据库处于恢复挂起状态的解决办法
一.总结 如果数据库处于一个恢复挂起的状态,并且对数据库做脱机和分离的操作,报出数据库文件不可访问的错误,可能是因为数据库的数据文件和日志文件在数据库正常连接的情况下,文件所在的磁盘脱机了,导致数据库 ...
- LD_PRELOAD和ld --wrap
前言 LD_PRELOAD和ld --wrap都能实现不修改原始代码,替换指定函数的实现.通常我们会使用这些方法,替换如malloc)()/free().read()/write()等函数,并在替换函 ...
- 决策树ID3原理及R语言python代码实现(西瓜书)
决策树ID3原理及R语言python代码实现(西瓜书) 摘要: 决策树是机器学习中一种非常常见的分类与回归方法,可以认为是if-else结构的规则.分类决策树是由节点和有向边组成的树形结构,节点表示特 ...
- Linux权限管理(7)
权限的基本介绍: rwx权限详解: rwx作用到文件: [r]:代表可读,可以读取.查看 [w]:代表可写,可以修改,但不代表可以删除该文件,删除一个文件的前提条件是对该文件所在的目录有写权限才能删除 ...
- Spring学习之旅(十二)--持久化框架
对于本职工作来说 JDBC 就可以很好的完成,但是当我们对持久化的需求变得更复杂时,如: 延迟加载 预先抓取 级联 JDBC 就不能满足了,我们需要使用 ORM框架 来实现这些需求. Spring 对 ...
- jvm系列(四):jvm调优-命令篇
运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎鼎的VisualVM,IBM的Memory Analyzer ...
- Java多线程之线程的启动
Java多线程之线程的启动 一.前言 启动线程的方法有如下两种. 利用Thread 类的子类的实例启动线程 利用Runnable 接口的实现类的实例启动线程 最后再介绍下java.util.concu ...
- C#开发BIMFACE系列10 服务端API之获取文件下载链接
系列目录 [已更新最新开发文章,点击查看详细] 通过BIMFACE控制台或者调用服务接口上传文件成功后,默认场景下需要下载该源文件,下载文件一般需要知道文件的下载链接即可.BIMACE平台提供 ...
- appium在windows系统下环境搭建
对于appium的介绍我就不说了,之前的文章介绍过.下面直入主题. 命令版本在安装过程中需要有python2环境,装完你可以装python3来写脚本. 环境要求: JDK >java语言安装包 ...