用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上查看,代码量很少,可以直接复制使用,感谢您的阅读。

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

推荐阅读更多精彩内容