Android EventBus 的源码解析(二)

Stella981
• 阅读 611

项目上线,加了个通宵,到现在才下班。虽然身体感觉很疲乏,但是心却不感觉累。

1、源码分析

在分析EventBus源码的时候,我们先从获取一个EventBus实例的方法入手,然后再分别看一下它的注册、取消注册、发布事件以及触发观察方法的代码是如何实现的。在下面的文章中我们将会回答以下几个问题:

  1. 在EventBus中,使用@Subscribe注解的时候指定的ThreadMode是如何实现在不同线程间传递数据的?

  2. 使用注解和反射的时候的效率问题,是否会像Guava的EventBus一样有缓存优化?

  3. 黏性事件是否是通过内部维护了之前发布的数据来实现的,是否使用了缓存?

1.1 获取实例

在创建EventBus实例的时候,一种方式是按照我们上面的形式,通过EventBus的静态方法getDefault来获取一个实例。getDefault本身会调用其内部的构造方法,通过传入一个默认的EventBusBuilder来创建EventBus。此外,我们还可以直接通过EventBus的builder()方法获取一个EventBusBuilder的实例,然后通过该构建者模式来个性化地定制自己的EventBus。即:

// 静态的单例实例

1.2 注册

当调用EventBus实例的register方法的时候,会执行下面的逻辑:

