Android框架式编程之EventBus
一、EventBus 简介
EventBus是一种用于Android的事件发布-订阅总线,由GreenRobot开发,Gihub地址是:EventBus。
它简化了应用程序内各个组件之间进行通信的复杂度,尤其是碎片之间进行通信的问题,可以避免由于使用广播通信而带来的诸多不便。
1. EventBus的三个角色
-
Event:事件,它可以是任意类型,EventBus会根据事件类型进行全局的通知。
- Subscriber:事件订阅者,在EventBus 3.0之前我们必须定义以onEvent开头的那几个方法,分别是onEvent、onEventMainThread、onEventBackgroundThread和onEventAsync,而在3.0之后事件处理的方法名可以随意取,不过需要加上注解@subscribe,并且指定线程模型,默认是POSTING。
- Publisher:事件的发布者,可以在任意线程里发布事件。一般情况下,使用EventBus.getDefault()就可以得到一个EventBus对象,然后再调用post(Object)方法即可。
2. 四种线程模型
EventBus3.0有四种线程模型,分别是:
- ThreadMode.POSTING,默认的线程模式,在哪个线程发送事件就在对应线程处理事件,避免了线程切换,效率高。
- ThreadMode.MAIN,如在主线程(UI线程)发送事件,则直接在主线程处理事件;如果在子线程发送事件,则先将事件入队列,然后通过 Handler 切换到主线程,依次处理事件。
- ThreadMode.MAIN_ORDERED,无论在那个线程发送事件,都先将事件入队列,然后通过 Handler 切换到主线程,依次处理事件。
- ThreadMode.BACKGROUND,如果在主线程发送事件,则先将事件入队列,然后通过线程池依次处理事件;如果在子线程发送事件,则直接在发送事件的线程处理事件。
- ThreadMode.ASYNC,无论在那个线程发送事件,都将事件入队列,然后通过线程池处理。
二、EventBus 使用
1. 引入依赖
在使用之前先要引入如下依赖:
implementation 'org.greenrobot:eventbus:3.1.1'
2. 定义事件
定义一个事件的封装对象。在程序内部就使用该对象作为通信的信息:
public class MessageWrap { public final String message; public static MessageWrap getInstance(String message) { return new MessageWrap(message); } private MessageWrap(String message) { this.message = message; } }
在定义事件对象的时候,而我们可以对字段进行拓展,方便后期的使用和维护。
3. 发布事件
然后,我们定义一个Activity:
@Route(path = BaseConstants.LIBRARY_EVENT_BUS_ACTIVITY1) public class EventBusActivity1 extends CommonActivity<ActivityEventBus1Binding> { @Override protected void doCreateView(Bundle savedInstanceState) { // 为按钮添加添加单击事件 getBinding().btnReg.setOnClickListener(v -> EventBus.getDefault().register(this)); getBinding().btnNav2.setOnClickListener( v -> ****); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } @Subscribe(threadMode = ThreadMode.MAIN) public void onGetMessage(MessageWrap message) { getBinding().tvMessage.setText(message.message); } }
这里我们当按下按钮的时候向EventBus注册监听,然后按下另一个按钮的时候跳转到拎一个Activity,并在另一个Activity发布我们输入的事件。在上面的Activity中,我们会添加一个监听的方法,即onGetMessage
,这里我们需要为其加入注解Subscribe
并指定线程模型为主线程MAIN
。最后,就是在Activity的onDestroy
方法中取消注册该Activity。
下面是另一个Activity的定义,在这个Activity中,我们当按下按钮的时候从EditText中取出内容并进行发布,然后我们退出到之前的Activity,以测试是否正确监听到发布的内容。
@Route(path = BaseConstants.LIBRARY_EVENT_BUS_ACTIVITY2) public class EventBusActivity2 extends CommonActivity<ActivityEventBus2Binding> { @Override protected void doCreateView(Bundle savedInstanceState) { getBinding().btnPublish.setOnClickListener(v -> publishContent()); } private void publishContent() { String msg = getBinding().etMessage.getText().toString(); EventBus.getDefault().post(MessageWrap.getInstance(msg)); ToastUtils.makeToast("Published : " + msg); } }
根据测试的结果,我们的确成功地接收到了发送的信息。
4. 黏性事件
所谓的黏性事件,就是指发送了该事件之后再订阅者依然能够接收到的事件。使用黏性事件的时候有两个地方需要做些修改。一个是订阅事件的地方,这里我们在先打开的Activity中注册监听黏性事件:
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true) public void onGetStickyEvent(MessageWrap message) { String txt = "Sticky event: " + message.message; getBinding().tvStickyMessage.setText(txt); }
另一个是发布事件的地方,这里我们在新的开的Activity中发布黏性事件。即调用EventBus的postSticky
方法来发布事件:
private void publishStickyontent() { String msg = getBinding().etMessage.getText().toString(); EventBus.getDefault().postSticky(MessageWrap.getInstance(msg)); ToastUtils.makeToast("Published : " + msg); }
按照上面的模式,我们先在第一个Activity中打开第二个Activity,然后在第二个Activity中发布黏性事件,并回到第一个Activity注册EventBus。根据测试结果,当按下注册按钮的时候,会立即触发上面的订阅方法从而获取到了黏性事件。
5. 优先级
在Subscribe
注解中总共有3个参数,上面我们用到了其中的两个,这里我们使用以下第三个参数,即priority
。它用来指定订阅方法的优先级,是一个整数类型的值,默认是0,值越大表示优先级越大。在某个事件被发布出来的时候,优先级较高的订阅方法会首先接受到事件。
为了对优先级进行测试,这里我们需要对上面的代码进行一些修改。这里,我们使用一个布尔类型的变量来判断是否应该取消事件的分发。我们在一个较高优先级的方法中通过该布尔值进行判断,如果未true
就停止该事件的继续分发,从而通过低优先级的订阅方法无法获取到事件来证明优先级较高的订阅方法率先获取到了事件。
这里有几个地方需要注意:
1. 只有当两个订阅方法使用相同的ThreadMode
参数的时候,它们的优先级才会与priority
指定的值一致;
2. 只有当某个订阅方法的ThreadMode
参数为POSTING
的时候,它才能停止该事件的继续分发。
所以,根据以上的内容,我们需要对代码做如下的调整:
// 用来判断是否需要停止事件的继续分发 private boolean stopDelivery = false; @Override protected void doCreateView(Bundle savedInstanceState) { // ... getBinding().btnStop.setOnClickListener(v -> stopDelivery = true); } @Subscribe(threadMode = ThreadMode.POSTING, priority = 0) public void onGetMessage(MessageWrap message) { getBinding().tvMessage.setText(message.message); } // 订阅方法,需要与上面的方法的threadMode一致,并且优先级略高 @Subscribe(threadMode = ThreadMode.POSTING, sticky = true, priority = 1) public void onGetStickyEvent(MessageWrap message) { String txt = "Sticky event: " + message.message; getBinding().tvStickyMessage.setText(txt); if (stopDelivery) { // 终止事件的继续分发 EventBus.getDefault().cancelEventDelivery(message); } }
即我们在之前的代码之上增加了一个按钮,用来将stopDelivery
的值置为true
。该字段随后将会被用来判断是否要终止事件的继续分发,因为我们需要在代码中停止事件的继续分发,所以,我们需要将上面的两个订阅方法的threadMode
的值都置为ThreadMode.POSTING
。
按照,上面的测试方式,首先我们在当前的Activity注册监听,然后跳转到另一个Activity,发布事件并返回。第一次的时候,这里的两个订阅方法都会被触发。然后,我们按下停止分发的按钮,并再次执行上面的逻辑,此时只有优先级较高的方法获取到了事件并将该事件终止。
三、EventBus 订阅索引
使用上述方式使用EventBus时,其注册事件流程主要是在项目运行时通过反射来查找订事件的方法信息,这也是默认的实现,如果项目中有大量的订阅事件的方法,必然会对项目运行时的性能产生影响。其实除了在项目运行时通过反射查找订阅事件的方法信息,EventBus 还提供了在项目编译时通过注解处理器查找订阅事件方法信息的方式,生成一个辅助的索引类来保存这些信息,这个索引类就是Subscriber Index,其实和 ButterKnife 的原理类似。
要在项目编译时查找订阅事件的方法信息,首先要在 app 的 build.gradle 中加入如下配置:
android { defaultConfig { javaCompileOptions { annotationProcessorOptions { // 根据项目实际情况,指定辅助索引类的名称和包名 arguments = [ eventBusIndex : 'com.shh.sometest.MyEventBusIndex' ] } } } } dependencies { compile 'org.greenrobot:eventbus:3.1.1' // 引入注解处理器 annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1' }
然后在项目的 Application 中添加如下配置,以生成一个默认的 EventBus 单例:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
之后的用法就和我们平时使用 EventBus 一样了。当项目编译时会在生成对应的MyEventBusIndex类:
对应的源码如下:
public class MyEventBusIndex implements SubscriberInfoIndex { private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX; static { SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>(); putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] { new SubscriberMethodInfo("changeText", String.class), })); } private static void putIndex(SubscriberInfo info) { SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info); } @Override public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) { SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass); if (info != null) { return info; } else { return null; } } }
其中SUBSCRIBER_INDEX是一个HashMap,保存了当前注册类的 Class 类型和其中事件订阅方法的信息。
接下来分析下使用 Subscriber Index 时 EventBus 的注册流程,我们先分析:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
首先创建一个EventBusBuilder,然后通过addIndex()方法添加索引类的实例:
public EventBusBuilder addIndex(SubscriberInfoIndex index) { if (subscriberInfoIndexes == null) { subscriberInfoIndexes = new ArrayList<>(); } subscriberInfoIndexes.add(index); return this; }
即把生成的索引类的实例保存在subscriberInfoIndexes集合中,然后用installDefaultEventBus()创建默认的 EventBus实例:
public EventBus installDefaultEventBus() { synchronized (EventBus.class) { if (EventBus.defaultInstance != null) { throw new EventBusException("Default instance already exists." + " It may be only set once before it's used the first time to ensure consistent behavior."); } EventBus.defaultInstance = build(); return EventBus.defaultInstance; } }
public EventBus build() { // this 代表当前EventBusBuilder对象 return new EventBus(this); }
即用当前EventBusBuilder对象创建一个 EventBus 实例,这样我们通过EventBusBuilder配置的 Subscriber Index 也就传递到了EventBus实例中,然后赋值给EventBus的 defaultInstance成员变量。之前我们在分析 EventBus 的getDefault()方法时已经见到了defaultInstance:
public static EventBus getDefault() { if (defaultInstance == null) { synchronized (EventBus.class) { if (defaultInstance == null) { defaultInstance = new EventBus(); } } } return defaultInstance; }
所以在 Application 中生成了 EventBus 的默认单例,这样就保证了在项目其它地方执行EventBus.getDefault()就能得到唯一的 EventBus 实例!之前在分析注册流程时有一个方法findUsingInfo():
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { FindState findState = prepareFindState(); findState.initForSubscriber(subscriberClass); while (findState.clazz != null) { // 查找SubscriberInfo findState.subscriberInfo = getSubscriberInfo(findState); // 条件成立 if (findState.subscriberInfo != null) { // 获得当前注册类中所有订阅了事件的方法 SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); for (SubscriberMethod subscriberMethod : array) { // findState.checkAdd()之前已经分析过了,即是否在FindState的anyMethodByEventType已经添加过以当前eventType为key的键值对,没添加过返回true if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { // 将subscriberMethod对象添加到subscriberMethods集合 findState.subscriberMethods.add(subscriberMethod); } } } else { findUsingReflectionInSingleClass(findState); } findState.moveToSuperclass(); } return getMethodsAndRelease(findState); }
findUsingReflectionInSingleClass()
来反射解析订阅事件的方法。我们重点来看getSubscriberInfo()
都做了些什么:private SubscriberInfo getSubscriberInfo(FindState findState) { // 该条件不成立 if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); if (findState.clazz == superclassInfo.getSubscriberClass()) { return superclassInfo; } } // 该条件成立 if (subscriberInfoIndexes != null) { // 遍历索引类实例集合 for (SubscriberInfoIndex index : subscriberInfoIndexes) { // 根据注册类的 Class 类查找SubscriberInfo SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; }
subscriberInfoIndexes就是在前边addIndex()方法中创建的,保存了项目中的索引类实例,即MyEventBusIndex的实例,继续看索引类的getSubscriberInfo()方法,来到了MyEventBusIndex类中:
@Override public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) { SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass); if (info != null) { return info; } else { return null; } }
即根据注册类的 Class 类型从 SUBSCRIBER_INDEX 查找对应的SubscriberInfo,如果我们在注册类中定义了订阅事件的方法,则 info不为空,进而上边findUsingInfo()方法中findState.subscriberInfo != null成立,到这里主要的内容就分析完了,其它的和之前的注册流程一样。
所以 Subscriber Index 的核心就是项目编译时使用注解处理器生成保存事件订阅方法信息的索引类,然后项目运行时将索引类实例设置到 EventBus 中,这样当注册 EventBus 时,从索引类取出当前注册类对应的事件订阅方法信息,以完成最终的注册,避免了运行时反射处理的过程,所以在性能上会有质的提高。项目中可以根据实际的需求决定是否使用 Subscriber Index。