EventBus总结

前两天去面试,问起EventBus,发觉自己表达还是很糟糕啊~

EventBus是什么?应用场景?

方便组件通讯,降低耦合

例子:

问题需求:

QQ查找附近好友列表页面,跟这些用户关系,好友或非好友,查看某个非好友用户详情页面,并在详情页面添加为好友, 返回到列表页面时,与他的关系没有更新;

传统解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
//1. 定义接口
interface ChangeRelationListener{

//更新列表中某个用户的状态
fun change(userId:Int,newRelation){

}
}

//2. 列表页面实现该接口,并实现修改列表某个用户状态

//3.详情页面,获取到列表页面的引用(该接口实现的实例),调用change方法

其中麻烦的有两点:繁琐的创建额外一个接口;获取到接口实现的引用

当然,也可以不实现接口,详情页面直接获取到列表页面的引用,但这就不符合设计原则了,详情页面不该拥有对列表页面的全部控制权。

EventBus解决方案

  1. 列表页面作为订阅者,订阅修改状态的事件
  2. 详情页面作为发布者,发布状态更新的事件

这样,代码简单,耦合度也低了。
当然,评判好坏,偶合低的另一极端是,容易导致滥用,在多处地方发布时间,管理不当,可能会导致总线拥堵。

EventBus的实现原理?

注册订阅者时,遍历该订阅者类中标记@Subscriber的方法,明确这个订阅者订阅了什么事件,以及针对该事件该怎么处理,即将订阅者的信息 注册到EventBus实例中。
发布者发送事件时,查询EventBus中订阅有该事件的订阅者,调用相应订阅方法

其中,如何获取到订阅者信息呢?有两种方式。

  1. 运行时,通过反射获取订阅者信息
  2. 编译时,通过注解处理器生成订阅者信息的索引

对比两者优劣,前者简单,后者稍有麻烦,通过减少注册时消耗的时间,提升性能

注意,无论使用选择哪种方式,都要注意混淆时保留订阅者的处理事件方法的信息

1
2
3
4
5
-keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

编译时生成订阅者索引的具体操作方法:

使用kotlin如下,java环境参考官网文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

1. 应用对kotlin处理注解插件
apply plugin: 'kotlin-kapt' // ensure kapt plugin is applied

2. 添加依赖
dependencies {
implementation 'org.greenrobot:eventbus:3.1.1'
kapt 'org.greenrobot:eventbus-annotation-processor:3.1.1'
}

3. 确定生命索引的名字,在EventBus实例中用到
kapt {
arguments {
arg('eventBusIndex', 'com.example.myapp.MyEventBusIndex')
}
}


4. 创建实例时添加该索引
EventBus.builder().addIndex(MyEventBusIndex()).installDefaultEventBus()

ThreadMode 列举描述?

threadMode线程模式描述了处理事件应该在哪个线程

  • POSTING 与发送事件的线程相同
  • MAIN 始终在主线程
  • MAIN_ORDERED 基于MAIN,并保证事件会同步有序处理
  • BACKGROUND 与MAIN相反,是在不会在主线
  • ASYNC 始终不会在发送事件的线程,发送事件事件方法调用会立刻返回

具体解析:

POSTING是默认的,这是为了性能考虑。当发送事件的线程和处理事件线程相同时,不会引起线程切换,而是直接的方法调用
直接方法调用存在一个问题:
发送事件的方法要等到处理事件的方法返回才能返回,假设处理方法耗时很久,发送事件方法也要一直等。
MAIN模式中会有一个问题:
假设使用了MAIN模式来处理事件,线程t1, t2先后分别发送了e1,e2, 主线程会先处理e1,若是处理e1未结束,e2事件到来, 那么主线程会交错处理t1,t2,可能不会是我们预想的结果,先处理完t1,再开始处理t2

开始没有想通,故写了demo来验证,可参考

故可以使用MAIN_ORDERED来实现这个需求,内部通过一个维护一个pendingPostPool等待发送的事件列表,这样,才能够等t1处理完,在开始处理 t2.

Async 很强大保证每个发送的事件都会尽快处理,因为内部默认使用了可缓存的线程池CacheThreadPool来处理多个任务,避免多个事件到来时需要 等待处理。

newCacheThreadPool 描述:

作为一个线程池,当没有任务到来时不会创建有线程,
有任务到来会创建线程来处理,处理结束后,不会直接销毁,而是保留一段时间(默认60s),存活期间可处理继续处理任务,避免了创建线程影响开销。

相应的,除了上面提到的线程池,java默认还提供了3中线程池:

  • newFixedThreadPool 默认就创建了几个核心的线程,核心线程不会因为任务结束而销毁,且也会限制最大的线程个数
  • newScheduledThreadPool 定时或周期性执行任务
  • newSingleThreadExecutor 使用单线程来处理任务,未处理的线程会进入等待队列