EventBus源码赏析一 —— 基本使用

1,176 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情

EventBus简介

EventBus是一种用于Android的发布/订阅事件总线。我们经常用来在不同界面,不同线程传递数据,它解耦了事件发送方和事件处理方。 虽然Android本身提供了LocalBroadcastReceiver类可以实现类似的功能,但是LocalBroadcastReceiver使用起来稍微繁琐,而且传递数据大小也受intent限制。

EventBus角色

EventBus主要有一下三个角色:

  1. Event 事件,可以是任意引用类型,不能是基础数据类型如int,char。他是EventBus传递,处理的对象。
  2. Subscriber 事件订阅者,订阅者必须有@Subscribe注解的订阅方法,用来处理事件。
  3. Publisher 事件发布者,可以在任意线程发布事件。事件订阅者收到后会进行处理。

基本使用

添加依赖

implementation 'org.greenrobot:eventbus:3.3.1'

定义事件

public class MessageEvent {}

这一步不是必须的,因为事件可以是任意引用类型,所以可以使用一些已经存在的类作为事件类,比如String

注册/反注册

// 注册
protected void onCreate(@Nullable  Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EventBus.getDefault().register(this);
}
// 反注册
protected void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(this);
}

注册完了之后必须反注册,否则造成内存泄漏,在没有反注册之前,不能重复注册,不然会抛出异常。

事件处理

@Subscribe(threadMode = ThreadMode.MAIN,priority=1,sticky=true)
public void handleMessage(MessageEvent event) {
    Log.i("EventBusTest", event.toString());
}

在上一步注册的类里面添加一个方法,用于事件处理,方法名任意,但是只能有一个参数,而且修饰符必须是public的,不能使用static,abstract修饰,然后使用@Subscribe注解。@Subscribe提供了一些可选参数让我们在不同场景选用。

事件发送

EventBus.getDefault().post(new MessageEvent());

在需要发送事件的地方调用Eventbuspost()方法发送事件。

@Subscribe参数详解

@Subscribe有3个参数,threadMode用于指定线程模型,sticky指定是否处理粘性事件,priority指定优先级。

threadMode

EventBus允许事件发送的线程和事件处理的线程不一样,我们可以通过threadMode指定,ThreadMode是一个枚举类型,有以下五种取值:

  • POSTING 订阅者的订阅方法将在发布事件的同一线程中被调用,这种模式下不会涉及线程切换,所以开销比较小,但是可能发布事件的线程是主线程,所以需要避免在订阅方法中处理耗时操作
  • MAIN 订阅方法将在UI线程被调用,如果发布事件的线程也是UI线程,则可以直接执行调用订阅方法,否则会使用Handler切换到UI线程
  • MAIN_ORDEREDMAIN类似,只不过不管发布线程在不在UI线程,都会由Handler处理,这确保了post()的调用是非阻塞的
  • BACKGROUND 订阅方法将在非UI线程被调用,如果发布事件的线程在UI线程,则会创建一个线程来执行订阅方法,否则直接执行,为避免后续事件的发送或调用,需要避免在订阅方法中处理耗时操作
  • ASYNC 订阅方法的调用线程将独立于发送事件的线程,所以这种情况下可以做耗时操作,但是需要避免在同一时间进行大量的异步订阅,控制并发线程的数量。

ThreadMode.POSTING

事件发送

new Thread(() -> EventBus.getDefault().post(new MessageEvent(Thread.currentThread().getName()))).start();

在发布线程处理事件

@Subscribe(threadMode = ThreadMode.POSTING)
public void postingButton(MessageEvent event) {
    Log.i("EventBusTest", "事件发送线程:" + event.name + ",事件处理线程:" + Thread.currentThread().getName());
}

结果

EventBusTest: 事件发送线程:Thread-7,事件处理线程:Thread-7

可以看出订阅者的订阅方法将在发布事件的同一线程中被调用。

ThreadMode.MAIN

事件发送

long start = System.currentTimeMillis();
EventBus.getDefault().post(new MessageEvent(Thread.currentThread().getName()));
long end = System.currentTimeMillis();
Log.i("EventBusTest", "间隔 " + (end - start) + "ms 才后发送下一个事件");
new Thread(() -> EventBus.getDefault().post(new MessageEvent(Thread.currentThread().getName()))).start();

处理事件

@Subscribe(threadMode = ThreadMode.MAIN)
public void mainButton(MessageEvent event) {
    Log.i("EventBusTest", "事件发送线程: " + event.name + ",事件处理线程: " + Thread.currentThread().getName());
    SystemClock.sleep(1000);
}

