基于观察者模式的事件发布/订阅框架。通过极少的代码实现模块间的通信,无须层层传递。使用方便,性能高,接入成本低,降低耦合,支持多线程的优点。
流程图在EventBus 3.0版本中引入了 EventBusAnnotationPRocessor(注解分析生成索引)技术,大大提高了EventBus的运行效率。
App 的 build.gradle 中:
compile'org.greenrobot:eventbus:3.0.0'首先,在项目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; }注意:如果在EventBus 3.0中使用了索引加速,事件类的修饰符必须为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
根据订阅者的类来找回调方法,把订阅者和回调方法封装成关系,并保存到相应的数据结构中,为随后的事件分发做好准备,最后处理黏性事件:
registerEventBus 3.0使用了注解表示回调,可以出现相同的ThreadMode的回调方法监听相同的事件,此时会根据注册的先后顺序,先注册先分发事件,注意不是先收到事件,收到事件的顺序还是得看poster中Handler的调度。
2.3 post
分析事件后,得到所有监听该事件的订阅者的回调方法,并利用反射来invoke方法,实现回调:
post图中看到poster的调度事件功能,同时调度的单位细化成了Subscription,即每一个方法都有自己的优先级和是否接收黏性事件。在源代码中为了保证post执行不会出现死锁,等待和对同一订阅者发送相同的事件,增加了很多线程保护锁和标志位,值得每个开发者学习。
2.4 unregister
把在注册时往两个数据结构中添加的订阅者信息删除即可:
unregister2.5 黏性事件
为什么要有这个设计呢?举个栗子:在登陆成功后自动播放歌曲,而登陆和监听登陆是同时进行的。如果登陆流程走得特别快,在登陆成功后播放模块才注册了监听。此时播放模块便会错过了【登陆成功】的事件,出现“虽然登陆成功了,回调却没执行”的情况。而如果【登陆成功】这个事件是一个黏性事件的话,那么即使我后来才注册了监听(并且回调方法设置为监听黏性事件),则回调就能在注册的那一刻被执行,这个问题就能被优雅地解决,而不需要额外去定义其他标志位。
3 索引加速
在EventBus 3.0的介绍中,作者提到以前的版本为了保证性能,在遍历寻找订阅者的回调方法时使用反射而不是注解。但现在却能在使用注解的前提下,大幅度提高性能,同时作者在博客中放出了这张对比图:
速度对比性能方面,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只支持跨线程,而不支持跨进程。如果一个app的service起到了另一个进程中,那么注册监听的模块则会收不到另一个进程的EventBus发出的事件。这里可以考虑利用IPC做映射表,并在两个进程中各维护一个EventBus,不过这样就要自己去维护register和unregister的关系,比较繁琐,而且这种情况下通常用广播会更加方便,大家可以思考一下有没有更优的解决方案。
4.3 事件环路
在使用EventBus时,通常会把两个模块相互监听,来达到一个相互回调通信的目的。但这样一旦出现死循环,而且如果没有相应的日志信息,很难定位问题。所以在使用EventBus的模块,如果在回调上有环路,而且回调方法复杂到了一定程度的话,就要考虑把接收事件专门封装成一个子模块,同时考虑避免出现事件环路。
5 写在最后
EventBus并不是重构代码的唯一之选。作为观察者模式的“同门师兄弟”——RxJava,作为功能更为强大的响应式编程框架,可以轻松实现EventBus的事件总线功能(RxBus)。但毕竟大型项目要接入RxJava的成本高,复杂的操作符需要开发者投入更多的时间去学习。所以想在成熟的项目中快速地重构、解耦模块,EventBus依旧是不二之选。
老司机发车了
新闻热点
疑难解答