LightEventBus-轻量高效的事件总线

一、概述

事件总线有多种实现,仅Android平台就有 EventBusLiveEventBusRxBus等多种实现。
笔者之前也写过“50行代码完成事件总线”之类的实现。

最近重新研究了EventBus的源码。
在整理源码的过程中,一方面觉察自己之前实现的“50行代码事件总线”确实简单了些,另一方面则觉得EventBus在性能和代码复杂度方面都有较大的改进空间。
于是我参考EventBus的功能和实现,完成一个简化的版本。

LigthEventBus源码: https://github.com/BillyWei01/LigthEventBus
EventBus用法和源码解析:https://juejin.cn/post/7379831020495749157

二、用法

LightEventBus 的实现参考了 greenrobot 的 EventBus。
为了尽量代码兼容原版 EventBus 的API, 类名沿用EventBus而不是LightEventBus
并且 registerunregisterpost等方法名也沿用了 EventBus 的命名。

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

  1. 订阅方法的定:
    EventBus 通过给类方法添加 @Subscribe 注解来标记“订阅方法”, 并在注解中传入参数。
    LightEventBus 订阅方法不需要声明为类的方法,不需要注解,只需要创建EventHandler实例。
  2. register/unregister:
    EventBus 需要传入声明了订阅方法的“订阅者”对象。
    LightEventBus 传入的是EventHandler的列表。

例如:
EventBus 的用法如下:

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 的用法如下:

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

下面贴一下单个事件的测试代码。

// 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) {
    }
}
// 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) {
    }
}
//  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使用“订阅索引”,注册时比用反射快一些,但是准备阶段(执行addIndex)则相对耗时。
  2. LightEventBus的注册阶段不需要查找方法,所以比EventBus要快。
  3. LightEventBus的发送默认不使用事件继承,所以发送速度也比EventBus快。

四、实现

由于 LightEventBus 参考了 EventBus 的功能和实现。
因此,关于 LightEventBus 的实现,总体上可以参考笔者的另外一篇关于EnvetBus解析的文章:https://juejin.cn/post/7379831020495749157

这里我们先简单引述一下该文章关于 EventBus 的基本实现的描述,
然后再讲述一下 LightEventBus 相对 EventBus 做了那些些变更。

4.1 EventBus的基本实现

EventBus的架构如下:

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的订阅方法类:

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的“订阅方法”定义如下:

// (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. 实现上简化了很多,不再需要“查找方法”,性能也提升了不少,代码也简化了一大半。

例如:

EventBus
LightEventBus

LightEventBus 的实现只有几个文件,其中 “EventBus.kt” 三百多行代码(包含注释),其他文件几行到几十行不等。

4.3 细节处理

除了简化方法查找之外, LightEventBus 还在一些处理细节上的处理。

4.3.1. 事件继承

所谓“事件继承”,是指 :
如果方法订阅的事件类型是父类(或者接口),发布的事件类型是子类(或者实现),则方法能够收到该事件。

EventBus 实现方式是,如果eventInheritance为true(默认为true), 则除了获取事件本身的类型以外,还会去检索事件类型的父类,以及接口。
比如说,发送一个String类型的事件,执行如下:

EventBus - post

事件继承有时候是挺有用的特性。
但大多数情况下,其实发送者是有明确的意图的,发送者只想发送确定的类型给对应订阅者。
例如,登录模块会定义类似LoginEvent之类的类型,
其发送事件时,只期望订阅了LoginEvent类型的订阅者接收,而不期望被关注 Object 类型的订阅者接收。

于是,在LightEventBus的实现中,我将eventInheritance从全局变量改为post方法的参数。
同时,通过方法重载,不传eventInheritance参数的post方法,默认为false;
如果明确想要订阅父类类型的方法能接收到事件,则调用post(event, true)。

    fun post(event: Any) {
        post(event, false)
    }

    fun post(event: Any, eventInheritance: Boolean) {
    }

如此,大部分情况下,发布事件就不需要检索父类和接口了。

4.3.2 事件注册

EventBus实现如下:

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的实现如下:

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上的记录,EventBus已经有两年没有更新了,并且挂了很多issue没有处理;
加上EventBus是不可能变更订阅方法的用法的, 所以我就直接创建一个project来写了。
LightEventBus 毕竟是一个新的事件库, 有不足之处,欢迎交流指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容