结果输出

EventBusTest: 事件发送线程: main,事件处理线程: main
EventBusTest: 间隔 1001ms 才后发送下一个事件
EventBusTest: 事件发送线程: Thread-7,事件处理线程: main

可以看出,即使发送线程不在主线程,最后处理的时候也会切换到主线程,而且因为处理事件的方法做了耗时操作,所以隔了1s多才发送第二个事件。

ThreadMode.MAIN_ORDERED

事件发送与上面一样,处理事件的方法调整下,将 threadMode 设置为 ThreadMode.MAIN_ORDERED

@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void mainOrderButton(MessageEvent event) {
    Log.i("EventBusTest", "事件发送线程: " + event.name + ",事件处理线程: " + Thread.currentThread().getName());
    SystemClock.sleep(1000);
}

结果输出

EventBusTest: 间隔 0ms 才后发送下一个事件
EventBusTest: 事件发送线程: main,事件处理线程: main
EventBusTest: 事件发送线程: Thread-8,事件处理线程: main

可以看出MAIN_ORDEREDMAIN相似的是都是切换到MAIN线程处理事件,不一样的是ThreadMode.MAIN这种模型下,前一个事件处理可能会阻塞下一个事件的发送,而ThreadMode.MAIN_ORDERED不会

这里说的是“可能阻塞”,至于会不会阻塞取决于事件的发送线程,如果一个事件在非主线程发送,结果其实与MAIN_ORDERED一样,不会阻塞,如果是在主线程发送,线程模型又是指定的ThreadMode.MAIN,则会先立即执行处理此事件的方法,等到方法执行完毕,才会发送下一个事件。

ThreadMode.BACKGROUND

事件发送

new Thread(() -> {
    long start = System.currentTimeMillis();
    EventBus.getDefault().post(new MessageEvent(Thread.currentThread().getName(), start));
    long end = System.currentTimeMillis();
    Log.i("EventBusTest", "子线程发送耗时 " + (end - start) + "ms");
}).start();
//保证上下两种情况互不干扰
SystemClock.sleep(2000);
long start = System.currentTimeMillis();
EventBus.getDefault().post(new MessageEvent(Thread.currentThread().getName(), System.currentTimeMillis()));
long end = System.currentTimeMillis();
Log.i("EventBusTest", "主线程发送耗时 " + (end - start) + "ms");

这里在非MAIN线程和MAIN线程都分别发送了事件,主要是为了体现发布线程对结果的影响。

事件处理

@Subscribe(threadMode = ThreadMode.BACKGROUND,priority = 1)
public void bgButton(MessageEvent event) {
    long time = System.currentTimeMillis() - event.sendTime;
    Log.i("EventBusTest", "优先级1:事件发送线程: " + event.name + ",事件处理线程: " + Thread.currentThread().getName() + ",发送" + time + " m后才开始处理");
    SystemClock.sleep(500);
}
@Subscribe(threadMode = ThreadMode.BACKGROUND,priority = 2)
public void bgButton1(MessageEvent event) {
    long time = System.currentTimeMillis() - event.sendTime;
    Log.i("EventBusTest", "优先级2:事件发送线程: " + event.name + ",事件处理线程: " + Thread.currentThread().getName() + ",发送" + time + " m后才开始处理");
    SystemClock.sleep(500);
}

结果输出

EventBusTest: 优先级2:事件发送线程: Thread-7,事件处理线程: Thread-7 发送1 m后才开始处理
EventBusTest: 优先级1:事件发送线程: Thread-7,事件处理线程: Thread-7 发送501 m后才开始处理
EventBusTest: 子线程发送耗时 1002ms
EventBusTest: 主线程发送耗时 4ms
EventBusTest: 优先级2:事件发送线程: main,事件处理线程: pool-3-thread-1 发送4 m后才开始处理
EventBusTest: 优先级1:事件发送线程: main,事件处理线程: pool-3-thread-1 发送506 m后才开始处理

从日志可以看出

  • ThreadMode.BACKGROUND线程模型下如果发布线程不是MAIN线程,则事件处理是在当前线程进行,而且同一个线程内是串行执行的,上一个执行完了才会执行下一个。
  • 如果发布线程是MAIN线程,则事件处理线程会切换到非MAIN线程,而且事件处理不会阻塞MAIN线程,同一个线程内也是串行执行。

ThreadMode.ASYNC

事件发送与上面一致,事件处理方法体也一样,只是修改下threadMode

@Subscribe(threadMode = ThreadMode.ASYNC)

