LightEventBus-轻量高效的事件总线

一、概述

事件总线有多种实现,仅Android平台就有 EventBusLiveEventBusRxBus等多种实现。

笔者之前也写过"50行代码完成事件总线"之类的实现。

最近重新研究了EventBus的源码。

在整理源码的过程中,一方面觉察自己之前实现的"50行代码事件总线"确实简单了些,另一方面则觉得EventBus在性能和代码复杂度方面都有较大的改进空间。

于是我参考EventBus的功能和实现,完成一个简化的版本。

LigthEventBus源码: github.com/BillyWei01/...

EventBus用法和源码解析:juejin.cn/post/737983...

二、用法

LightEventBus 的实现参考了 greenrobot 的 EventBus。

为了尽量代码兼容原版 EventBus 的API, 类名沿用EventBus而不是LightEventBus

并且 registerunregisterpost等方法名也沿用了 EventBus 的命名。

在使用上,LightEventBus 和 EventBus 最大的不同之处在于:

  1. 订阅方法的定义: EventBus 通过给类方法添加 @Subscribe 注解来标记"订阅方法", 并在注解中传入参数。
    LightEventBus 订阅方法不需要声明为类的方法,不需要注解,只需要创建EventHandler实例。
  2. 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使用反射查找方法下的测试结果。

测试结果解析:

  1. EventBus使用"订阅索引",注册时比用反射快一些,但是准备阶段则相对耗时。
  2. LightEventBus的注册阶段不需要查找方法,所以比EventBus要快。
  3. 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等参数;
    • 将"订阅者,事件,方法,以及其他参数"记录到 subscriptionsByEventTypetypesBySubscriber两个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 做了如下简化:

  1. 订阅方法不需要定义成某个类的方法,可以一个方法接口(lambda形式)替代。
  2. 弱化了订阅者的概念(去掉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最大差异。

此变更主要影响了两个方面:

  1. API改变了,这一点 "用法" 一章已有说明,这里不再赘述;
  2. 实现上简化了很多,不再需要"查找方法",性能也提升了不少,代码也简化了一大半。

例如:

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;
            }
        }
    }
}

其核心处理,概括而言,就是:

  1. 查找到订阅者的方法列表后,遍历订阅者方法列表;
  2. 在注册订阅方法时,根据方法的事件类型,从 subscriptionsByEventType 检索事件类型对应的方法列表;
  3. 从头开始比较,找到优先级小于当前订阅方法的位置,插入该方法的后面(使列表中的方法,按按优先级逆序排列)。

另外,EventBus保存订阅方法列表用的是:CopyOnWriteArrayList

因为registerunregister方法会更新方法列表,而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, 做了两个处理:

  1. 优先级处理 由于大部分情况下,使用者不会特别去设置优先级,所有订阅方的优先级基本都是0。 因此,插入列表时,可以直接和列表末尾的方法比较,如果小于或等于其优先级,则插入队列末尾。 如此,就不需要遍历整个列表了。
  2. CopyOnWrite LightEventBus 增加了一个 postingCount 变量,在发生事件时+1; 在执行registerunregister时,如果 postingCount 为0,则说明没有任何线程在遍历订阅方法列表; 这时候可以直接添加在当前的方法列表中,而不需要先CopyWrite

五、总结

EventBus是比较优秀的事件通信框架,容易上手,功能丰富。

在研究其源码之后,发现也有可以改进的地方。

但看github上的记录,EnentBus已经有两年没有更新了,并且挂了很多issue没有处理;

加上EventBus是不可能变更订阅方法的用法的, 所以我就直接创建一个Project来写了。 LightEventBus 毕竟是一个新的Project, 有不足之处,欢迎交流指正。

相关推荐
HerayChen4 分钟前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野5 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11237 分钟前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件33 分钟前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252031 小时前
group_concat配置影响程序出bug
android·bug
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea
- 羊羊不超越 -2 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨3 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow