前言
本文章是在笔者在阅读 EventBus 源码后的一些总结,文章不涉及 EventBus 的流程源码分析。
问题
EventBus 的原理
Eventbus 主要由4部分组成,分别是 发布者,订阅者,事件,事件总线。
- 发布者:发布事件,调用
post()发布普通事件,调用postSticky()发布黏性事件。 - 订阅者:可以订阅自己需要的事件,调用
register()后才能接收事件,不需要接收事件可以调用unRegister()。订阅者必须有订阅方法 - 事件:事件可以分为普通事件可黏性事件。普通事件不会被事件总线保留,而黏性事件在被发布后就会被事件总线保留,直到手动移除。
- 事件总线:负责接收和分发事件。
主要工作流程分为:订阅者注册,发布者发布事件,事件总线推送事件。
-
订阅者注册
注册主要有两步:1. 找出订阅者的所有订阅方法。2. 将每个订阅方法和订阅者构成订阅关系,添加到事件总线中,为之后订阅者接收事件做准备。找订阅方法时,先从内存缓存中查找,内存缓存中没有时,默认优先使用索引类,当索引类不存在时才使用反射获取。把订阅关系添加到事件总线时,会检查订阅者是否订阅了黏性事件,如果有,就把黏性事件推送给订阅者。
-
发布者发布事件
发布普通事件用
post(),发布黏性事件用postSticky()。postSticky()只比post()多了一个步骤,就是先把黏性事件保存在事件总线中,再调用post(),保存形式:Map<事件类型,事件实例>。具体的发布过程:通过 ThreadLocal 获得当前线程的事件队列,把要发布的事件入队,之后总线将事件逐个出队并推送给订阅者。 private final Map<Class<?>, Object> stickyEvents; public void postSticky(Object event) { synchronized (stickyEvents) { stickyEvents.put(event.getClass(), event); } post(event); } -
总线推送事件
post()方法正常情况下会调用postSingleEvent(),再调用postSingleEventForEventType()。在postSingleEventForEventType()方法中, 总线根据事件类型找出所有相关的订阅方法(订阅者,订阅方法),并调用postToSubscription()来推送事件。postToSubscription()方法会根据订阅方法的线程模式作不同的处理,最后都会调用invokeSubscriber()来反射调用订阅方法。
五种线程模式
线程模式决定调用订阅方法的是哪个线程,对发布事件的线程没有影响。
- POSTING:哪个线程发布就哪个线程调用订阅方法。
- MAIN:如果发布事件的线程是主线程,则直接调用订阅方法;不是则通过 Handler 切换到主线程并且排队,等待被订阅者接收。
- MAIN_ORDERED:不管发布线程的是不是主线程,都需要排队,等待被订阅者接收。
- BACKGROUND:发布线程本身是子线程就直接调用订阅方法,不是则开启一个子线程调用。
- ASYNC:开启新线程调用订阅方法。
线程切换实现原理
MAIN 和 MAIN_ORDERED 模式是通过 Handlelr 来切换线程的。代码主要在 HandlerPoster 这个类中(Android 平台中),这个类继承了 Handler ,重写了 handleMessage() ,Looper 是主线程的,内部有一个保存事件的队列,我们就叫这个类为发布器吧。当需要从子线程切换到主线程时,使用发布器 enqueue() ,将事件入队,如果发布器不是处于活跃状态的话,就发送一个空 Message ,在 Looper 里排队等待被主线程调用,从而完成从子线程到主线程的切换。
BACKGROUND 模式和 ASYNC 模式都是通过 线程池 来完成线程切换的。对应的类分别是 BackgroundPoster 和 AsyncPoster 。这两个类实现很相似,都实现了 Runnable 接口重写 run() ,内部和 HandlerPoster 一样有一个保存事件的队列。
MAIN 和 MAIN_ORDERED 的区别
MAIN_ORDERED 模式能保证订阅者接收事件的顺序和事件的发布顺序是一致的,而 MAIN 模式不能。
举个通俗点的例子:
1个订阅方法,3个线程(主线程,子线程A,子线程B),这三个线程并发发布事件类型一样的事件,无论是 MAIN 模式还是 MAIN_ORDERED 模式,子线程发布的事件只能乖乖排队等待被接收。但是如果是在 MAIN 模式下,主线程发布的事件可以插队,MAIN_ORDERED 模式则不行。
//线程A发布事件5~9
new Thread(new Runnable() {
@Override
public void run() {
for (int i=5;i<10;i++){
EventBus.getDefault().post(String.valueOf(i));
}
}
}).start();
//线程B发布事件10~15
new Thread(new Runnable() {
@Override
public void run() {
for (int i=10;i<15;i++){
EventBus.getDefault().post(String.valueOf(i));
}
}
}).start();
//主线程发布事件0~5
for (int i=0;i<5;i++){
EventBus.getDefault().post(String.valueOf(i));
}
//在EventBus中postToSubscription方法中添加打印语句,表示发布顺序
public void postToSubscription(...) {
if (event instanceof String){
System.out.println("FFF: 发布事件: "+event);
}
...
}
//订阅方法的模式指定为 MAIN
@Subscribe(threadMode = ThreadMode.MAIN)
public void hello(String str) {
System.out.println("JJJ 接收到事件 "+str);
}
}
运行结果:
可以看到主线程发布的事件都插队了,而子线程发布的事件顺序是一致的。
再把线程模式修改为 MAIN_ORDERED ,运行结果如下:
可以看到事件的发布顺序和接收顺序是一致的。
MAIN , MAIN_ORDERED 模式下对防止主线程阻塞的优化
-
MAIN,MAIN_ORDERED 模式下,订阅方法本来就不应该执行耗时长的任务。一个订阅方法执行时间过长必定会阻塞主线程。
-
但是如果主线程收到了大量事件,即使单个订阅方法的耗时不长,一次性接收完这些事件并执行订阅方法也是可能阻塞主线程的,EventBus 针对这种情况做了优化。
HandlerPoster 继承 Handler, 重写了
handleMessage()@Override public void handleMessage(Message msg) { boolean rescheduled = false; try { // 获取一个开始时间 long started = SystemClock.uptimeMillis(); // 死循环 while (true) { // 取出队列中最前面的元素 PendingPost pendingPost = queue.poll(); // 接下来会进行两次校验操作 // 判空 有可能队列中已经没有元素 if (pendingPost == null) { // 再次检查,这次是同步的 synchronized (this) { // 继续取队头的元素 pendingPost = queue.poll(); if (pendingPost == null) { // 还是为 null,将处理状态设置为不活跃,跳出循环 handlerActive = false; return; } } } // 调用订阅者方法 eventBus.invokeSubscriber(pendingPost); // 获得方法耗时 long timeInMethod = SystemClock.uptimeMillis() - started; // 判断循环耗时是否超过预设值 if (timeInMethod >= maxMillisInsideHandleMessage) { if (!sendMessage(obtainMessage())) { throw new EventBusException("Could not send handler message"); } // 设置为活跃状态 rescheduled = true; return; } } } finally { // 更新 Handle 状态 handlerActive = rescheduled; } }在循环前记录一个开始时间,没调用完一次订阅者方法就更新耗时,这个耗时表示从循环开始到现在所调用的订阅者方法的总耗时,如果这个耗时超过预设值(默认为10ms),就发送一个空 Message ,结束方法,让其他"共享" Handler 执行任务,自己(HandlerPoster本身也是Handler)则继续排队。从而防止一次性推送多个事件而导致阻塞主线程。当然,如果单个订阅方法就耗时过长,这种优化也是没有用的,所以耗时长的订阅方法的线程模式不能是 MAIN,MAIN_ORDERD。
黏性事件的发布与接收
- 黏性事件的发布比普通事件只多了一个步骤,就是把时间实例缓存在一个Map中,形式为:Map<事件类型,事件实例>。之后调用
post()
- 黏性事件的接收分2种情况,第一种,事件发布前订阅者就存在,这种情况跟普通事件一样,是在
post()方法调用后才调用 订阅方法。第二种,事件发布前订阅者不存在,调用订阅方法的时机是在register()中:先检查订阅方法是否接收黏性事件,在根据订阅方法的事件类型从总线中找出匹配的黏性事件,借着调用订阅方法。
EventBus 能否跨进程
不能,事件的发布与接收只能在同一个 EventBus 实例内,不同进程不能共享同一个实例
EventBus 的优缺点
-
优点:
- 简化不同组件之间的通讯
- 解耦发送者和接收者
- 快速,轻量,开销小
- 可动态设置事件处理线程和优先级。
- 避免复杂且容易出错的依赖关系和生命周期问题
-
缺点:
- 每个事件必须自定义一个事件类,增加了维护成本。
为什么只有 POSTING 线程模式能取消事件传递
private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
@Override
protected PostingThreadState initialValue() {
return new PostingThreadState();
}
};
public void cancelEventDelivery(Object event) {
PostingThreadState postingState = currentPostingThreadState.get();
if (!postingState.isPosting) {
throw new EventBusException(
"This method may only be called from inside event handling methods on the posting thread");
} else if (event == null) {
throw new EventBusException("Event may not be null");
} else if (postingState.event != event) {
throw new EventBusException("Only the currently handled event may be aborted");
} else if (postingState.subscription.subscriberMethod.threadMode != ThreadMode.POSTING) {
throw new EventBusException(" event handlers may only abort the incoming event");
}
postingState.canceled = true;
}
可以看到,取消事件的传递是通过设置 postingState 的 canceled 来实现的。而 postingState 是通过 ThreadLocal 获得的,修改一个线程在 ThreadLocal 保存的变量显然对另外一个线程没有影响。而五种线程模式中,除了 POSTING 模式,其他模式都可能存在线程切换,所以取消事件传递只有 POSTING 模式支持。
与 Broadcast(广播) ,LocalBroadcast(本地广播) 的比较
-
广播,四大组件之一,可用于进程间通信,也可以在进程内通信。使用 Broadcast 的开销太大,只在 APP 内部使用却要经过 system_server 进程,两次 Binder Call,并且存在被劫持的风险。
-
本地广播的的设计模式与 EventBus 很像。使用 Intent 传递信息,使用 IntentFliter 过滤信息,需要用到一个 BroadcastReceiver接收信息,都需要注册和解注册,LocalBroadcastManager 相当于总线,负责接收,管理,发送信息。
-
EventBus 相比 LocalBroadcast 的优点:
- LocalBroadcast 不够轻量,数据的传递依赖系统的
BroadcastReceiver,里面糅合了很多跟我们业务无关的东西,违反了 迪米特原则。 - LocalBroadcast 不支持指定接收线程,只有主线程能接收事件,通过 Handler 完成事件分发。
- LocalBroadcast 不支持黏性事件,优先级,取消事件的传递。
- EventBus 的使用起来更简洁。
- LocalBroadcast 不够轻量,数据的传递依赖系统的
EventBus 为什么不常被大项目使用
猜测:在多人协作开发时,如果发布事件不按照一个好的规范,可能导致乱发信息,而且较难找到信息的发送者,比如复用其他开发人员定义的事件。
eventInheritance(事件继承)
注册时对黏性事件的推送和普通事件的推送都会涉及到事件继承。如果事件之间没有继承关系,可以关闭事件继承(eventInheritance默认为true),可以提高性能。比如 lookupAllEventTypes() 可能会多次用到反射,这在事件之间没有继承关系时是没必要的。
APT
EventBus有两种方式寻找订阅方法,一种是利用反射,另外一种就是 APT 。EventBus 非常推荐用户使用 APT 来避免一些反射异常问题。使用方法在这:greenrobot.org/eventbus/do…。另外,使用反射查找订阅方法时,如果注册类的方法很多,必然会导致性能的下降,EventBus在这方面相关优化就是向上查找父类的方法时,如果父类是系统类,就会停止查找,否则像我们平常注册的 Activity 有大量的方法,每个都要需要遍历看看是不是订阅方法,这是完全没必要的。