结果输出

EventBusTest: 子线程发送耗时 1ms
2EventBusTest: 事件发送线程: Thread-7,事件处理线程: pool-3-thread-1 发送0 m后才开始处理
EventBusTest: 事件发送线程: Thread-7,事件处理线程: pool-3-thread-2 发送0 m后才开始处理
EventBusTest: 主线程发送耗时 2ms
EventBusTest: 事件发送线程: main,事件处理线程: pool-3-thread-1 发送1 m后才开始处理
EventBusTest: 事件发送线程: main,事件处理线程: pool-3-thread-2 发送0 m后才开始处理

ThreadMode.BACKGROUND相似的是,事件处理线程都不会是MAIN线程,不一样的是即使ThreadMode.ASYNC是在非MAIN线程发送的事件,处理事件的线程也不会是原线程,而且事件处理是异步的,不会阻塞原线程,多个事件处理方法也是并行执行。

sticky

粘性事件是指发送了事件之后在注册订阅者,订阅者依然能收到事件。

需要注意的是粘性事件是以参数类型来缓存的,同一类型的事件只会保留最近的一个。

priority

EventBus可以指定事件处理方法的优先级。默认情况下为0。数值越大优先级越高,优先级只有在相同的线程模式下才有意义。

首先我们发送一个事件

MessageEvent event = new MessageEvent(Thread.currentThread().getName());
event.info = "项目100天完成";
EventBus.getDefault().post(event);

然后定义三个不同优先级的事件处理方法

@Subscribe(priority = 0,threadMode = ThreadMode.POSTING)
public void priorityButton0(MessageEvent event) {
    Log.i("EventBusTest","码农收到消息: "+event.info);
}
@Subscribe(priority = 1,threadMode = ThreadMode.POSTING)
public void priorityButton1(MessageEvent event) {
    Log.i("EventBusTest","经理收到消息: "+event.info);
    EventBus.getDefault().cancelEventDelivery(event);
}
@Subscribe(priority = 2,threadMode = ThreadMode.POSTING)
public void priorityButton2(MessageEvent event) {
    Log.i("EventBusTest","主管收到消息: "+event.info);
    event.info = "项目10天完成";
}

既然事件是按照优先级处理的,而且处理的时候能拿到事件本身,那么高优先级的就可以对事件进行修改和取消,所以结果输出如下:

EventBusTest: 主管收到消息: 项目100天完成
EventBusTest: 经理收到消息: 项目10天完成

优先级为2的先收到原始事件,他处理完成后对事件进行了修改,优先级为1的收到的就是他修改之后的事件,在优先级为1的方法处理完成之后将事件取消,导致优先级为0的就收不到事件了。

需要注意的是事件的取消是有限制的

  • 它的线程模型必须是ThreadMode.POSTING
  • 取消的事件不能为null并且必须和发送中的事件一样。
  • 只有在事件发送中才能取消,否则事件都发送完了,取消了个寂寞。

订阅者索引

经过上面的使用我们基本掌握了EventBus常见用法,但是这并没有享受到EventBus3.x给我们带来的性能提升。

我们知道EventBus3.0之后就引入了编译时注解,避免了反射查找订阅方法带来的性能损耗。虽然上面我们引入的是3.3.1的包,但是查找订阅方法使用的依然是反射。虽然默认情况下EventBus3.x会优先使用编译时注解,但是在上面的使用过程中我们并没有提供订阅者索引,导致最后降级为反射查找。

要想享受到性能提升,首先需要配置索引类的信息

android {
    defaultConfig {
            kapt  {
                arguments  {
                    arg("eventBusIndex","com.example.test.MyEventBusIndex")
                }
        }
    }
}
dependencies {
	kapt 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}

build之后,EventBus会生成MyEventBusIndex文件。里面包含了我们的订阅方法的信息。

然后通过EventBusBuilder配置索引类

EventBus eventBus= EventBus.builder().addIndex(new MyEventBusIndex()).build()

虽然通过EventBusBuilder#buildr()可以获取EventBus实例,但是我们在平时使用更多的是EventBus.getDefault()获取,为了让getDefault()方法获取的实例设置了索引类,我们需要使用installDefaultEventBus()方法初始化默认实例defaultInstance

installDefaultEventBus()只能在defaultInstancenull的时候调用,所以一般在Application#onCreate()中调用。

事件继承

默认情况下,发送一个事件,该事件类父类的订阅方法也能得到调用,如果想避免这种情况,只需要调用EventBusBuilder#eventInheritance()方法将eventInheritance属性设置为false就可以了。