近日,据国外媒体报道,电动汽车厂商特斯拉的股价本周一再次大涨,延续上周4个交易日连续大涨的势头,CEO埃隆·马斯克的身价也因此而增至463亿美元,比马云和拼多多创始人黄峥均高出了31亿美元。
/ 前言 /
EventBus大家都很熟悉了,各种实现方式也是层出不穷,然而,作为有追求的程序员们,永远在不停的造轮子,毕竟,在程序员的眼中,至今,没有哪个轮子看上去是完美无暇的。
因此,作为有追求的程序员中的一员,我也想假装很权威的站出来,然后无所畏惧的从远古时期讲讲事件总线的来龙去脉。有兴趣的小伙伴可以搬个椅子听我白话一番。也许可以给你带来不一样的视野。
/ 什么是EventBus /
1) 要搞清楚什么是事件总线,我们先了解一下什么叫总线:
总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。在计算机系统中,各个部件之间传送信息的公共通路叫总线,微型计算机是以总线结构来连接各个功能部件的。
其实总线一词来源于计算机组成原理,计算机总线是一组能为多个部件分时共享的信息传送线,用来连接多个部件并为之提供信息交换通路。总线不仅是一组信号线,从广义上讲,总线是一组传送线路及相关的总线协议。
总而言之,在计算机中用于连接各种功能部件并在它们之间传送数据的公用线路或通路,我们称之为总线。用现实生活打比方,如果把人当作需要传输的数据,总线就和日常生活中的公交车一样,公交车翻译成Bus,看来很符合实际啊。
2) 在总线上传输的数据分很多种,根据用途可以分为以下几种:
数据总线(Data Bus):在CPU与RAM之间来回传送需要处理或是需要储存的数据。
地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中储存的数据的地址。
控制总线(Control Bus):将微处理器控制单元(Control Unit)的信号,传送到周边设备。
扩展总线(Expansion Bus):外部设备和计算机主机进行数据通信的总线,例如ISA总线,PCI总线。
局部总线(Local Bus):取代更高速数据传输的扩展总线。
其实上面的总线都是实际物理存在的概念,而事件本身也是信息的一种形式,为了理解方便,在软件开发领域,大家便把,在多个模块间,为事件提供传输通道的组件或者说库,称之为事件总线,也可以称之为消息总线。
/ 为什么要使用事件总线 /
搞清楚事件总线的定义之后,我们来分析一下,为什么我们需要使用事件总线:
在软件开发中,为了软件前期开发的高效,中期合作的顺畅,后期维护的便利,架构师们会施展浑身解数,比如运用面向对象思想,面向对象六大原则(单一、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、开闭原则),以及传说中的23种设计模式,等花里胡哨的技术,尽量把软件架构设计得看上去很完美,于是产生了处理各种逻辑的模块,比如我们熟知的界面层、数据逻辑层等,各层内部也存在处理不同逻辑的组件,面对这些完美的架构设计,这么多的模块和组件为了相互之间的交互,必定会涉及到数据传递。
但是有人会说了,在没有总线的情况下,我们也是可以进行数据传递的,比如参数传递,事件回调等。我们知道,事件总线涉及三个要素:事件源、订阅者、通道。
当一个模块处理逻辑时会产生事件,我们称这个模块为事件源。
有的模块等待事件的产生,然后去执行特定的任务,我们称这个模块为订阅者。
维护这些事件源和订阅者的关系的组件我们称之为通道,也就是总线。
想象一下,如果没有一个通道管理,事件源并不想知道谁会关心自己产生的事件,订阅者也不想操心是谁产生的事件,因为同样一个事件可能有N个模块会产生,也可能会有M个模块会订阅,但每个模块都只希望聚焦于自己的逻辑,如果要通过回调或者其他方式去管理,对谁来说都是一个灾难。因此,引入一个和平使者第三方:总线,是面向对象原则的使用,也是很好的一种设计,可以做到更好的解藕。
可以看到,如上图所示,无论是作为发布事件的组件,还是订阅事件的组件,在总线的帮助下,都可以很好的进行解藕。
插播说明:我们发现发布-订阅模式包含生产-消费者模式,也包含观察-被观察者模式,其中还有中介者模式,其实,我们常说的23种设计模式,大部分时候并不是单独存在的,那只是最小设计模式单元,为了便于理解,以上图为参考画出如下两种模式,对比一下你就明白了,这些模式之间的微妙关系。
/ EventBus常见实现方式 /
结合上一章节对事件总线的了解,我们知道,事件总线必不可缺的元素就是事件,另一个必备点就是,当有新的事件时可以通知到关心的模块,不然其他模块总要隔三差五的来询问有没有事件,那样效率也太低了吧。
所以事件总线的订阅和发布是逃不了了,观察者模式也是板上钉钉的了,为了后面更顺畅,在说明常见事件总线实现方式之前,我们还得补充一个小知识:(觉得长的这块也可以略过)
要实现事件总线,就涉及到事件(信息)的流转分发,因此,在此之前我们还应该聊一聊信息流转方式,而信息流转又分为进程内流转和进程间流转。
进程内流转又分为单线程和多线程。
单线程数据流转其实就是参数传递,值传递或者引用传递。如函数调用,以及注册回调函数。多线程之间数据流转,其实就是多个线程在操作同一个资源,但是操作的的动作不同。如,一个线程对资源进行写的操作,一个线程对资源进行读的操作。因为数据在同一个进程中都可以访问(不考虑私域变量),这个时候考虑的不是数据的流转,而是同步和互斥问题。
进程间流转也就是我们常说的进程间通信(IPC,InterProcess Communication)方式。而IPC方式一般有如下方式:说明:可以用于进程间通信的方式,同一个进程内自然也是可以,只是杀鸡焉用牛刀。
管道( pipe )
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (namedpipe)
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量(semophore)
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列(messagequeue)
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 (signal)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory )
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字(socket)
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
Binder
Android优化的跨进程通信方式,更高效的数据传输方式。AIDL、BroadCast、AMS等本质都用到了Binder。
了解了信息的流转方式后,我们再来看看常见的事件总线是怎么实现的吧!
1、直接使用注册回调方式实现最简单的观察者模式,由被观察者统一管理注册的观察者,事件发生时通知所有的观察者。
优点:简单直接,适合部分场景。
缺点:无法跨线程,更无法跨进程,容易造成内存泄露,要注意注册(register)和解注册(unregister)配对使用,没有生命周期管理等。
2、使用系统提供的广播机制实现消息事件的订阅和发布。
优点:支持跨进程,适合多进程事件总线
缺点:如果使用非应用内广播(Local Broadcast),可能造成安全性问题,而且效率不高,建议少用或者不用,且要注意注册(register)和解注册(unregister)配对使用,没有生命周期管理等。
3、GreenRobot提供的EventBus库利用Handler机制,系统的Message等技术,实现的跨线程通信,是现在大家用得最多的一个事件总线,也是大家公认的EventBus。
优点:可继承、优先级、粘滞,使用简单
缺点:事件分发是通过注解函数的参数类型决定的,这就导致了当接受者过多或相同参数时很难理清消息流。
4、RxBus是基于RxJava实现的一个消息总线,其实网上有很多实现方式,因为核心还是依赖与RxJava,所以实现都大同小异,有兴趣可以自己去查查。
优点:如果不考虑RxJava的代码,RxBus代码简单,是搭配RxJava的天然利器。
缺点:性能依赖于RxJava,方法调用栈相对来说比较深。
5、LiveEventBus其实也算是依赖于其他库或者组件发展起来的一种消息总线,目前实现方式也比较杂,各有千秋。
优点:天然的生命周期感知能力,粘性事件,跨线程能力。原生LiveData的良好扩展。
缺点:为了解决粘性问题,有各种反射或者侵入性比较强的解决方式。
综上所见,可能还有其他方式实现事件总线,可以看到各有优缺点,其实也没有说谁是最好的实现,存在即合理,每种方式都有自己的生存空间,合适即合理,没有最好的,只有最合适的。
/ LiveData简介 /
上面巴拉巴拉了那么多,其实就是想从一个大局方向分析一下事件总线的来龙去脉,也为下面用LiveData实现事件总线做一个伏笔。
虽然市面上有很多事件总线的实现方式,但是基于简约原则,少即是多,越少变数越安全的理念,能不引入新的第三方时,我尽量使用原生实现。这个带来的好处有几个:
减小应用的体积。
减少引入的不稳定因素,第三方库随时有可能停止更新,随时可能被原生排斥。
杀鸡不用牛刀,用最合适的方式解决问题。
管理方便,统一总体架构。
深入思考,学会在现有环境解决现有问题。
那么,什么是LiveData呢?虽然很多人都很熟悉了,但是这里还是本着尽量不让一个人落下的原则,还是介绍一下:
LiveData是一种可观察的数据存储器类。与常规的可观察类不同,LiveData具有生命周期感知能力,意指它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的应用组件观察者。
生命周期感知能力,是不是听上去很高级,我是觉得很高级,像拥有超能力一样。那么这种超能能力又有什么优点呢?
确保界面符合数据状态
LiveData 遵循观察者模式。当生命周期状态发生变化时,LiveData 会通知 Observer 对象。您可以整合代码以在这些 Observer 对象中更新界面。观察者可以在每次发生更改时更新界面,而不是在每次应用数据发生更改时更新界面。
不会发生内存泄露
观察者会绑定到 Lifecycle对象,并在其关联的生命周期遭到销毁后进行自我清理。
不会因 Activity 停止而导致崩溃
如果观察者的生命周期处于非活跃状态(如返回栈中的 Activity),则它不会接收任何 LiveData 事件。
不再需要手动处理生命周期
界面组件只是观察相关数据,不会停止或恢复观察。LiveData 将自动管理所有这些操作,因为它在观察时可以感知相关的生命周期状态变化。
数据始终保持最新状态
如果生命周期变为非活跃状态,它会在再次变为活跃状态时接收最新的数据。例如,曾经在后台的 Activity 会在返回前台后立即接收最新的数据。
适当的配置更改
如果由于配置更改(如设备旋转)而重新创建了 Activity 或 Fragment,它会立即接收最新的可用数据。
共享资源
您可以使用单一实例模式扩展 LiveData对象以封装系统服务,以便在应用中共享它们。LiveData 对象连接到系统服务一次,然后需要相应资源的任何观察者只需观察 LiveData 对象。
看了上面这些描述,是不是和我一样觉得LiveData很高级?下面我们就开始把这高级的东西用起来吧!
/ 用LiveData实现简单的LiveEventBus /
话不多说,直接上代码:
import androidx.lifecycle.MutableLiveData; import java.util.HashMap; import java.util.Map; /** * Created by cody.yi. on 2020/6/9. * LiveEventBus */ public class LiveEventBus { public static <T> MutableLiveData<T> getDefault(String key, Class<T> clz) { return ready().with(key, clz); } private final Map<String, MutableLiveData<Object>> bus; private LiveEventBus() { bus = new HashMap<>(); } private static class InstanceHolder { static final LiveEventBus INSTANCE = new LiveEventBus(); } private static LiveEventBus ready() { return LiveEventBus.InstanceHolder.INSTANCE; } @SuppressWarnings("unchecked") private <T> MutableLiveData<T> with(String key, Class<T> clz) { if (!bus.containsKey(key)) { MutableLiveData<Object> liveData = new MutableLiveData<>(); bus.put(key, liveData); } return (MutableLiveData<T>) bus.get(key); } }
使用步骤
发送数据
LiveEventBus.getDefault("Event1",String.class).setValue("推送数据1"); 或者在非主线程时使用 LiveEventBus.getDefault("Event1",String.class).postValue("推送数据1");
监听数据
LiveEventBus.getDefault("Event1",String.class).observe(this, new Observer<String>() { @Override public void onChanged(final String event) { Toast.makeText(BusDemoActivity.this, "监听到数据 -> " + event, Toast.LENGTH_SHORT).show(); } });
简单两步实现了事件总线,而且使用简单,不需要手动注册反注册操作。LiveData具有的优势它都有,是不是很香?
/ 简单LiveEventBus包含的问题 /
其实简单需求,上面已经够用了,但是看了LiveData的源码就会发现,其实还有几个小瑕疵,在某些特殊情况下会有点问题,下面先逐一分析问题,然后统一给出最终的解决方案。
问题一、因为LiveData机制,默认所有消息都是粘性事件
也就是说,无论什么时候开始监听数据变化,之前发送的数据也会被观察者收到,这在有些场景其实是OK的,但是有些时候就有点不合逻辑了。比如我们做一个价格监听的事件,用户预定了一个商品,希望降价的时候提示用户,如果是粘性事件,就有可能之前的价格变化也会提示用户,这种时候粘性事件就是多余的。
另一种场景就很适合粘性事件,比如我们记录登录状态,或者用户信息,无论我何时进入新页面,都可以把最后一次用户信息变化回调给我,这样还可以一定程度上减少全局变量。
解决方案网上也有很多:
比如美团大佬用的反射方式,还有包侵入方式修改或者获取LiveData的包可见字段方式,还有使用字段记录是否粘性方式(以上各个方案的具体实现可以去参考各位大佬的文章,如有不同看法欢迎讨论)。
其实当初看了美团大佬的反射方式,虽然学到了很多,但是,对于反射,我不是很满意,但是也给大佬提了建议-传送门,好像大佬也没有时间改,本着程序员爱折腾的本性,我自己实现了一套更优雅的方式,当然,也是仁者见仁,智者见智了,至少我看来是优雅的。实现完后自己项目中用了很长时间,后来机缘巧合看了别人的分享,也做了一下对比,还是觉得自己的比较优雅,考虑原因有以下几点。
1、反射是官方不提倡的,因此也随时有可能无效,而且版本兼容也是问题,侵入性强。
2、包可见侵入也是不符合面向对象原则的,人家设置包可见就是不想别人去动里面的东西。不是我的我不动,暂且算是程序员的洁癖吧。
3、第三种方式是最近看到的,看了一下逻辑,明显强依赖用户的使用方式,很容易失效。
4、侵入性强的方式对混淆有特殊要求,很容易因为混淆配置问题出错。
问题二、post方式有可能丢失事件
其实看一下LiveData的源码就可以发现,postValue方法做了一个小优化,当数据变更太快时,会选择行的丢掉之前还没有来得急发送的数据,其实如果只是用于数据监听,这个优化其实是很好的设计,但是用在事件总线的时候就是一个小坑了。
private final Runnable mPostValueRunnable = new Runnable() { @Override public void run() { Object newValue; synchronized (mDataLock) { newValue = mPendingData; // 缓存里的值已经被发送,被发送了就回到未设置状态 mPendingData = NOT_SET; } setValue((T) newValue); } }; protected void postValue(T value) { boolean postTask; synchronized (mDataLock) { // 缓存里面是否还有值已被发送 postTask = mPendingData == NOT_SET; mPendingData = value; } if (!postTask) {//就是这块返回,导致并不是每个数据都会被执行 return; } ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); }
其实这个问题,我们可以自己把postValue重写一下就可以了。
问题三、关心事件发送是否在主线程
LiveData本身提供了两个方法来更新数据,setValue和postValue,在主线程的时候使用setValue,其他线程使用postValue,作为消息总线,其实可以统一封装成post方法,在方法里判断是在什么线程。
问题四、事件名称随意写,容易出错
GreenRobot的EventBus是使用类类型作为事件的区分,而我们这种使用字符串来指定事件名称其实很容易因为写错一个大小写,或者一个字母而导致收不到事件,因此,可以使用统一管理的方式或者去掉手工书写的方式,比如APT技术自动生成事件进行管理。
问题五、事件无法做到分组,统一打开或关闭事件总线
比如有老业务突然要下线,以前发送的事件都要取消,这个时候按照业务分组的消息就可以很灵活了,可以直接把相关业务的事件分组关闭,这样其他地方都不需要修改,什么时候需要打开了也可以打开进行操作,另一种情况,不同业务可能会有相同的消息,比如操作成功消息,大家都叫操作成功,如果按照业务来区分,也是一个不错的选择。
在我的设计里,我把不同业务场景或者分组叫(Group)。
问题六、无法实现跨进程
因为LiveData本身是单进程数据,不支持跨进程,考虑到跨进程场景比较少,我并没有做支持,如果业务需要,其实我们可以用上面说到的Android的跨进程方式做一个桥接,比如说AIDL,从而实现LiveData的跨进程通信。新的版本已经做了支持,使用了AIDL和messenger分别实现了一套。为了大家使用方便,项目也做了抽取,传送门:ElegantBus
/ 设计思路以及部分代码 /
1、通过包装类自己管理注册序号(源码通过版本管理),解决粘性问题。
2、实现自己的post方式,统一判断当前所在线程,调用合适的方式。
3、通过定义事件Event和范围EventGroup,给事件定义含义以及数据类型,然后使用APT技术自动生成需要使用的事件类。
4、提供统一调用方式,也提供简单测试方式,支持扩展。
5、进程间消息总线扩展,使用AIDL和Messenger两种实现方式。
事件值包裹类:
final class ValueWrapper<T> { // 每个被观察的事件数据都有一个序号,只有产生的事件数据在观察者加入之后才通知到观察者 // 即事件数据序号要大于观察者序号 final int sequence; @NonNull final T value; ValueWrapper(@NonNull T value, int sequence) { this.sequence = sequence; this.value = value; } }
观察者包裹类:
public abstract class ObserverWrapper<T> { // 每个观察者都记录自己序号,只有在进入观察状态之后产生的数据才通知到观察者 int sequence; Observer<ValueWrapper<T>> observer; }
LiveData包裹类:
/** * Created by xu.yi. on 2019/3/31. * 和lifecycle绑定的事件总线 * 每添加一个observer,LiveEventWrapper 的序列号增加1,并赋值给新加的observer, * 每次消息更新使用目前的序列号进行请求,持有更小的序列号才需要获取变更通知。 * <p> * 解决会收到注册前发送的消息更新问题 */ @SuppressWarnings("unused") public class LiveEventWrapper<T> { private int mSequence = 0; private final MutableLiveData<ValueWrapper<T>> mMutableLiveData; public LiveEventWrapper() { mMutableLiveData = new MutableLiveData<>(); } /** * 如果在多线程中调用,保留每一个值 * 无需关心调用线程,只要确保在相同进程中就可以 * * @param value 需要更新的值 */ public void post(@NonNull T value) { checkThread(() -> setValue(value)); } /** * 设置监听之前发送的消息不可以接收到 * 重写 observer 的函数 isSticky ,返回true,可以实现粘性事件 * * @param owner 生命周期拥有者 * @param observerWrapper 观察者包装类 */ public void observe(@NonNull LifecycleOwner owner, @NonNull ObserverWrapper<T> observerWrapper) { observerWrapper.sequence = observerWrapper.isSticky() ? -1 : mSequence++; checkThread(() -> mMutableLiveData.observe(owner, filterObserver(observerWrapper))); } /** * 是否是在主线程 * * @return 是主线程 */ private boolean isMainThread() { return Looper.getMainLooper().getThread() == Thread.currentThread(); } /** * 检查线程并执行不同的操作 * * @param runnable 可运行的一段代码 */ private void checkThread(Runnable runnable) { if (isMainThread()) { runnable.run(); } else { // 主线程中观察 BusFactory .ready() .getMainHandler() .post(runnable); } } }
从包装类中过滤出原始观察者:
/** * 从包装类中过滤出原始观察者 * * @param observerWrapper 包装类 * @return 原始观察者 */ @NonNull private Observer<ValueWrapper<T>> filterObserver(@NonNull final ObserverWrapper<T> observerWrapper) { if (observerWrapper.observer != null) { return observerWrapper.observer; } return observerWrapper.observer = valueWrapper -> { // 产生的事件序号要大于观察者序号才被通知事件变化 if (valueWrapper != null && valueWrapper.sequence > observerWrapper.sequence) { if (observerWrapper.uiThread()) { observerWrapper.onChanged(valueWrapper.value); } else { BusFactory .ready() .getExecutorService() .execute(() -> observerWrapper.onChanged(valueWrapper.value)); } } }; }
通过APT动态生成代码的部分,因为比较简单,对APT感兴趣的可以去看一下相关文章,也可以参考一下我的源码。
高端使用方式
1、 定义事件
//定义了事件范围为demo,激活 @EventGroup(value = "TestScope", active = true) public class EventDefine { @Event(description = "eventInt 事件测试", multiProcess = false, active = true) Integer eventInt; @Event(description = "eventString 事件测试", multiProcess = true, active = true) String eventString; @Event(description = "eventBean 事件测试", multiProcess = true, active = true) JavaBean eventBean; }
说明
其实事件定义只用到两个注解:
@EventGroup 使用在 class 上,定义事件分组名,是否激活
@Event 使用在变量上,定义具体 事件描述,是否激活,是否支持多进程
定义完注解后,通过前面导入的注解处理器 annotationProcessor ,ElegantBus 会自动生成以 EventGroup 定义的分组名的事件总线 例如上面的定义就会生成一个 TestScopeBus。然后我们所有地方就可以直接使用这个事件总线进行事件管理。
2、发送事件和监听事件:
TestScopeBus.eventInt().post(888); TestScopeBus.eventString().post("新字符串"); TestScopeBus.eventBean().post(new JavaBean());
TestScopeBus.eventInt().observe(owner, new ObserverWrapper<Integer>() { @Override public void onChanged(final Integer value) { ... } });