美文网首页Android开发Android开发
为什么Google要将LiveData设计成粘性的

为什么Google要将LiveData设计成粘性的

作者: 丘卡皮 | 来源:发表于2022-05-19 21:59 被阅读0次

    来源:https://juejin.cn/post/7098224450117566478

    前言

    相信很多人在职业生涯的面试过程中都被问过一个问题?

    面试官:我看你简历上有写 LiveData,那你能说说 LiveData 是粘性的吗?

    这确实是一个值得深入思考的知识点,今天就让我们站在Google设计者的角度来深入学习一下LiveData。

    LiveData是粘性的吗?

    关于这个问题,我们首先应该知道,粘性是什么意思?

    不知道你对EventBus熟不熟悉,我第一次接触粘性这个概念,就来自于EventBus的粘性事件

    粘性事件:相对比普通事件,粘性事件支持先发送事件,再去注册订阅者。一旦完成订阅动作,这个订阅者就会接收到该粘性事件。

    所以粘性其实就可以理解为观察者模式的升级,让观察者与被观察者对象之间更加的粘合。正常情况下,我们需要先注册观察者对象,然后再去更改被观察者对象,这样观察者对象才能接收到这个观察事件。而粘性,则支持先去触发更改被观察者对象,产生观察事件,然后再去注册观察者对象,注册成功后,该观察者对象还是可以接收到该观察事件,执行对应的观察动作。

    来个例子吧!看看是不是粘性的
    我们利用LiveData来做APP的全局状态管理。

    object GlobalState {
    
        val jcTestNumberLd: MutableLiveData<Int> = MutableLiveData<Int>()
    
    }
    

    然后在Fragment以及Activity中观察该jcTestNumberLd。

    /** 观察 GlobalState 的 Activity */
    class JcTestActivity : BaseVmDbActivity<MainViewModel, ActivityJcTestBinding>() {
    
        override fun initView() {
            viewBinding.incButton.setOnClickListener {
                GlobalState.jcTestNumberLd.value =
                    if (GlobalState.jcTestNumberLd.value == null) {
                        1
                    } else {
                        GlobalState.jcTestNumberLd.value!!.toInt().inc()
                    }
            }
        }
    
        override fun initObserve() {
            GlobalState.jcTestNumberLd.observe(this, {
                Log.e(TAG, "initObserve: jctestNumber = $it")
            })
    
        }
    
        ........
    }
    
    /** 观察 GlobalState 的 Fragment */
    class EventFragment : BaseVmDbFragment<EventViewModel, FragmentEventBinding>() {
    
        override fun setObservers() {
            GlobalState.jcTestNumberLd.observe(viewLifecycleOwner, {
                Log.e(TAG, "setObservers: jctestNumber = $it", )
            })
        }
    
        ........
    }
    

    注意:这里例子中的 EventFragment 并不是关联到 JcTestActivity 的。用户会先进入到 JcTestActivity,然后由用户控制进入到另一个Activity中,加载EventFragment。

    我们来执行一下以下五步操作,来看一下输出的日志。

    当我们第一次进入 JcTestActivity 时,注册了观察者,没有接收到观察事件,所以也就不会执行观察动作。
    然后我们点击自增按钮为jcTestNumberLd 赋予新值,接收到观察事件,执行观察动作,输出 1。
    再次点击自增按钮,有观察事件,执行观察动作,输出 2。
    再次点击自增按钮,有观察事件,执行观察动作,输出 3。
    然后我们到 EventFragment 中,注册新的观察者,发现直接接收到观察事件,执行观察动作,输出 3。
    输出结果:

    E/JcTestActivity: initObserve: jctestNumber = 1
    E/JcTestActivity: initObserve: jctestNumber = 2
    E/JcTestActivity: initObserve: jctestNumber = 3
    
    E/EventFragment: setObservers: jctestNumber = 3
    

    这就是粘性事件!所以说,LiveData是粘性的。

    LiveData 是怎么实现粘性的呢?

    在知道LiveData是粘性后,我不经问自己:它是怎么实现粘性的呢?

    这里我们先来回顾一下EventBus粘性事件的实现原理。

    EventBus在发送粘性事件时,会将这粘性事件存到一个叫做stickyEvents的集合中,然后等注册订阅新的观察者对象时,会去遍历该集合中的粘性事件,如果有找到对应的粘性事件,就将该粘性事件发送给该观察者。(如果你对EventBus粘性事件不熟悉,可以点击EventBus 源码解析(很细 很长)进一步了解学习。)

    那LiveData是不是也是以同样的原理来实现粘性的呢?

    public LiveData(T value) {
        mData = value;
        mVersion = START_VERSION + 1;
    }
    
    /**
     * Creates a LiveData with no value assigned to it.
     */
    public LiveData() {
        mData = NOT_SET;
        mVersion = START_VERSION;
    }
    

    从 LiveData 的构造函数中可以发现有一个 mVersion 参数,它代表着 LiveData 的版本号,每当我们进行setValue时,都会让mVersion进行自增。

    另外,ObserverWrapper 这个观察者包装类中也有一个int mLastVersion = START_VERSION 版本号。

    这两个版本号分别是被观察者对象与观察者对象的版本号,那这二者之间又有什么关系呢?

    在判断是否通知观察者的 considerNotify(ObserverWrapper observer) 方法中,会对这两个版本号进行比较。

    private void considerNotify(ObserverWrapper observer) {
        ...省略代码...
    
        //如果观察者的版本号 >= LiveData的版本号,就说明该观察者已经接收过该观察事件,也就不再分发。
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        //反之,分发观察事件给该观察者,让其执行对应的观察动作,并更新观察者的版本号
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }
    

    概括一下:根据比对观察者对象的版本号与LiveData的版本号来判断是否分发当前版本的数据给该观察者。如果观察者对象的版本号大于等于LiveData的版本号,也就说明该观察者已经接收过当前版本的数据了,也就不需要再次分发了(等待下一次数据更新)。反之,则分发当前版本的数据给该观察者,让其执行对应的观察动作,并更新观察者的版本号,也就是更新为LiveData的版本号。

    利用Hook验证一下
    我们利用 Hook 分别拿到 LiveData 的 mVersion 以及 ObserverWrapper 的 mLastVersion 来看一下。

    /** 主动 hook 检测版本号 */
    dataBinding.hookCheckVersionButton.setOnClickListener {
        GlobalState.jcTestNumberLd.hook()
    }
    
    fun LiveData<Int>.hook() {
        //get liveData mVersion
        val mVersion = this.javaClass.superclass.getDeclaredField("mVersion")
        mVersion.isAccessible = true
        val mVersionValue = mVersion.get(this)
        Log.e(TAG, "hook: LiveData mVersion = $mVersionValue")
    
        val mObservers = this.javaClass.superclass.getDeclaredField("mObservers")
        mObservers.isAccessible = true
        //SafeIterableMap<Observer<? super T>, ObserverWrapper>
        val mObserversValue = mObservers.get(this)
        Log.e(TAG, "hook: mObserversValue = $mObserversValue")
    
        val methodGet = mObserversValue.javaClass.getDeclaredMethod("get", Any::class.java)
        methodGet.isAccessible = true
        //myObserver就是自定义的Observer,即通过Observer这个key来拿到SafeIterableMap的值,
        //这里也就是LifecycleBoundObserver
        val objectWrapperEntry = methodGet.invoke(mObserversValue, myObserver)
        val objectWrapper = (objectWrapperEntry as Map.Entry<*, *>).value
        //ObserverWrapper mLastVersion
        val mLastVersion = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion")
        mLastVersion.isAccessible = true
        val mLastVersionValue = mLastVersion.get(objectWrapper)
        Log.e(TAG, "hook: observerWrapper mLastVersion = $mLastVersionValue")
    }
    
    val myObserver = Observer<Int> {
        Log.e(TAG, "initObserve: jctestNumber = $it")
        dataBinding.testNumberTv.text = it.toString()
    }
    

    接着,我们来分析一下上面所介绍的那五个步骤:

    1. 当我们第一次进入 JcTestActivity 时,注册了观察者,没有接收到观察事件,所以也就不会执行观察动作。Hook一下,此时 LiveData mVersionValue = -1,observerWrapper mLastVersionValue = -1 。
    2. 然后我们点击自增按钮为jcTestNumberLd 赋予新值,接收到观察事件,执行观察动作,输出 1。
      再次点击自增按钮,有观察事件,执行观察动作,输出 2。
    3. 再次点击自增按钮,有观察事件,执行观察动作,输出 3。Hook一下,此时 LiveData mVersionValue = 2,observerWrapper mLastVersionValue = 2 。
    4. 然后我们退出重新进到 JcTestActivity,注册新的观察者,发现直接接收到观察事件,执行观察动作,输出 3。Hook一下,此时 LiveData mVersionValue = 2,observerWrapper mLastVersionValue = -1 。

    输出的 Log信息 如下:

    //第一次进入JcTestActivity
    E/JcTestActivity: LiveData hook: mVersionValue = -1
    E/JcTestActivity: hook: observerWrapper mLastVersionValue = -1
    
    //执行两次Inc
    E/JcTestActivity: initObserve: jctestNumber = 1
    E/JcTestActivity: initObserve: jctestNumber = 2
    E/JcTestActivity: initObserve: jctestNumber = 3
    E/JcTestActivity: LiveData hook: mVersionValue = 2
    E/JcTestActivity: hook: observerWrapper mLastVersionValue = 2
    
    //退出JcTestActivity后,重新进入JcTestActivity
    E/JcTestActivity: initObserve: jctestNumber = 3
    E/JcTestActivity: LiveData hook: mVersionValue = 2
    E/JcTestActivity: hook: observerWrapper mLastVersionValue = -1
    

    这里最后一步重新进入了JcTestActivity 拿到 observerWrapper mLastVersionValue = -1 ,是因为重新进入后,这个观察者是新创建的,其mLastVersionValue 初始值就是-1。

    新观察者创建成功后,触发considerNotify()方法,进行版本号对比,此时,LiveData.mVersion -> 2 大于 observer.mLastVersion -> -1,所以LiveData会将最新数据分发给当前观察者。

    通过 Hook 分别拿到 LiveData 的 mVersion 以及 ObserverWrapper 的 mLastVersion 来进行对比,进一步证实了LiveData是粘性的。

    为什么Google要将LiveData设计成粘性的

    LiveData 是可观察的数据存储器类,这样也就意味着存储在LiveData中的数据是会更新的,既然是会更新的,那必定就会存在状态,即最新数据状态。

    所以,当数据状态发生改变时(数据发生了更新),LiveData需要告诉所有处于活跃状态的观察者, 让其同步更新数据。这应该很好理解了,因为这就是普通事件,先注册观察者,再去更新被观察者对象,触发观察事件。

    那这时,你再去新注册一个观察者对象,你认为它需不需要知道此时LiveData最新的数据呢?

    答案是:需要。

    因为所有的观察者,都只需要知道LiveData中存储的数据,而且是最新数据。不管我是新注册的观察者,只要你LiveData有了最新数据,就需要告诉我。而关于有无新数据,从代码上体现出来的就是,LiveData.mVersion > Observer.mLastVersion 。

    这也就是粘性事件,先更新被观察者对象,触发观察事件,再去注册观察者,观察者会直接接收到该观察事件,执行对应的观察动作。

    它的功能属性导致其只能是粘性的。

    总结

    本篇文章,我们先是通过EventBus的粘性事件来了解了什么是粘性?通过案例来初步推出LiveData是粘性的,接着通过探索源码来发现LiveData实现粘性的原理,并通过反射hook,来进一步证实,最后站在Google设计者的角度来思考为什么要将LiveData设计成粘性的。相信你通过本篇文章,肯定对LiveData有了进一步的了解。

    到此本篇文章就结束啦,如果你有任何疑问或者不同的想法,欢迎在评论区留言与我一起探讨。

    相关文章

      网友评论

        本文标题:为什么Google要将LiveData设计成粘性的

        本文链接:https://www.haomeiwen.com/subject/uhlxprtx.html