用LiveData实现一个事件总线

在通信总线类框架中,EventBus因其简洁的使用方式和解耦能力受到广大开发者的喜爱并在之后衍生除了诸如RxBus等基于观察者模式的框架写的通信库。对于LiveData的使用者来说,我们也可以使用LiveData来实现一个事件总线,因为它们都依托于观察者模式。

为什么要使用LiveData

LiveData是Android Architecture Components提出的框架。LiveData是一个可以被观察的数据持有类,它可以感知并遵循Activity、Fragment或Service等组件的生命周期。正是由于LiveData对组件生命周期可感知特点,因此可以做到仅在组件处于生命周期的激活状态时才更新UI数据。
LiveData的优点:

  • 确保UI符合数据状态
    LiveData遵循观察者模式。 当生命周期状态改变时,LiveData会向Observer发出通知。 您可以把更新UI的代码合并在这些Observer对象中。不必去考虑导致数据变化的各个时机,每次数据有变化,Observer都会去更新UI。
  • 没有内存泄漏
    Observer会绑定具有生命周期的对象,并在这个绑定的对象被销毁后自行清理。
  • 不会因停止Activity而发生崩溃
    如果Observer的生命周期处于非活跃状态,例如在后退堆栈中的Activity,就不会收到任何LiveData事件的通知。
  • 不需要手动处理生命周期
    UI组件只需要去观察相关数据,不需要手动去停止或恢复观察。LiveData会进行自动管理这些事情,因为在观察时,它会感知到相应组件的生命周期变化。
  • 始终保持最新的数据
    如果一个对象的生命周期变到非活跃状态,它将在再次变为活跃状态时接收最新的数据。 例如,后台Activity在返回到前台后立即收到最新数据。
  • 正确应对配置更改
    如果一个Activity或Fragment由于配置更改(如设备旋转)而重新创建,它会立即收到最新的可用数据。
  • 共享资源
    您可以使用单例模式扩展LiveData对象并包装成系统服务,以便在应用程序中进行共享。LiveData对象一旦连接到系统服务,任何需要该资源的Observer都只需观察这个LiveData对象。

实现思路

上面我们有说到,将LiveData包装成一个共享的资源对象,当这个对象内容发生改变时,能对所有观察者发送消息并执行相应的动作,所以我们需要一个全局的LiveData作为总线(Bus),但是因为LiveData没有过滤事件的方法,我们无法只对特定事件的观察者发送消息,所以将Bus类型定义为Map,不同的事件对应于不同的LiveData。先看初版代码

class LiveBus private constructor() {

  private val busMap by lazy { ConcurrentHashMap<Class<*>, MutableLiveData<*>>() }

  private fun <T> bus(clazz: Class<T>) = busMap.getOrPut(clazz) { MutableLiveData<T>() }

  fun <T> with(clazz: Class<T>) = bus(clazz) as MutableLiveData<T>

  companion object {
      @Volatile
      private var instance: LiveBus? = null

      @JvmStatic
      fun getInstance() = instance ?: synchronized(this) {
          instance ?: LiveBus().also { instance = it }
      }
   }
}

不到20行的代码,就可以实现一个功能简单的事件总线,使用起来也是十分简洁方便。

发送事件:

LiveBus.getInstance().with(String::class.java).setValue("str")

订阅事件:

LiveBus.getInstance().with(String::class.java).observe(this, Observer { // TODO })

只需要通过with方法传入一个事件的类型,便可以获取到对应的LiveData,之后的操作就跟平时使用没有区别了。

存在的问题

测试中发现,在订阅一个事件之后,订阅者会收到订阅之前的消息,也就是说,LiveData默认是个粘性消息的实现,这不是我们需要的,那要如何处理呢?我们先来看看LiveData的实现。LiveData的数据回调关键方法是dispatchingValue(ObserverWrapper initiator)

if (mDispatchingValue) {
           // ***
        do {
            mDispatchInvalidated = false;
            // 调用observe方法后,会进入到if代码块
            if (initiator != null) {
                considerNotify(initiator);
                initiator = null;
            } else {
                // ***
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

可以看到,调用observe方法后,最终会进入到dispatchingValue的do-while语句,然后调用considerNotify(initiator)

private void considerNotify(ObserverWrapper observer) {
       // ***
      // 当observer的mLastVersion小于了LiveData的mVersion时,会执行一次回调,然后将mVersion赋值给mLastVersion
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion
        observer.mObserver.onChanged((T) mData);
    }

mVersion是LiveData内部的一个标志位,初始值为-1,每次调用setValue(T value)时值会自增1(postValue最终也会调用setValue)

static final int START_VERSION = -1;
private int mVersion = START_VERSION;

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
   }

而observer内部也有一个初始值为-1的标志位mLastVersion,当有消息需要分发时,会通过判断两个标志位来决定是否需要回调事件。所以,解决的方法很简单,在observe的时候,强行将mLastVersion跟mVersion置位相等,就能避免第一次的回调了,但是我们无法直接操作ObserverWrapper的mLatVersion,所以我们通过继承LiveData,按照源码的逻辑写一个包装类就行了

    class BusLiveData<T> : MutableLiveData<T>() {

        var mVersion = START_VERSION

        override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
            // 将observer替换为我们的包装类
            super.observe(owner, ObserverWrapper(observer, this))
        }

        override fun setValue(value: T) {
            mVersion++
            super.setValue(value)
        }

        override fun postValue(value: T) {
            mVersion++
            super.postValue(value)
        }

        companion object {
            const val START_VERSION = -1
        }
    }

    private class ObserverWrapper<T>(val observer: Observer<T>, val liveData: BusLiveData<T>) : Observer<T> {

        // 通过标志位过滤旧数据
        private var mLastVersion = liveData.mVersion

        override fun onChanged(t: T?) {
            if (mLastVersion >= liveData.mVersion) {
                return
            }
            mLastVersion = liveData.mVersion

            observer.onChanged(t)
        }
    }

这样就解决了旧数据的问题。

粘性事件

Sticky Event,中文名为粘性事件。普通事件是先注册,然后发送事件才能收到;而粘性事件,在发送事件之后再订阅该事件也能收到。此外,粘性事件会保存在内存中,每次进入都会去内存中查找获取最新的粘性事件,除非你手动解除注册。

前面说过,LiveData是默认支持粘性事件的,但这个粘性事件是无差别分发,并不是我们想要的。我们需要的粘性事件需满足以下条件:

  • 通过特定方法,如setValueSticky(value)发送粘性事件,所有已订阅的订阅者都能收到该事件。
  • 订阅者根据自身需求设置是否接收粘性事件,如果接收,首次订阅后会接收到最后一次发送的粘性事件,而不会收到普通事件。
  • 粘性事件支持删除,防止重复接收。

先对BusLiveData做修改,增加一个用于保存最新粘性事件的变量,然后增加相应方法

        var mStickyEvent: T? = null

        fun setValueSticky(value: T) {
            mStickyEvent = value
            setValue(value)
        }

        fun postValueSticky(value: T) {
            mStickyEvent = value
            postValue(value)
        }

        fun removeSticky() {
            mStickyEvent = null
        }

然后在ObserverWrapper里原本拦截粘性事件的位置增加相应的判断

private class ObserverWrapper<T>(val observer: Observer<T>, val liveData: BusLiveData<T>, val sticky: Boolean) : Observer<T> {

        // 通过标志位过滤旧数据
        private var mLastVersion = liveData.mVersion

        override fun onChanged(t: T?) {

            if (mLastVersion >= liveData.mVersion) {
                // 回调粘性事件
                if (sticky && liveData.mStickyEvent != null) {
                    observer.onChanged(liveData.mStickyEvent)
                }
                return
            }
            mLastVersion = liveData.mVersion

            observer.onChanged(t)
        }
    }

当我们要订阅黏性事件时,只需要调用

fun observe(owner: LifecycleOwner, observer: Observer<T>, sticky: Boolean) {
            super.observe(owner, ObserverWrapper(observer, this, sticky))
        }

将sticky标志位赋值为true就行了,这样一个事件总线就完成了。


最终效果

完整代码可到我的github上查看,代码量很少,可以直接复制使用,感谢您的阅读。

推荐阅读更多精彩内容