项目上线,加了个通宵,到现在才下班。虽然身体感觉很疲乏,但是心却不感觉累。
1、源码分析
在分析EventBus源码的时候,我们先从获取一个EventBus实例的方法入手,然后再分别看一下它的注册、取消注册、发布事件以及触发观察方法的代码是如何实现的。在下面的文章中我们将会回答以下几个问题:
在EventBus中,使用
@Subscribe
注解的时候指定的ThreadMode
是如何实现在不同线程间传递数据的?使用注解和反射的时候的效率问题,是否会像Guava的EventBus一样有缓存优化?
黏性事件是否是通过内部维护了之前发布的数据来实现的,是否使用了缓存?
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的源码分析,这里我们回答之前提出的几个问题来作结:
- 在EventBus中,使用
@Subscribe
注解的时候指定的ThreadMode
是如何实现在不同线程间传递数据的?
要求主线程中的事件通过Handler
来实现在主线程中执行,非主线程的方法会使用EventBus内部的ExecutorService
来执行。实际在触发方法的时候会根据当前线程的状态和订阅方法的ThreadMode
指定的线程状态来决定何时触发方法。非主线程的逻辑会在post
的时候加入到一个队列中被随后执行。
- 使用注解和反射的时候的效率问题,是否会像Guava的EventBus一样有缓存优化?
内部使用了缓存,确切来说就是维护了一些映射的关系。但是它的缓存没有像Guava一样使用软引用之类方式进行优化,即一直是强引用类型的。
- 黏性事件是否是通过内部维护了之前发布的数据来实现的,是否使用了缓存?
黏性事件会通过EventBus内部维护的一个事件类型-黏性事件
的哈希表存储,当注册一个观察者的时候,如果发现了它内部有黏性事件监听,会执行post
类似的逻辑将事件立即发送给该观察者。
本文分享自微信公众号 - Android架构师成长之路(gh_07f996f00d9b)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。