EventBus 3.0源码解析
现在网上讲解EventBus的文章大多数都是针对2.x版本的,比较老旧,本篇文章希望可以给大家在新版本上面带来帮助。
EventBus 是专门为Android设计的用于订阅,发布总线的库,用到这个库的app很多,因为它有很多的优点。比如:
它可以简单Android组件之间的通信
它可以避免了Android四大组件复杂的生命周期处理
它可以让你的代码更为简洁。
先一起了解下如何使用,然后在分析它的源码,知道它的工作原理。我们直接来使用EventBus 3.0,3.x主要的一个新的特性就是使用了注解,我们的 Subscribe 可以在代码中就指定我们的 EventBus 使用什么 ThreadMode,是否粘性事件,优先级。
这些,等会在源码分析的时候会讲,先看看如何使用:
视图上显示一个TextView和一个Button,点击按钮的时候,就执行NetWork任务请求,请求执行结束后,在 onEventMainThread 方法更新UI,下面是 NetWork :
代码看起来要比广播和回调方便多了,下面是UI显示:
初始化:
上面是 EventBus 初始化的三个步骤,直观上看用到 单例模式和Builder模式,将构造参数给分离了出来,实际上还用到了策略模式,其中Builder中有些参数用于代码执行的策略,就说,你传的参数不一样,我执行的方式不一样,像 ignoreGeneratedIndex 作用就是让 EventBus 如何查找出订阅方法的策略。这些布尔类型的参数,在分析代码中可以逐步的了解到,先了解一些缓存对象,以更容易的了解源码:
subscriptionsByEventType : 内部是一个Map集合,可以根据 EventType 查找订阅事件。
typesBySubscriber : 根据我们的订阅对象找到 EventType。
stickyEvents : 粘性事件的缓存。
事件投递者 : mainThreadPoster, backgroundPoster, asyncPoster 根据订阅注解ThreadMode 去选择不同的投递者,不同投递者投递事件,接收函数会执行在不同的线程中。
subscriberMethodFinder :查找方法用的,内部维护了一个订阅方法的集合。
注册:
当我们调用 register(this) 的时候就把订阅者给传了进来,代码量很少,主要就两个步骤,第一个 findSubscriberMethods 找出一个 SubscriberMethod 的集合,然后就遍历SubscriberMethod 去订阅事件,我们先看看 findSubscriberMethods() 里面到底做了什么,返回的是什么。
首先从缓存中查找,如果找到了就立马返回。如果缓存中没有的话,则根据ignoreGeneratedIndex 选择如何查找订阅方法,最后,找到订阅方法后,放入缓存,以免下次继续查找。ignoreGeneratedIndex 默认就是 false,那我们会执行 findUsingInfo() 方法,但是这里先分析 findUsingReflection(),因为在我们没有做任何配置的情况下还是会执行上面的findUsingReflection(),就是通过反射来解析注解。
从上面的代码块,我们可以看到,第一步准备 FindState,我们点进去看看:
这里我们先了解从池中拿出一个 FindState对象,FindState 中维护的就是我们对订阅方法查找结果的封装。其实,往后面,我们就知道作者这里设计的非常精妙。第二步,initForSubscriber() 就是将我们的订阅者传给FindState对象。第三步做的就是不断从订阅者和订阅者的父类去查找订阅方法,一起看 findUsingReflectionInSingleClass():
代码很长,其实就是对订阅者方法的遍历,看有没有注解,有的话就解析注解,最后将找到的订阅方法的集合封装到 FindState 对象中的 subscriberMethods 集合中。我们解析完了之后,在看 findUsingReflection() 方法的最后,返回了 getMethodsAndRelease(FindState),将我们的 FindState 传给了getMethodsAndRelease() 方法,好,我们点进去:
从这里,我们就知道作者设计 FindState池的初心了,解析完了之后,将订阅方法赋给List集合,再回收 FindState,继续接收解析,内存没有半点浪费。最后,我们返回的是一个订阅方法的集合。这样,我们通过反射解析注解,找到订阅方法的方式,我们已经分析完了。再看看通过apt处理器来找,我们知道apt处理,是针对源码的处理,是执行在编译过程中的。所以性能要比反射好的多,所以推荐大家使用这种方式。好,那最开始,我说使用 findUsingInfo() 方法如果没有配置还是使用反射呢,我们一起看看:
上面,我注释解析的很清楚了,前几步的操作基本一致,主要看 getSubscriberInfo(findState)这个方法:
上面一段代码是针对缓存的,如果缓存中有就直接返回,底下,是从 subscriberInfoIndexes中查找订阅信息的。那么,我们的 subscriberInfoIndexes 从哪来的呢,是 EventBus Builder对象:
public class EventBusBuilder {
private final static ExecutorService DEFAULT_EXECUTOR_SERVICE = Executors.newCachedThreadPool();
......
List<SubscriberInfoIndex> subscriberInfoIndexes;
默认是空的,所以结合上面的代码,还是执行了 findUsingReflection() 方法。那么,我们应该如何操作呢?我们需要配置下。第一步引入EventBusAnnotationProcessor 库:
这个库是在预编译过程,对我们的代码进行处理,找到我们有订阅方法的类的。第二步,配置项目的build.gradle文件:
将我们的库和 andorid-apt 引入,其中参数 arguments 是指定,我们处理完我们的源码文件,生成的文件名的。好,我们rebuild一下我们的项目。然后在build文件夹下面发现了:
我们点进去看看:
把我们的订阅类和订阅方法全找出来了,当,我们这个类初始化的时候,我们的SUBSCRIBER_INDEX这个集合就已经有我们的订阅信息了。所以,你知道为什么,叫ignoreGeneratedIndex了把。好,最后一步,我们就通过EventBus初始化,将这个类给传进去:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus().register(this);
这样,它就会通过apt方式来处理我们的注解了,需要强调的是,我们是在编译过程中就已经找到我们的订阅信息了,不会在运行期做任何事情,所以这个效率是非常高的。好了,我们查找订阅的代码已经全部分析完了,这个部分是3.0最核心的代码,作者很精心的设计以提高EventBus的性能,我们看完之后也受益良多啊。好,继续分析,继续看findUsingInfo()方法,之前,我注释的也很清楚,但是还有个地方需要注意:
我们添加订阅方法的时候还有个检查,我们分析下:
这里,有两步检查,第一步检查,很好懂,第二步,根据什么签名来检查,我们看看:
其实作者,这里又做了一个优化,将方法名和事件类型当作key,来保存方法,将传来的方法类型和我们签名的保存的比较,如果我们保存的是父类,就返回 true,如果是子类,就将传来的方法保存起来,返回 false。这样做的意图是,如果有父类的方法了,就没有必要添加子类的方法了,因为继承会执行到的。至此,我们对查找订阅方法的过程已经完全分析完了。看懂了之后,非常的过瘾。无论哪种方式查找,都返回了SubscriberMethod 对象,我们看看它维护了什么属性:
/**
* Used internally by EventBus
* and generated subscriber indexes.
*/
public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class<?> eventType;
final int priority;
final boolean sticky;
/** Used for efficient comparison */
String methodString;
订阅:
订阅的代码比较长,但是理解起来并不难,而且,我也做了很详细的注释,其实里面就做了两件事,将我们的订阅方法和订阅者,封装到 subscriptionsByEventType和typesBySubscriber,至于这两个对象是干什么的呢?第一个是我们投递订阅事件的时候,就是根据我们的 EventType 找到我们的订阅事件,从而去分发事件,处理事件的;第二个是在调用 unregister(this) 的时候,根据订阅者找到我们的 EventType,又根据我们的 EventType 找到订阅事件,从而解绑用的,由于篇幅问题就不详细分析了。第二件事,就是如果是粘性事件的话,就立马投递、执行。
发布
当我们调用 post(Object object) 的方法的时候就执行了上面的代码,PostingThreadState 是维护了投递的状态,最后循环投递,直到 PostingThreadState 中的 EventQueue 为空。那么代码最终执行到:
那么,我们就会根据 ThreadMode 去处理事件了。也是由于篇幅的问题,就分析一种了,当线程模式是主线程的时候,意味着,我们需要执行的代码在主线程中操作。如果是主线程,就是通过反射,直接运行订阅的方法,如果不是主线程,我们需要 mainThreadPoster 将我们的订阅事件入队列,一起看看 mainThreadPoster 的工作原理:
其实,在 EventBus初始化的时候,mainThreadPoster 就已经获取主线程的Looper了,就是用到了我们Android的消息处理机制:Looper,Handler。至于消息队列是自己维护的一个单向的链表。每次向Andorid的主线程Looper投递一个空消息,然后在 HandlerMessage() 方法里面从自己维护的队列中取出 PendingPost 进行处理。
final class PendingPost {
private final static List<PendingPost> pendingPostPool = new ArrayList<PendingPost>(); Object event;
Subscription subscription;
PendingPost next;
而 PendingPost 中维护的是订阅事件,EventType 和下一个 PendingPost 的地址。
反注册
反注册就是通过,我们EventBus中typesBySubscriber这个属性,通过订阅者去查找订阅事件,然后去一一解绑的。当然,反注册主要是为了提高效率的,不然订阅的事件太多,非常影响性能。
新特性-粘性事件:
是否还记得,我们看订阅中的最后一段代码呢?不记得,也没事,回顾一下:
我们来看看 checkPostStickyEventToSubscription:
看到没有,如果我们在注册的时候,指定我们要发布粘性事件,那么我们在订阅的时候,就立马调用 postToSubscription,去发布了,至于它从缓存中 stickyEvents 中获取订阅事件,你可能要问,我们什么时候把EventType放进去的呢?
public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted,
// in case the subscriber wants to remove immediately
post(event);
}
没错,就是我们在调用 postSticky() 方法的时候,放入了缓存。当然,你也可以移除粘性事件:
根据源码的分析,我们把上面的代码改成粘性事件。首先,在 NetWork 里面:
@Override
protected void onPostExecute(Integer integer) {
if(integer == SUCCESS){
EventBus.getDefault().postSticky(NetTask.this);
}
}
然后,在MainActivity跳转到SecondActivity,让SecondActivity粘性注册:
这样,我们只要一打开SecondActivity,TextView上面就会显示Event Bus,下面是UI显示:
思考:
分析了好久的源码,我们收获很多,除了对框架的理解得到了升华,其次,我们也体会到了作者的设计思想,这在我们平常工作中都可以去尝试,体验 。最后,我们来思考下使用它的优点。首先和Android自带的组件广播相比,除了代码简洁之外,更多的是,我们能获取到更多的信息,众所周知,观察者有两种模式,一种被观察者推,一种是观察者自己拉取。
我们可以在投递的过程把被观察着本身投递出去,然后从被观察者身(NetTask)上拉取数据。既然提到观察者模式,我们和jdk自带的观察者模式对比,相信很多人都在Android代码中使用jdk封装好的类吧,那么我就随便举个例子:
我们写个任务的基类来继承Observable,当任务执行完了之后,通知观察者们去取,我们只要在Activity中实现Observer,实现方法update就可以完成数据的更新:
这种实现方式虽然也能达到和 EventBus 相同的作用,但是和我们的 EventBus 相比,就更能突出我们 EventBus 最大的优势就是解耦和,将业务和视图分离。只要在BaseAcitity中注册,解绑。通过注解我们就能很随意的获取到任何回调的结果,更为任性的是我们想在哪个线程中运行就再哪个线程中运行,并且接收事件的先后顺序也可以自定义。代码写出来更加的优雅,灵活。
也许,你可能会说,性能相比呢?确实,我们的EventBus用用到了大量的缓存和反射,但是3.0后,已经做了很大改变了,我们可以通过apt预编译找到订阅着,避免了运行期间的反射处理解析。更为重要的是,牺牲一点内存达到代码的解耦,以后维护起来更加的方便,我觉得还是有所需要的。最后和Otto框架来比较,你就会知道EventBus框架是多么的优秀了!
EventBus 3.0源码解析的更多相关文章
- Android事件总线(二)EventBus3.0源码解析
1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus ...
- solr&lucene3.6.0源码解析(四)
本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...
- solr&lucene3.6.0源码解析(三)
solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...
- Heritrix 3.1.0 源码解析(三十七)
今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...
- solr&lucene3.6.0源码解析(二)
上文描述了solr3.6.0怎么采用maven管理的方式在eclipse中搭建开发环境,在solr中,为了提高搜索性能,采用了缓存机制,这里描述的是LRU缓存,这里用到了 LinkedHashMap类 ...
- solr&lucene3.6.0源码解析(一)
本文作为系列的第一篇,主要描述的是solr3.6.0开发环境的搭建 首先我们需要从官方网站下载solr的相关文件,下载地址为http://archive.apache.org/dist/luc ...
- apache mina2.0源码解析(一)
apache mina是一个基于java nio的网络通信框架,为TCP UDP ARP等协议提供了一致的编程模型:其源码结构展示了优秀的设计案例,可以为我们的编程事业提供参考. 依照惯例,首先搭建a ...
- EventBus3.0源码解析
本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...
- Retrofit2.0源码解析
欢迎访问我的个人博客 ,原文链接:http://wensibo.net/2017/09/05/retrofit/ ,未经允许不得转载! 今天是九月的第四天了,学校也正式开学,趁着大学最后一年的这大好时 ...
随机推荐
- 【xsy1230】 树(tree) 点分治+线段树
题目大意:有一棵$n$个节点的树,点的标号为$1$到$n$.树中的边有边权.给你$m$个询问,每个询问包含三个参数$l,r,pos$,你要求出标号在$l$到$r$之间的所有点中,到节点$pos$距离最 ...
- MapReduce求最大值最小值问题
import java.io.File; import java.io.IOException; import org.apache.hadoop.conf.Configuration; import ...
- PHP SNOOPY采集类 总结
1.基础教程 Snoopy的一些特点: 1抓取网页的内容 fetch 2 抓取网页的文本内容 (去除HTML标签) fetchtext 3抓取网页的链接,表单 fetchlinks fetchform ...
- 如何正确删除VMare虚拟机上的系统机器(图文详解)
不多说,直接上干货! 打开虚拟机进入操作系统列表 在操作系统列表,点击要删除的操作系统,如win7, 点击要删除的操作系统后,在主菜单中找到虚拟机. 如图所示,从磁盘中彻底删除. 是 即,成功从虚拟机 ...
- linux内核学习之保护模式(一)
来源:http://blog.csdn.net/yishuige/article/details/50434746 这一章涉及intel8086系列cpu的保护模式编程,应该是学习内核编程,驱动编程及 ...
- mongodb 错误 SCRAM-SHA-1 authentication failed for --转
log 日志错误信息 2018-10-24T16:14:42.244+0800 I NETWORK [initandlisten] connection accepted from 192.168.1 ...
- CentOS SVN Failed to load JavaHL Library
在CentOS 6上的eclipse安装了svbclipse插件后,svn不能使用,并且第一次使用的时候还出现下面错误窗口提示 Failed to load JavaHL Library. These ...
- sparkthriftserver启动及调优
Sparkthriftserver启用及优化 1. 概述 sparkthriftserver用于提供远程odbc调用,在远端执行hive sql查询.默认监听10000端口,Hiveserver2默 ...
- SSH 整合 (Maven)
1.SSH 教程详见我的上一篇博客 SSH(Struts 2.3.31 + Spring 4.1.6 + Hibernate 5.0.12 + Ajax)框架整合实现简单的增删改查(包含分页,Ajax ...
- 在LaTeX中配置西夏文字体与环境
目录 1 配置字族 2 粗体.斜体设定 3 文本编辑器的字体设定(以Sublime Text为例) 4 附录:一些字体的下载源 警告:这篇文章的部分内容需要西夏文字体才能正常显示.若您需要安装,可参考 ...