一、概述
事件总线有多种实现,仅Android平台就有 EventBus、LiveEventBus、RxBus等多种实现。
笔者之前也写过"50行代码完成事件总线"之类的实现。
最近重新研究了EventBus的源码。
在整理源码的过程中,一方面觉察自己之前实现的"50行代码事件总线"确实简单了些,另一方面则觉得EventBus在性能和代码复杂度方面都有较大的改进空间。
于是我参考EventBus的功能和实现,完成一个简化的版本。
LigthEventBus源码: github.com/BillyWei01/...
EventBus用法和源码解析:juejin.cn/post/737983...
二、用法
LightEventBus 的实现参考了 greenrobot 的 EventBus。
为了尽量代码兼容原版 EventBus 的API, 类名沿用EventBus
而不是LightEventBus
;
并且 register
、unregister
、post
等方法名也沿用了 EventBus 的命名。
在使用上,LightEventBus 和 EventBus 最大的不同之处在于:
- 订阅方法的定义: EventBus 通过给类方法添加
@Subscribe
注解来标记"订阅方法", 并在注解中传入参数。
LightEventBus 订阅方法不需要声明为类的方法,不需要注解,只需要创建EventHandler
实例。 - register/unregister: EventBus 需要传入声明了订阅方法的"订阅者"对象。
LightEventBus 传入的是EventHandler
的列表。
例如: EventBus 的用法如下:
Kotlin
class Event1
class Event2
class EventHandler {
@Subscribe
fun onEvent1(event: Event1) {
}
@Subscribe(threadMode = ThreadMode.ASYNC, sticky = true, priority = 100)
fun onEvent2(event: Event2) {
}
}
class EventBusTest {
private val subscriber = EventHandler()
fun test() {
EventBus.getDefault().register(subscriber)
EventBus.getDefault().post(Event1())
EventBus.getDefault().postSticky(Event2())
EventBus.getDefault().unregister(subscriber)
}
}
LightEventBus 的用法如下:
kotlin
class Event1
class Event2
class LightEventBusTest {
private val handlers = listOf(
EventHandler.create<Event1> { event ->
},
EventHandler.create<Event2>(threadMode = ThreadMode.ASYNC, sticky = true, priority = 100) { event ->
}
)
fun test() {
EventBus.getDefault().register(handlers)
EventBus.getDefault().post(Event1())
EventBus.getDefault().postSticky(Event2())
EventBus.getDefault().unregister(handlers)
}
}
三、性能测试
测试方式:冷启动,记录首次结果(各阶段的耗时,时间单位:ms)
测试设备:Huawei P30 pro
测试代码:Benchmark.kt
下面贴一下单个事件的测试代码。
kotlin
// EventBus, 通过订阅索查找方法
object IndexEventBusTest {
fun test(): String {
val t0 = System.nanoTime()
val handler1 = IndexEvent1Handler()
// 这里触发"添加索引",涉及类加载和方法查找
val eventBus = EventBus.builder().addIndex(AppEventBusIndex()).build()
val t1 = System.nanoTime()
eventBus.register(handler1)
val t2 = System.nanoTime()
eventBus.post(Event1())
val t3 = System.nanoTime()
eventBus.unregister(handler1)
val t4 = System.nanoTime()
return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
"post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
}
}
class IndexEvent1Handler {
@Subscribe(threadMode = ThreadMode.POSTING)
fun onEvent1(event: Event1) {
}
}
kotlin
// EventBus, 通过反射查找方法
object ReflectionEventBusTest {
fun test(): String {
val t0 = System.nanoTime()
val handler1 = ReflectionEvent1Handler()
val t1 = System.nanoTime()
// 查找方法发生在注册阶段
EventBus.getDefault().register(handler1)
val t2 = System.nanoTime()
EventBus.getDefault().post(Event1())
val t3 = System.nanoTime()
EventBus.getDefault().unregister(handler1)
val t4 = System.nanoTime()
return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
"post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
}
}
class ReflectionEvent1Handler {
@Subscribe(threadMode = ThreadMode.POSTING)
fun onEvent1(event: Event1) {
}
}
kotlin
// LightEventBus
object LightEventTest {
fun test() : String {
val t0 = System.nanoTime()
val handler1 = listOf(EventHandler.create<Event1> { })
val t1 = System.nanoTime()
EventBus.getDefault().register(handler1)
val t2 = System.nanoTime()
EventBus.getDefault().post(Event1())
val t3 = System.nanoTime()
EventBus.getDefault().unregister(handler1)
val t4 = System.nanoTime()
return "prepare:${formatTime(t1 - t0)}, register:${formatTime(t2 - t1)}, " +
"post${formatTime(t3 - t2)}, unregister:${formatTime(t4 - t3)}"
}
}
实际上测试代码是由ksp生成, 可以通过配置生成的事件数量,以下是生成100个事件时的测试结果。
方式 | 准备 | 注册 | 发送 | 取消注册 |
---|---|---|---|---|
Index-EventBus | 14.9 | 4.1 | 3.1 | 0.4 |
Reflection-EventBus | 0.8 | 8.7 | 1.6 | 0.4 |
LightEventBus | 0.6 | 0.4 | 1.0 | 0.2 |
备注:
EventBus 3 提供了通过注解处理器生成"订阅索引"来提升EventBus的"方法查找"速度。
Index-EventBus EventBus使用"订阅索引"下的测试结果;
Reflection-EventBus 是EventBus使用反射查找方法下的测试结果。
测试结果解析:
- EventBus使用"订阅索引",注册时比用反射快一些,但是准备阶段则相对耗时。
- LightEventBus的注册阶段不需要查找方法,所以比EventBus要快。
- LightEventBus的发送默认不使用事件继承,所以发送速度也比EventBus快。
四、实现
由于 LightEventBus 参考了 EventBus 的功能和实现。
因此,关于 LightEventBus 的实现,总体上可以参考笔者的另外一篇关于EnvetBus解析的文章:juejin.cn/post/737983...
这里我们先简单引述一下该文章关于 EventBus 的基本实现的描述,
然后再讲述一下 LightEventBus 相对 EventBus 做了那些变更。
4.1 EventBus的基本实现
EventBus的架构如下:
java
public class EventBus {
// 事件 -> 订阅方法列表
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
// 订阅者 -> 关注的事件
private final Map<Object, List<Class<?>>> typesBySubscriber;
public void register(Object subscriber) {
}
public synchronized void unregister(Object subscriber) {
}
public void post(Object event) {
}
}
EventBus的主体功能包含两个容器,三个方法。
容器 :
- typesBySubscriber : 订阅者 -> 关注的事件
- subscriptionsByEventType: 事件 -> 订阅方法列表
方法:
- 订阅 (regiester)
- 检索订阅者的方法,查找其中添加了
@Subscribe
注解的方法; - 取方法参数的类型得到
eventType
, 取注解参数得到threadMode
,sticky
等参数; - 将"订阅者,事件,方法,以及其他参数"记录到
subscriptionsByEventType
和typesBySubscriber
两个Map中。
- 检索订阅者的方法,查找其中添加了
- 取消订阅(unregister)
- 索引订阅者关注的事件列表,遍历事件,移除
subscriptionsByEventType
中关于此事件的订阅方法; - 从
typesBySubscriber
中移除订阅者以及所关联的事件列表。
- 索引订阅者关注的事件列表,遍历事件,移除
- 发布(post)
- 在
subscriptionsByEventType
中索引Event
所关联的Subscription
列表; - 遍历
Subscription
列表,执行其方法。 - 默认情况下,启用事件继承 (eventInheritance) 。
- 在
4.2 实现简化
EventBus 的源码中,查找方法花费了相当多的代码,同时拖慢EventBus的性能。
虽然后来增了注解处理器来支持加速方法查找,但又会引入编译耗时和启动耗时等负面作用。
如果去掉方法查找,换用其他的定义订阅方法的方式,那实现就简单很多了。
EventBus的订阅方法类:
java
final class Subscription {
final Object subscriber;
final SubscriberMethod subscriberMethod;
}
public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class<?> eventType;
final int priority;
final boolean sticky;
}
为了简化使用,在实现上 LightEventBus 做了如下简化:
- 订阅方法不需要定义成某个类的方法,可以一个方法接口(lambda形式)替代。
- 弱化了订阅者的概念(去掉
subscriber
),注册时只需要传入方法列表,也不用考虑继承等复杂因素。
最终,LightEventBus的"订阅方法"定义如下:
kotlin
// (event: T) -> Unit 翻译成Java后,是一个名为 Function1 的接口类型
typealias Action<T> = (event: T) -> Unit
class EventHandler<T>( // 对应 SubscriberMethod
val eventType: Class<*>,
val threadMode: ThreadMode,
val sticky: Boolean,
val priority: Int,
val action: Action<T> // 对应 Method
) {
companion object {
// 增加一个静态方法,方便构建实例 (Kotlin语法糖)
inline fun <reified T> create(
threadMode: ThreadMode = ThreadMode.POSTING,
sticky: Boolean = false,
priority: Int = 0,
noinline action: Action<T>
): EventHandler<T> {
return EventHandler(T::class.java, threadMode, sticky, priority, action)
}
}
}
因为不再使用 Method的概念,故而直接用lambda形式的接口替代原来的"方法",并命名为Action
。
相应地,将"事件的处理"定义为 EventHandler
(对应原版的SubscriberMethod
)。
以上所述,是EventBus和LightEventBus最大差异。
此变更主要影响了两个方面:
- API改变了,这一点 "用法" 一章已有说明,这里不再赘述;
- 实现上简化了很多,不再需要"查找方法",性能也提升了不少,代码也简化了一大半。
例如:
LightEventBus 的实现只有几个文件,其中 "EventBus.kt" 三百多行代码(包含注释),其他文件几行到几十行不等。
4.3 细节处理
除了简化方法查找之外, LightEventBus 还在一些处理细节上的处理。
4.3.1. 事件继承
所谓"事件继承",是指 :
如果方法订阅的事件类型是父类(或者接口),发布的事件类型是子类(或者实现),则方法能够收到该事件。
EventBus 实现方式是,如果eventInheritance
为true(默认为true), 则除了获取事件本身的类型以外,还去检索时间类型的父类,以及接口。
事件继承有时候是挺有用的特性。
但大多数情况下,其实发送者是有明确的意图的,发送者只想发送确定的类型给对应订阅者。
例如,登录模块会定义类似LoginEvent之类的类型,
其发送事件时,只期望订阅了LoginEvent类型的订阅者接收,而不期望被关注 Object 类型的订阅者接收。
于是,在LightEventBus的实现中,我将eventInheritance
从全局变量改为post
方法的参数。
同时,通过方法重载,不传eventInheritance
参数的post
方法,默认为false;
如果明确想要订阅父类类型的方法能接收到事件,则调用post(event, true)。
java
fun post(event: Any) {
post(event, false)
}
fun post(event: Any, eventInheritance: Boolean) {
}
如此,大部分情况下,发布事件就不需要检索父类和接口了。
4.3.2 事件注册
EventBus实现如下:
java
public class EventBus {
// 事件 -> 订阅方法列表
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType;
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
}
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
}
}
其核心处理,概括而言,就是:
- 查找到订阅者的方法列表后,遍历订阅者方法列表;
- 在注册订阅方法时,根据方法的事件类型,从
subscriptionsByEventType
检索事件类型对应的方法列表; - 从头开始比较,找到优先级小于当前订阅方法的位置,插入该方法的后面(使列表中的方法,按按优先级逆序排列)。
另外,EventBus保存订阅方法列表用的是:CopyOnWriteArrayList
。
因为register
和unregister
方法会更新方法列表,而post
方法会查询方法列表;
用CopyOnWriteArrayList
可以避免遍历的过程中写入而引发ConcurrentModificationException
。
LightEventBus的实现如下:
kotlin
class EventBus {
// 事件 -> 订阅者(集合)
private val subscriptions = mutableMapOf<Class<*>, ArrayList<EventHandler<*>>>()
// 正在发送事件的线程的数量
private val postingCount = AtomicInteger()
fun register(handlers: List<EventHandler<*>>) {
synchronized(this) {
handlers.forEach { handler ->
val eventType = handler.eventType
val list = subscriptions.getOrPut(eventType) { ArrayList(2) }
// 如果没有线程正在访问方法列表,则直接添加;
// 如果有,则执行 CopyOnWrite
if (postingCount.get() == 0) {
addHandler(list, handler)
} else {
subscriptions[eventType] = ArrayList(list).apply { addHandler(this, handler) }
}
}
}
}
// 按优先级逆序排列
private fun addHandler(list: ArrayList<EventHandler<*>>, handler: EventHandler<*>) {
val size = list.size
val priority = handler.priority
// 快速判断:列表为空,或者优先级小于等于列表末尾,则直接插入列表末尾
if (size == 0 || priority <= list[size - 1].priority) {
list.add(handler)
} else {
for (i in 0..<size) {
if (priority > list[i].priority ) {
list.add(i, handler)
return
}
}
list.add(size, handler)
}
}
}
相比EventBus, 做了两个处理:
- 优先级处理 由于大部分情况下,使用者不会特别去设置优先级,所有订阅方的优先级基本都是0。 因此,插入列表时,可以直接和列表末尾的方法比较,如果小于或等于其优先级,则插入队列末尾。 如此,就不需要遍历整个列表了。
- CopyOnWrite LightEventBus 增加了一个
postingCount
变量,在发生事件时+1; 在执行register
和unregister
时,如果postingCount
为0,则说明没有任何线程在遍历订阅方法列表; 这时候可以直接添加在当前的方法列表中,而不需要先Copy
再Write
。
五、总结
EventBus是比较优秀的事件通信框架,容易上手,功能丰富。
在研究其源码之后,发现也有可以改进的地方。
但看github上的记录,EnentBus已经有两年没有更新了,并且挂了很多issue没有处理;
加上EventBus是不可能变更订阅方法的用法的, 所以我就直接创建一个Project来写了。 LightEventBus 毕竟是一个新的Project, 有不足之处,欢迎交流指正。