背景
设计这个 BusUtils
其实是在做 ApiUtils 时顺手做的,因为两者实现方式基本一致,设计前我也没想着要和 greenrobot 的 EventBus
一较高低,但设计完总需要一个对比,所以就拿业界最优秀的事件总线 EventBus
比较一下吧,然后就发现我这区区 300 行不到的 BusUtils
性能比 EventBus
要高出好多,当然,这一切的前提都是在 BusUtils
是切实可用并且有效的,它也是一款线程安全的事件总线,这些我都在单测中有做过实际测试的,不吹不擂,后面我们拿数据说话,有小伙伴不相信的话也可以通过下载我的源码来比较即可,单测地址:BusUtilsVsEventBusTest,Android 测试地址:BusCompareActivity,BusUtils
在 AucFrame 中的作用就是模块内传值,其扮演的角色如下所示:
下面介绍其使用:
使用
配置
在项目根目录的 build.gradle
中添加 bus
插件:
1 | buildscript { |
然后在 application 模块中使用该插件:
1 | apply plugin: "com.blankj.bus" |
给你的项目添加 AndroidUtilCode 依赖:
1 | api "com.blankj:utilcode:latest_version |
如果你单纯只想引入 BusUtils
也是可以的,需要你自己拷贝一份这个类放到你工程里,记得还要拷贝 ThreadUtils
哦,然后在 app 下的 build.gradle
中 配置 bus 的 DSL 域如下所示:
1 | bus { |
可以猜测到默认的 busUtilsClass 为 com.blankj.utilcode.util.BusUtils
哈。
如果开启混淆的话还需要配置你的 BusUtils
中注解方法的防混淆,如果直接用 AndroidUtilCode 的话是不需要你配置的,我已经帮你做完了,配置你自己的 BusUtils
防混淆应该如下所示:
1 | -keepattributes *Annotation* |
当然,如果你项目是开启混淆的话,全量引入 AndroidUtilCode 也是可以的,混淆会帮你去除未使用到的类和方法。
好了,插件和依赖都配置完毕,下面介绍基本使用。
基本使用
1 | public static final String TAG_NO_PARAM = "TagNoParam"; |
使用过 EventBus
的肯定一下子就能看懂。
高级使用
粘性事件
支持粘性事件,也就是先发送,然后在订阅的时候接收到之前发送的粘性事件,把其消费掉,使用方式和 EventBus
一致,就是在 `@BusUtils.Bus注解中设置
sticky = true`,具体例子如下所示:1
2
3
4
5
6
7
8
9
10
11
12public static final String TAG_NO_PARAM_STICKY = "TagNoParamSticky";
true) .Bus(tag = TAG_NO_PARAM_STICKY, sticky =
public void noParamStickyFun() {/* Do something */}
BusUtils.postSticky(TAG_NO_PARAM_STICKY);
BusUtils.register(xxx);// will invoke noParamStickyFun
BusUtils.removeSticky(TAG_NO_PARAM_STICKY);// When u needn't use the sticky, remove it
BusUtils.unregister(xxx);
线程切换
线程切换使用的是 ThreadUtils 中的线程池,它具有安全的 Cached 线程池,以及 MAIN, IO, CPU, CACHED, SINGLE 线程池,默认不设置的话就是在提交的线程 POSTING,使用的话就是在 `@BusUtils.Bus注解中设置
threadMode = BusUtils.ThreadMode.xx` 即可。
规范
要想工具用得舒服,规范肯定要遵守的,所谓无规矩不成方圆,不然五花八门的问题肯定一堆堆,这里推荐如下规范:
- 持有事件的类和函数确保确保都是
public
的。 - 由于
BusUtils
是用于模块内调用,所以可以写一个BusConfig
的类来保存一个模块内所有 bus 的Tag
,方便查找到使用方及调用方。 Tag
中最好还能带有业务模块后缀名防止重复,是 sticky 类型的话也带上 sticky,指定具体线程的话也带上线程名,例如:update_avatar_sticky_main_info
这个Tag
,让人直接望文生义。- 如果能结合 AucFrame 来使用,那就更规范不过了。
- 对
BusUtils
中事件传输的的bean
都需要keep
下来,否则开启混淆后会找不到该实体对象而报错。
使用已经介绍完毕,下面我们来和 EventBus
对比下性能。
性能测试
首先,把两者的事件定义好,因为比较的是事件达到的快慢,所以内部都是空实现即可,具体代码如下所示:
1 |
|
BusUtils
在编译时会根据 `@BusUtils.Bus` 注解生成一份记录 tag 和 方法签名的映射表,因为是在编译时完成的,这里我们通过反射来完成。
1 |
|
通过比较如下几点的测试来完成对比:
- 注册 10000 个订阅者,共执行 10 次取平均值
- 向 1 个订阅者发送 * 1000000 次,共执行 10 次取平均值
- 向 100 个订阅者发送 * 100000 次,共执行 10 次取平均值
- 注销 10000 个订阅者,共执行 10 次取平均值
测试机器如下所示:
1 | macOS: 2.2GHz Intel Core i7 16GB |
在 Android 上,我们加入 EventBus
的注解处理器来提升 EventBus
效率,让其在最优情况下和 BusUtils
比较。
接下来,我们把测试的模板代码写好,方便后续可以直接把两者比较的代码往回调中塞入即可,具体代码如下所示:
1 | /** |
下面就让我们来一一对比测试。
注册 10000 个订阅者,共执行 10 次取平均值
1 | /** |
向 1 个订阅者发送 * 1000000 次,共执行 10 次取平均值
1 | /** |
向 100 个订阅者发送 * 100000 次,共执行 10 次取平均值
1 | /** |
注销 10000 个订阅者,共执行 10 次取平均值
1 | /** |
结论
为了方便观察,我们生成一份图表来比较两者之间的性能:
图表中分别对四个函数在 MacOS 和 OnePlus6 中的表现进行统计,每个函数中从左向右分别是 「MacOS 的 BusUtils」、「MacOS 的 EventBus」、「OnePlus6 的 BusUtils」、「OnePlus6 的 EventBus」,可以发现,BusUtils 在注册和注销上基本比 EventBus
要快上好几倍,BusUtils 在向少量订阅者发送多次事件比 EventBus
也快上好多,在向多个订阅者发送多次事件也比 EventBus
快上些许。
基于以上说的这么多,如果你项目中事件总线用得比较频繁,那么可以试着用我的 BusUtils
来替代 EventBus
来提升性能,或者在新的项目中,你也可以直接使用性能更好的 BusUtils
。
下面来总结下 BusUtils
的优点:
BusUtils
是通过事件Tag
来确定唯一事件的,所以接收函数支持无参或者一个参数,而EventBus
只能通过 MessageEvent 来确定具体的接收者,只能接收一个参数,即便仅仅是通知,也需要定义一个 MessageEvent,所以,BusUtils
传参更灵活。BusUtils
在应用到项目中后,编译后便会在 application 中生成__bus__.json
事件列表,如上生成的事件列表如下所示:
1 | { |
修改 oneParamFun
为两个参数的话,为了确保项目不会因为 BusUtils
在运行时崩溃,api
插件会使其在编译时就不过,此时 __bus__.json
文件如下所示,提示你参数个数不对:
1 | { |
同理,如果两个 bus 的 (2.1 版本已支持 Tag 一对多及事件优先级)Tag
相同了,也会编译不过,提示你项目中存在 Tag
相同的 bus。
所以,BusUtils
比 EventBus
更友好。
BusUtils
比EventBus
代码少得太多,BusUtils
的源码只有区区 300 行,而EventBus
3000 行肯定是不止的哈。BusUtils
比EventBus
性能更好。
原理
bus 插件原理分析
bus 插件的源码在这里:bus 插件源码传送门,该插件通过 Gradle 的 transform 来完成对 BusUtils.init()
做注入,下面来一步步分析:
不明白 transform 的可以先去了解下,简单来说 transform 就是专门用来做字节码插入操作的,最常见的就是 AOP(面向切面编程),这部分我就不科普了,有兴趣的可以自己搜索了解。
说到字节码操作,那就又有知识点了,想要上手快速简单的可以使用 javassist
,不过,我选择了更强大快速的 ASM
,这里我就不详细介绍了,有兴趣的可以自己去学习,ASM
其实也很简单的,在 ASM Bytecode Outline 这个插件帮助下写得还是很快的。
通过 ASM 扫描出所有带有 `@BusUtils.Bus` 注解的函数,读取并保存注解的值和函数的参数信息,相关代码如下所示:
1 |
|
然后往 BusUtils.init()
插入扫描出来的内容,比如上面提到的 oneParamFun
这个函数,那么其最终插入的代码如下所示:
1 |
|
其 ASM 插入的代码如下所示:
1 |
|
BusUtils 原理分析
接下来看下 BusUtils.registerBus
的实现:
1 | private void registerBus(String tag, |
很简单,就是往 mTag_BusInfoMap
中插入了 key 为 tag
,value 为 BusInfo
的一个实例,这样便把一个事件保留了下来。
接下来就是使用了,一开始我们都是先 register,源码如下所示:
1 | public static void register(final Object bus) { |
我们获取 bus 的类名,然后对 mClassName_BusesMap
加锁来把它插入到 mClassName_BusesMap
的 value 的集合中,可以看到我们用了线程安全的 CopyOnWriteArraySet
集合,然后还需要处理下之前是否订阅过粘性事件 processSticky
,到这里 register 便结束了。
然后就是 post
来发送事件了,源码如下:
1 | public static void post(final String tag) { |
可以看到代码还是比较多的,不过别急,我们一步步来还是很简单的,首先在我们之前注入的 mTag_BusInfoMap
中查找是否有该 tag
的 BusInfo
,没有的话就输出错误日志直接返回。
然后我们根据获取到的 BusInfo
来找到 method
实例,BusInfo
第一次会把 method
保存在实例中,之后调用的话直接从实例中取出 method
即可。
接着我们从 BusInfo
中取出线程信息,最后在线程中执行 method
的反射,大体就是这样,具体细节的话还是需要自己分析源码。
最后就是 unregister
了:
1 | public static void unregister(final Object bus) { |
unregister
和 register
相反,就是从 mClassName_BusesMap
的 value 集合中移除,同样需要对 mClassName_BusesMap
加锁哦。