一. 序
虽然现在互联网行业的就业形式「相当严峻」,张小胖还是成功跳槽涨薪。
入职第一天 Leader 说,“你刚来,这周先熟悉熟悉咱们的项目吧”。
张小胖熟练的用 Git pull 代码到本地,环境变量一通配置,终于把项目跑了起来,看着项目里的网络请求数据,居然全是靠 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。
【本文为51CTO专栏作者“张旸”的原创稿件,转载请通过微信公众号联系作者获取授权】