作者:Markus Junginger
Github:greenrobot/EventBus
原文:老司机教你 “飙” EventBus 3
Is what
基于观察者模式的事件发布/订阅框架。
通过极少的代码实现模块间的通信,无须层层传递。使用方便,性能高,接入成本低,降低耦合,支持多线程。
3.0 新特性
在EventBus 3.0
版本中引入了 EventBusAnnotationProcessor
(注解分析生成索引)技术,大大提高了EventBus
的运行效率。
1. 使用
1.1 添加依赖
在 build.gradle
中添加依赖:
compile'org.greenrobot:eventbus:3.0.0'
1.2 添加加速索引
- 在项目
gradle
的dependencies
中引入apt
编译插件:
classpath'com.neenbedankt.gradle.plugins:android-apt:1.8'
- 在
App
的build.gradle
中应用apt
插件,并设置apt
生成的索引的包名和类名:
applyplugin:'com.neenbedankt.android-apt'
apt {
arguments{
eventBusIndex"com.study.sangerzhong.studyapp.MyEventBusIndex"
}
}
- 在
App
的dependencies
中引入EventBusAnnotationProcessor
:
apt'org.greenrobot:eventbus-annotation-processor:3.0.1'
注意:
- 注解解析依赖于
android-apt-plugin
; - 加速索引可以不加;
- 应用
EventBusAnnotationProcessor
却没有设置arguments
,编译时会报错:No option eventBusIndex passed to annotation processor
,此时需要先编译一次,生成索引类。编译成功之后,会在\ProjectName\app\build\generated\source\apt\PakageName\
下看到通过注解分析生成的索引类,如此便可在初始化EventBus
时应用生成的索引了。
1.3 初始化
两种初始方式:
-
默认单例获取(
EventBus
默认有一个实例)
EventBus mEventBus = EventBus.getDefault();
- 自定义
//如:应用生成好的索引时
EventBus mEventBus = EventBus.builder()
.addIndex(new MyEventBusIndex())
.build();
//如:自定义的设置应用到默认单例中
EventBus mEventBus = EventBus.builder()
.addIndex(newMyEventBusIndex())
.installDefaultEventBus();
1.4 定义事件
所有能被实例化为 Object
的实例都可以作为事件:
public class DriverEvent {
public String info;
}
注意:若使用了索引加速,事件类的修饰符必须为
public
,否则编译时会报错:Subscriber method must be public
。
1.5 监听事件
在订阅事件(接收事件)的模块,注册EventBus
:
//如:Activity 中可写在 onCreate() 方法内
mEventBus.register(Object);
在订阅事件(接收事件)的模块,注销EventBus
:
//如:Activity 中可写在 onDestory() 方法内
mEventBus.unregister(Object);
3.0前,需要区分是否监听黏性(sticky
)事件。
3.0中,改为添加注解的形式:
@Subscribe(threadMode = ThreadMode.POSTING, priority =0, sticky =true)
public void handleEvent ( DriverEventevent ){
Log.d(TAG,event.info);
}
注解有三个参数:
- threadMode: 回调所在的线程
- priority: 优先级
- sticky: 是否接收黏性事件
注册监听模块必须有一个标注Subscribe
注解方法,否则register
时会抛出异常:
Subscriberclass XXX and its super classes havenopublic methods with the@Subscribeannotation
1.6 发送事件
调用post
或postSticky
即可。
接收事件的模块需要注册
发送事件的模块无须注册
EventBus.getDefault().post(new DriverEvent("magnet:?xt=urn:btih……"));
以上便完成了EventBus
的学习。
总结
- 实际使用中,
register
、unregister
通常与Activity
、Fragment
的生命周期相关; -
ThreadMode.MainThread
解决了界面刷新必须在UI
线程的问题; - 黏性事件可以解决了
post
与register
同时执行时的异步问题; - 事件传递没有序列化与反序列化的性能消耗。
2. 原理分析
2.1 和新架构
订阅者模块需要通过EventBus
订阅相关的事件,并准备好处理事件的回调方法;
事件发布者则在适当的时机把事件post
出去,EventBus
就能搞定一切。
在架构方面,EventBus 3.0
与之前稍老版本有不同,如图:
核心类EventBus
,其中subscriptionByEventType
是以事件的类为key
,订阅者的回调方法为value
的映射关系表。即EventBus
在收到事件时,可根据该事件的类型,在subscriptionByEventType
中找到所有监听了该事件的订阅者及处理事件的回调方法。typesBySubscriber
是每个订阅者所监听的事件类型表,在取消注册时通过该表中保存的信息,快速删除subscriptionByEventType
中订阅者的注册信息,避免遍历查找。注册事件、发送事件和注销都是围绕着这两个核心数据结构来展开。Subscription
可以理解为每个订阅者与回调方法的关系,在其他模块发送事件时,会通过这个关系,让订阅者执行回调方法。
回调方法在这里被封装成了SubscriptionMethod
,里面保存了在需要反射invoke
方法时的各种参数,包括优先级,是否接收黏性事件和所在线程等信息。而要生成这些封装好的方法,则需要SubscriberMethodFinder
,它可以在regster
时得到订阅者的所有回调方法,并封装返回给EventBus
。右边的加速器模块,是为了提高SubscriberMethodFinder
的效率。
四种Poster
:是EventBus
能在不同的线程执行回调方法的核心
-
POSTING:调用
post
所在的线程执行回调,不需要poster
调度,直接运行; -
MAIN:
UI
线程回调,如果post
在UI
线程则直接执行,否则通过mainThreadPoster
调度; -
BACKGROUND:
Backgroud
线程回调,如果post
不在UI
线程则直接执行,否则通过backgroundPoster
调度; -
ASYNC:交给线程池管理,直接通过
asyncPoster
调度。
不同的Poster
会在post
事件时,调度相应的事件队列PendingPostQueue
,让每个订阅者的回调方法收到相应的事件,并在其注册的Thread
中运行。而这个事件队列是一个链表,由一个个PendingPost
组成,其中包含了事件,事件订阅者,回调方法这三个核心参数,以及需要执行的下一个PendingPost
。
2.2 register
根据订阅者的类来找回调方法,把订阅者和回调方法封装成关系,并保存到相应的数据结构中,为随后的事件分发做好准备,最后处理黏性事件:
EventBus 3.0
使用了注解表示回调,可以出现相同的ThreadMode
的回调方法监听相同的事件,此时会根据注册的先后顺序,先注册先分发事件,注意不是先收到事件,收到事件的顺序还是得看poster
中Handler
的调度。
2.3 post
分析事件后,得到所有监听该事件的订阅者的回调方法,并利用反射来invoke
方法,实现回调:
图中看到poster
的调度事件功能,同时调度的单位细化成了Subscription
,即每一个方法都有自己的优先级和是否接收黏性事件。源代码中为了保证post
执行不会出现死锁,等待和对同一订阅者发送相同的事件,增加了很多线程保护锁和标志位。
2.4 unregister
把在注册时往两个数据结构中添加的订阅者信息删除即可:
2.5 黏性事件
举栗:在登陆成功后自动播放歌曲,登陆和监听登陆是同时进行的。
- 正常情况:如果登陆流程走得快,在登陆成功后播放模块才注册了监听。此时播放模块便错过了【登陆成功】的事件,出现“虽然登陆成功了,回调却没执行”的情况。
- 粘性事件:如果【登陆成功】是黏性事件,即使后来才注册了监听(回调方法设置为监听黏性事件),则回调就能在注册的那一刻被执行,无需额外定义其他标志位。
3 索引加速
旧版本为了保证性能,在遍历寻找订阅者的回调方法时使用反射而不是注解。而新版本在使用注解的前提下,大幅度提高了性能。作者放出的对比图:
性能方面,EventBus 3.0
由于使用了注解,比起使用反射来遍历方法的2.4版本逊色不少。但开启索引后性能远远超出旧版本。
关于索引加速的具体分析请看原文。
4 其他
4.1 混淆
EventBus 3.0
使用注解的方式。作者的意思是在混淆时就不用再keep
住相应的类和方法。
//运行时,抛出错误
java.lang.NoSuchFieldError: No static field POSTING。
//解决方法:keep住所有eventbus相关的代码
-keepclassde.greenrobot.** {*;}
分析,在SubscriberMethodFinder的findUsingReflection
方法中,在调用Method.getAnnotation()
时获取ThreadMode
这个enum
失败了,所以需要keep
住这个enum
(如下)。
-keeppublicenumorg.greenrobot.eventbus.ThreadMode {
publicstatic*;
}
这样就能编译通过了,如果使用了索引加速,是不会有上面这个问题的。因为在找方法时,调用的不是findUsingReflection
,而是findUsingInfo
。
//使用索引加速后,编译后抛出错误:
Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?
因为生成索引GeneratedSubscriberIndex
是在代码混淆之前进行的,混淆之后类名和方法名都不一样了(上面这个错误是方法无法找到),需要keep
住所有被Subscribe
注解标注的方法:
-keepclassmembersclass* {
@de.greenrobot.event.Subscribe ;
}
这里就得权衡一下利弊:使用了注解不用索引加速,则只需要keep
住EventBus
相关的代码,现有的代码可以正常的进行混淆。而使用了索引加速的话,则需要keep
住相关的方法和类。
4.2 跨进程
目前EventBus
支持跨线程,不支持跨进程。这里可以考虑利用IPC
做映射表,并在两个进程中各维护一个EventBus
,不过这样就要自己去维护register
和unregister
的关系,比较繁琐,这种情况下用广播更加方便。
4.3 事件环路
使用EventBus
,通常会把两个模块相互监听,来达到相互回调通信的目的。一旦出现死循环,且没有相应的日志信息,很难定位问题。所以如果在回调上有环路,且回调方法十分复杂,就要考虑把接收事件专门封装成一个子模块,同时考虑避免出现事件环路。
5 写在最后
EventBus
并不是重构代码的唯一之选。作为观察者模式的“同门师兄弟”——RxJava
,作为功能更为强大的响应式编程框架,可以轻松实现EventBus
的事件总线功能(RxBus)。但毕竟大型项目要接入RxJava
的成本高,复杂的操作符需要开发者投入更多的时间去学习。所以在成熟的项目中快速地重构、解耦模块,EventBus
是不二之选。