• 解决LiveData数据倒灌的新思路


    数据倒灌现象

    对于LiveData“数据倒灌”的问题,我相信很多人已经都了解了,这里提一下。所谓的“数据倒灌”:其实是类似粘性广播那样,当新的观察者开始注册观察时,会把上次发的最后一次的历史数据传递给当前注册的观察者

    比如在在下面的例子代码中:

    val testViewModel = ViewModelProvider(this)[TestViewModel::class.java]
    testViewModel.updateData("第一次发送数据")
    testViewModel.testLiveData.observe(this,object :Observer{
        override fun onChanged(value: String) {
            println("==============$value")
        }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    updateData方法发送了一次数据,当下面调用LiveData的observe方法时,会立即打印==============第一次发送数据,这就是上面说的“数据倒灌”现象。

    发生原因

    原因其实也很简单,其实就是 LiveData内部有一个mVersion字段,记录版本,其初始的 mVersion 是-1,当我们调用了其 setValue 或者 postValue,其 mVersion+1;对于每一个观察者的封装 ObserverWrapper,其初始 mLastVersion 也为-1,也就是说,每一个新注册的观察者,其 mLastVersion 为-1;当 LiveData 设置这个 ObserverWrapper 的时候,如果 LiveDatamVersion 大于 ObserverWrappermLastVersionLiveData 就会强制把当前 value 推送给 Observer

    也就是下面这段代码

        private void considerNotify(ObserverWrapper observer) {
            if (!observer.mActive) {
                return;
            }
    
            if (!observer.shouldBeActive()) {
                observer.activeStateChanged(false);
                return;
            }
            // 判断observer的版本是否大于LiveData的版本mVersion
            if (observer.mLastVersion >= mVersion) {
                return;
            }
            observer.mLastVersion = mVersion;
            observer.mObserver.onChanged((T) mData);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    所以要解决这个问题,思路上有两种方式:

    • 通过改变每个ObserverWrapper的版本号的值
    • 通过某种方式,保证第一次分发不响应

    解决方法

    目前网络上可以看到有三种解决方式

    每次只响应一次

    public class SingleLiveData extends MutableLiveData {
        private final AtomicBoolean mPending = new AtomicBoolean(false);
    
        public SingleLiveData() {
        }
    
        public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
            super.observe(owner, (t) -> {
                if (this.mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
    
            });
        }
    
        @MainThread
        public void setValue(@Nullable T t) {
            this.mPending.set(true);
            super.setValue(t);
        }
    
        @MainThread
        public void call() {
            this.setValue((Object)null);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这个方法能解决历史数据往回发的问题,但是对于多Observe监听就不行了,只能单个监听,如果是多个监听,只有一个能正常收到,其他的就无法正常工作

    反射

    这种方式就是每次注册观察者时,通过反射获取LiveData的版本号,然后又通过反射修改当前Observer的版本号值。这种方式的优点是:

    • 能够多 Observer 监听
    • 解决粘性问题

    但是也有缺点:

    • 每次注册 observer 的时候,都需要反射更新版本,耗时有性能问题

    UnPeekLiveData

    public class SingleLiveData extends MutableLiveData {
        private final AtomicBoolean mPending = new AtomicBoolean(false);
    
        public SingleLiveData() {
        }
    
        public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
            super.observe(owner, (t) -> {
                if (this.mPending.compareAndSet(true, false)) {
                    observer.onChanged(t);
                }
    
            });
        }
    
        @MainThread
        public void setValue(@Nullable T t) {
            this.mPending.set(true);
            super.setValue(t);
        }
    
        @MainThread
        public void call() {
            this.setValue((Object)null);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这个其实就是上面 SingleLiveData 的升级版,SingleLiveData 是用一个变量控制所有的 Observer,而上面采用的每个 Observer 都采用一个控制标识进行控制。 每次 setValue 的时候,就打开所有 Observer 的开关,表示可以接受分发。分发后,关闭当前执行的 Observer 开关,即不能对其第二次执行了,除非你重新 setValue。 这种方式基本上是比价完美了,除了内部多一个用HashMap存放每个Observer的标识,如果Observer比较多的话,会有一定的内存消耗。

    新的思路

    我们先看下LiveData获取版本号方法:

    int getVersion() {
        return mVersion;
    }
    
    • 1
    • 2
    • 3

    这个方法是一个包访问权限的方法,如果我新建一个和LiveData同包名的类,是不是就可以不需要反射就能获取这个值呢?其实这是可行的

    // 跟LiveData同包名
    package androidx.lifecycle
    
    open class SafeLiveData : MutableLiveData() {
    
        override fun observe(owner: LifecycleOwner, observer: Observer) {
            // 直接可以通过this.version获取到版本号
            val pictorialObserver = PictorialObserver(observer, this.version > START_VERSION)
            super.observe(owner, pictorialObserver)
        }
    
        class PictorialObserver(private val realObserver: Observer, private var preventDispatch: Boolean = false) :
            Observer {
    
            override fun onChanged(value: T) {
                // 如果版本有差异,第一次不处理
                if (preventDispatch) {
                    preventDispatch = false
                    return
                }
                realObserver.onChanged(value)
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这种取巧的方式的思路就是:

    • 利用同包名访问权限可以获取版本号,不需要通过反射获取
    • 判断LiveDataObserver是否有版本差异,如果有,第一次不响应,否则就响应

    我个人是偏向这种方式,也应用到了实际的开发中。这种方式的优点是:改动小,不需要反射,也不需要用HashMap存储等,缺点是:有一定的侵入性,假如后面这个方法的访问权限修改或者包名变动,就无效了,但是我认为这种可能性是比较小,毕竟androidx库迭代了这么多版本,算是比较稳定了。

    Android 学习笔录

    Android 性能优化篇:https://qr18.cn/FVlo89
    Android Framework底层原理篇:https://qr18.cn/AQpN4J
    Android 车载篇:https://qr18.cn/F05ZCM
    Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
    Android 音视频篇:https://qr18.cn/Ei3VPD
    Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
    OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
    Kotlin 篇:https://qr18.cn/CdjtAF
    Gradle 篇:https://qr18.cn/DzrmMB
    Flutter 篇:https://qr18.cn/DIvKma
    Android 八大知识体:https://qr18.cn/CyxarU
    Android 核心笔记:https://qr21.cn/CaZQLo
    Android 往年面试题锦:https://qr18.cn/CKV8OZ
    2023年最新Android 面试题集:https://qr18.cn/CgxrRy
    Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
    音视频面试题锦:https://qr18.cn/AcV6Ap

  • 相关阅读:
    结合RocketMQ 源码,带你了解并发编程的三大神器
    Elasticsearch HTTP查询
    Java之三大特性
    Flutter:getX的学习
    FPGA板卡启动以及LED灯带调试
    轻松实现H5页面下拉刷新:滑动触发、高度提示与数据刷新全攻略
    【react】react-redux 使用指南
    sql题目练习
    performance_schema
    PC辉光效果一切正常,安卓辉光却没效果、显示异常(爆闪、黑屏等)
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/133320396