public void register(Object subscriber) {

这里的SubscriberMethod封装了订阅方法(使用@Subscribe注解的方法)类型的信息,它的定义如下所示。从下面可以的代码中我们可以看出,实际上该类就是通过几个字段来存储@Subscribe注解中指定的类型信息,以及一个方法的类型变量。

public class SubscriberMethod {

register方法通过subscriberMethodFinder实例的findSubscriberMethods方法来获取该观察者类型中的所有订阅方法,然后将所有的订阅方法分别进行订阅。下面我们先看下查找订阅者的方法。

查找订阅者的订阅方法

下面是SubscriberMethodFinder中的findSubscriberMethods方法:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {

这里我们先从缓存当中尝试获取某个观察者中的所有订阅方法,如果没有可用缓存的话就从该类中查找订阅方法,并在返回结果之前将这些方法信息放置到缓存当中。这里的ignoreGeneratedIndex参数表示是否忽略注解器生成的MyEventBusIndex,该值默认为false。然后,我们会进入到下面的方法中获取订阅方法信息:

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {

在上面的代码中,会从当前订阅者类开始直到它最顶层的父类进行遍历来获取订阅方法信息。这里在循环的内部会根据我们是否使用了MyEventBusIndex走两条路线,对于我们没有使用它的,会直接使用反射来获取订阅方法信息,即进入2处。

下面是使用反射从订阅者中得到订阅方法的代码:

private void findUsingReflectionInSingleClass(FindState findState) {

这里会对当前类中声明的所有方法进行校验,并将符合要求的方法的信息封装成一个SubscriberMethod对象塞到列表中。

注册订阅方法

直到了如何拿到所有的订阅方法之后,我们回到之前的代码,看下订阅过程中的逻辑:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {

这里涉及到了几个集合,它们是用来做缓存的,还有就是来维护观察者、事件类型和订阅方法之间的关系的。注册观察的方法比较长,我们可以一点一点来看。

首先,会在代码1处将观察者和订阅方法封装成一个Subscription对象。

然后,在2处用到了CopyOnWriteArrayList这个集合,它是一种适用于多读写少场景的数据结构,是一种线程安全的数组型的数据结构,主要用来存储一个事件类型所对应的全部的Subscription对象。EventBus在这里通过一个Map<Class<?>, CopyOnWriteArrayList<Subscription>>类型的哈希表来维护这个映射关系。然后,我们的程序执行到2处,在这里会对Subscription对象的列表进行遍历,并根据订阅方法的优先级,为当前的Subscription对象寻找一个合适的位置。

3的地方主要的逻辑是获取指定的观察者对应的全部的观察事件类型,这里也是通过一个哈希表来维护这种映射关系的。

然后,在代码4处,程序会根据当前的订阅方法是否是黏性的,来决定是否将当前缓存中的信息发送给新订阅的方法。这里会通过checkPostStickyEventToSubscription方法来发送信息,它内部的实现的逻辑和post方法类似,我们不再进行说明。

取消注册的逻辑比较比较简单,基本上就是注册操作反过来——将当前订阅方法的信息从缓存中踢出来,我们不再进行分分析。下面我们分析另一个比较重要的地方,即发送事件相关的逻辑。

1.3 通知

通知的逻辑相对来说会比较复杂一些,因为这里面涉及一些线程之间的操作。我们看下下面的代码吧:

public void post(Object event) {

这里的currentPostingThreadState是一个ThreadLocal类型的变量,其中存储了对应于当前线程的PostingThreadState对象,该对象中存储了当前线程对应的事件列表和线程的状态信息等。从上面的代码中可以看出,post方法会在1处不断从当前线程对应的队列中取出事件并进行发布。下面我们看以下这里的postSingleEvent方法。

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {

在上面的代码中,我们会根据eventInheritance的值决定是否要同时遍历当前事件的所有父类的事件信息并进行分发。如果设置为true就将执行这一操作,并最终使用postSingleEventForEventType对每个事件类型进行处理。

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {

在上面的代码中,我们会通过传入的事件类型到缓存中取寻找它对应的全部的Subscription,然后对得到的Subscription列表进行遍历,并依次调用postToSubscription方法执行事件的发布操作。下面是postToSubscription方法的代码,这里我们会根据订阅方法指定的threadMode信息来执行不同的发布策略。

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {

在上面的方法中,会根据当前的线程状态和订阅方法指定的threadMode信息来决定合适触发方法。

这里的invokeSubscriber会在当前线程中立即调用反射来触发指定的观察者的订阅方法。否则会根据具体的情况将事件加入到不同的队列中进行处理。

这里的mainThreadPoster最终继承自Handler,当调用它的enqueue方法的时候,它会发送一个事件并在它自身的handleMessage方法中从队列中取值并进行处理,从而达到在主线程中分发事件的目的。

这里的backgroundPoster实现了Runnable接口,它会在调用enqueue方法的时候,拿到EventBus的ExecutorService实例,并使用它来执行自己。在它的run方法中会从队列中不断取值来进行执行。

总结

以上就是Android中的EventBus的源码分析,这里我们回答之前提出的几个问题来作结:

  1. 在EventBus中,使用@Subscribe注解的时候指定的ThreadMode是如何实现在不同线程间传递数据的?

要求主线程中的事件通过Handler来实现在主线程中执行,非主线程的方法会使用EventBus内部的ExecutorService来执行。实际在触发方法的时候会根据当前线程的状态和订阅方法的ThreadMode指定的线程状态来决定何时触发方法。非主线程的逻辑会在post的时候加入到一个队列中被随后执行。

  1. 使用注解和反射的时候的效率问题,是否会像Guava的EventBus一样有缓存优化?

内部使用了缓存,确切来说就是维护了一些映射的关系。但是它的缓存没有像Guava一样使用软引用之类方式进行优化,即一直是强引用类型的。

  1. 黏性事件是否是通过内部维护了之前发布的数据来实现的,是否使用了缓存?

黏性事件会通过EventBus内部维护的一个事件类型-黏性事件的哈希表存储,当注册一个观察者的时候,如果发现了它内部有黏性事件监听,会执行post类似的逻辑将事件立即发送给该观察者。

本文分享自微信公众号 - Android架构师成长之路(gh_07f996f00d9b)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
java中比较两个时间的差值
项目背景1.某篇文稿的发布时间是publishDate,例如:2020072118:00:41。2.现要求判断该篇文稿的发布时间是否在近30天之内。publicstaticlongdayDiff(DatecurrentDate,DatepublishDate){LongcurrentTimecurrentDat
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这