【Jetpack】学穿:LiveData → ???(下)

简介: 在开始这篇文章前,我就遇到了第一个关于LiveData的问题:该怎么翻译这个词呢?

吼,就是给 源LiveData 添加了一个观察者,当它发生数据变化时更新 新LiveData 的值。写个简单例子:


// 源LiveData
val mUserLiveData = MutableLiveData<User>()
mUserLiveData.observe(this) {
    Log.e("Test", "mUserLiveData更新了:${it.javaClass.simpleName} --- $it") 
}
// 转换后的LiveData
val mUserNameLiveData = Transformations.map(mUserLiveData) { 
        user -> "${user.userName}-${user.code}" 
}
mUserNameLiveData.observe(this) {
    Log.e("Test", "mUserNameLiveData更新了:${it.javaClass.simpleName} --- $it") 
}
// 点击更新源LiveData数据
findViewById<Button>(R.id.bt_test).setOnClickListener {
    mUserLiveData.value = User("杰哥", (1..100).random())
}


运行后点击多次更新源LiveData数据,输出结果如下:


网络异常,图片无法展示
|


结论每次源LiveData数据发生变化,转换后的LiveData数据也跟着变化


Transformations.switchMap()


同样先看下switchMap()的具体实现:


网络异常,图片无法展示
|


留意传入的函数类型,它的返回值类型是 LiveData类型!!!然后在回调执行传入函数后,替换了源LiveData。怎么说?改下上面的简单例子:


val mUserNameLiveData = Transformations.switchMap(mUserLiveData) { user ->
    MutableLiveData<String>().apply { "${user.userName}-${user.code}"  }
}


运行后点击多次更新源LiveData数据,输出结果如下:


网络异常,图片无法展示
|


结论仅把源LiveData作为触发器,执行传入函数后返回新的LiveData,源LiveData数据发生变化不影响转换后的LiveData数据


到此,map()和switchMap()的区别就一清二楚了,还有一点要注意,转换方法都发生在 主线程。意味着如果在此进行耗时、太复杂的转换操作可能会 堵塞主线程,可以想办法将数据转换操作 异步化。如通过 CoroutineLiveData,用它得加下依赖:


implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'


包里有个 Transformations.kt,对原方法封装,定义成LiveData的扩展方法,便于链式调用:


网络异常,图片无法展示
|


CoroutineLiveData 中定义了liveData()这个顶层方法,用于构建 CoroutineLiveData


网络异常,图片无法展示
|


具体用法示例如下:


val mUserNameLiveData = mUserLiveData.switchMap() { user ->
    liveData(Dispatchers.Default){
        // 模拟耗时操作
        delay(5000)
        emit( "${user.userName}-${user.code}"  )
    }
}


⑥ 合并多个LiveData源


  • 场景:页面有多个数据源,单独用LiveData的话,每个都要定义一个observer,很繁琐。
  • 解法:可以利用 MediatorLiveData 来合并多个数据源,只需定义一个observer。


写个简单示例:


网络异常,图片无法展示
|


运行后点击多次按钮,输出结果如下:


网络异常,图片无法展示
|


细心的你有没有发现 mMediatorLiveData发生数据更新 这个日志没有输出?


因为它本身也是LiveData,有它自己的value,你得去修改它的值,才会触发数据更新回调~


合并数据源,非常适合存在多个 关联 网络请求或数据库查询的场景。


到此,关于LiveData的基本使用已经差不多了,和其它Jetpack组件的配合(ViewModel、Room等) 后续学到再讲。


0x3、LiveData 常见问题及解决


① 解决LiveData带来的粘性问题


上面说过粘性问题 → 添加新观察者,收到之前分发的值,原因如下:


添加观察者,引起生命周期组件状态变化,进行值分发,新观察者中值的版本号为-1,所以会触发值更新。


LiveData没有像EventBus一样,区分粘性和非粘性事件,也没有提供开关,在需要非粘性的场景,就得我们自己想办法了。


网上罗列的常见解决方式有三大类,一一讲解下~


方案一:反射干涉Version (无效)


思路如下


在observe()时,反射拿到LiveData的mVersion,然后赋值给 LifecycleBoundObserver 的 mLastVersion。


具体实现


class NoLiveData<T>(data: T) : MutableLiveData<T>(data) {
    // 传入粘性标记,区分粘性和非粘性
    fun observe(owner: LifecycleOwner, observer: Observer<in T>, isSticky: Boolean) {
        super.observe(owner, observer)
        if (!isSticky) hook(observer)
    }
    private fun hook(observer: Observer<in T>) {
        // 获取mVersion
        val mVersion = javaClass.superclass.superclass.getDeclaredField("mVersion")
            .apply { isAccessible = true }
        val mVersionValue = mVersion.get(this)
        // 获取mObservers
        val mObservers = javaClass.superclass.superclass.getDeclaredField("mObservers")
            .apply { isAccessible = true }
        val mObserversValue = mObservers.get(this)
        // 获取mObservers对象所属的SafeIterableMap
        val methodGet = mObserversValue.javaClass.getDeclaredMethod("get", Any::class.java)
            .apply { isAccessible = true }
        // 获取LifecycleBoundObserver
        val objectWrapperEntry = methodGet.invoke(mObserversValue, observer)
        val objectWrapper = (objectWrapperEntry as Map.Entry<*, *>).value
        // 获取ObserverWrapper
        val mLastVersion = objectWrapper!!.javaClass.superclass.getDeclaredField("mLastVersion")
            .apply { isAccessible = true; }
        val mLastVersionValue = mLastVersion.get(objectWrapper)
        // 将 mVersion的值 赋值给 mLastVersion
        mLastVersion.set(objectWrapper, mVersionValue)
    }
}


看似可以,但实际运行还是触发了粘性,断点发现 activeStateChanged() 会优于 hook() 方法的执行,方案一GG。


方案二:引入中间层


就是定义一个类包裹原始类型,在其中定义一个消费标记,用于标识值变化是否被处理,代码示例如下:


open class OneShotValue<out T>(private val value: T) {
    // 值是否被消费
    private var handled = false
    // 获取值,如果值未被处理则返回,否则返回空
    fun getValue(): T? {
        return if (handled) {
            null
        } else {
            handled = true
            value
        }
    }
    // 获取上次被处理的值,留给手动获取用
    fun peekValue(): T = value
}
// 调用处:
private val nNum = MutableLiveData<OneShotValue<Int>>()
nNum.observe(this) { Log.e("Test", "第一个观察者:${nNum.value!!.getValue()}") }
nNum.value = OneShotValue(100)
nNum.observe(this) { Log.e("Test", "第二个观察者:${nNum.value!!.getValue()}") }


运行结果如下


网络异常,图片无法展示
|


可以看出依旧是粘性的,只是第二次拿到的值为null,执行回调代码时,记得做下判空,为空不执行具体业务逻辑。


方案三:拦截观察者回调


谷歌官方给出的一个解决方案,源码可见:SingleLiveEvent.java


class SingleLiveEvent<T> : MutableLiveData<T>() {
    // 标志位,用于表达值是否被消费
    private val mPending: AtomicBoolean = AtomicBoolean(false)
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T?>) {
        // 中间观察者
        super.observe(owner) { t ->
            // 只有当值未被消费过时,才通知下游观察者
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            } else {
                Log.e("Test", "其实还是粘性...")
            }
        }
    }
    @MainThread
    override fun setValue(@Nullable t: T?) {
        // 当值更新时,置标志位为 true
        mPending.set(true)
        super.setValue(t)
    }
    @MainThread
    fun call() {
        value = null
    }
}


添加了一个 中间观察者,接管 传入观察者回调执行 的执行 ,添加一个mPending标记位(值会在setValue时更新),如果消费过就不执行回调。


这种玩法可以,但存在一个问题:所有观察者共享一个mPending 会导致第一个观察者消费后,其他观察者没机会消费。举个例:依次添加了A、B、C三个观察者,当值发生改变时,三个观察者都应该执行回调,但是用了这个类,实际上只有A会执行回调。


要解决上面这个问题也不难啊,给Observer包一层,添加一个当前观察者是否消费了数据的标记,具体代码如下:


class SingleLiveEvent<T> : MutableLiveData<T>() {
    // 暂存中间观察者
    private val observers = ArraySet<ObserverWrapper<in T>>()
    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T?>) {
        // 避免重复添加
        observers.find { it.observer === observer }?.let { _ -> return}
        ObserverWrapper(observer).apply {
            observers.add(this)
            super.observe(owner, this)
        }
    }
    @MainThread
    override fun observeForever(observer: Observer<in T>) {
        // 避免重复添加
        observers.find { it.observer === observer }?.let { _ -> return}
        ObserverWrapper(observer).apply {
            observers.add(this)
            super.observeForever(this)
        }
    }
    @MainThread
    override fun removeObserver(observer: Observer<in T>) {
        if (observer is ObserverWrapper && observers.remove(observer)) {
            super.removeObserver(observer)
            return
        }
        val iterator = observers.iterator()
        while (iterator.hasNext()) {
            val wrapper = iterator.next()
            if (wrapper.observer == observer) {
                iterator.remove()
                super.removeObserver(wrapper)
                break
            }
        }
    }
    @MainThread
    override fun setValue(@Nullable t: T?) {
        // 遍历更新所有中间观察者的标记为
        observers.forEach { it.newValue() }
        super.setValue(t)
    }
    // 中间观察者
    private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {
        // 标记当前观察者是否消费了数据
        private var pending = false
        override fun onChanged(t: T?) {
            // 保证只向下游观察者分发一次数据
            if (pending) {
                pending = false
                observer.onChanged(t)
            } else {
                Log.e("Test", "其实还是粘性...")
            }
        }
        fun newValue() {
            pending = true
        }
    }
}


运行下看看效果:


网络异常,图片无法展示
|


哈哈,其实还是粘性的,类似的方案还有 KunMinX大佬 开源的:UnPeek-LiveData,以下是某个比较老版本的代码:


public class ProtectedUnPeekLiveData<T> extends LiveData<T> {
    protected boolean isAllowNullValue;
    private final HashMap<Integer, Boolean> observers = new HashMap<>();
    public void observeInActivity(@NonNull AppCompatActivity activity, @NonNull Observer<? super T> observer) {
        LifecycleOwner owner = activity;
        Integer storeId = System.identityHashCode(observer);
        observe(storeId, owner, observer);
    }
    private void observe(@NonNull Integer storeId,
                         @NonNull LifecycleOwner owner,
                         @NonNull Observer<? super T> observer) {
        if (observers.get(storeId) == null) {
            observers.put(storeId, true);
        }
        super.observe(owner, t -> {
            if (!observers.get(storeId)) {
                observers.put(storeId, true);
                if (t != null || isAllowNullValue) {
                    observer.onChanged(t);
                }
            }
        });
    }
    @Override
    protected void setValue(T value) {
        if (value != null || isAllowNullValue) {
            for (Map.Entry<Integer, Boolean> entry : observers.entrySet()) {
                entry.setValue(false);
            }
            super.setValue(value);
        }
    }
    protected void clear() {
        super.setValue(null);
    }
}


不难看出另外定义了一个 HashMap<observer的唯一id,是否消费过的标记>,原理大同小异。


方案四:不用LiveData,改用Kotlin-Flow


今年的谷歌I/O大会,Yigit 在Jetpack的AMA中明确指出Livedata的存在就是为了照顾Java的使用者,短期内会继续维护。作为Livedata的替代品Flow会在今后渐渐成为主流,用上Flow,就不存在粘性问题了。


Kotlin Flow笔者还没来得及学,后续学完再来填这里的坑哈~


② LiveData会丢失数据吗?


答:在高频数据更新的场景下使用 LiveData.postValue() 时,会造成数据丢失。 因为 设值分发值 是分开执行的,存在延迟。值先被缓存在变量中,再向主线程抛一个分发值的任务。在这个延迟期间,再调用一次postValue(),变量中缓存的值被更新了,会导致之前的值在未分发前就被擦除。


相关代码如下:


网络异常,图片无法展示
|


③ lambda优化隐藏的坑


具体探究过程可以看下:《奇怪的编译优化》,直接说结论:

lambda写法,编译器在编译时会自作聪明优化成 添加的同一个静态的观察者


private void test3() {
    for (int i = 0;i < 10; i++) {
        model.getCurrentName().observe(this, s -> Log.v("ttt", "s:" + s))
    }
}


上述代码值发生改变,并不会收到10条通知,只会收到1条,引入外部变量 可以绕过这个优化。


④ Fragment中使用LiveData的注意事项


Fragment和其中的View生命周期不完全一致,观察LiveData时用 viewLifecycleOwner 而不是直接用 this


⑤ LiveData的数据抖动问题


所谓的数据抖动:LiveData的setValue()不会判断值是否与旧值相等,都会回调Observer.onChange()。


可以通过扩展 Transformations.kt 中的 distinctUntilChanged() 方法来解决,代码示例如下:


private val nNum = MediatorLiveData <Int>()
private val nMediatorData = Transformations.distinctUntilChanged(nNum)
nMediatorData.observe(this) {Log.e("Test", "观察者:${nNum.value!!}") }
findViewById<Button>(R.id.bt_test).setOnClickListener { nNum.value = 100 }


此时疯狂点击,控制台也只有一行输出,看一波源码~


网络异常,图片无法展示
|


啧啧,又是标志位,第一次肯定进,非第一次判断新旧值是否相等,非常简单~


0x4、小结


本节过了下LiveData用法,还对常见问题进行了归纳,虽说没有系统过一遍源码,不过大概怎么实现的心里也算有个底,使用起来也有的放矢了。


参考文献



相关文章
|
4月前
|
前端开发 Java API
Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData
Jetpack MVVM 七宗罪之五: 在 Repository 中使用 LiveData
59 0
|
4月前
|
前端开发 JavaScript Android开发
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
Jetpack MVVM 七宗罪之四: 使用 LiveData/StateFlow 发送 Events
51 0
|
9月前
|
Android开发
Android JetPack组件之LiveData的使用详解
Android JetPack组件之LiveData的使用详解
121 0
Android JetPack组件之LiveData的使用详解
|
10月前
|
存储 前端开发 Android开发
|
前端开发 数据处理 调度
Android 基于Jetpack LiveData实现消息总线
在Android开发中,跨页面传递数据(尤其是跨多个页面传递数据)是一个很常见的操作,可以通过Handler、接口回调等方式进行传递,但这几种方式都不太优雅,**消息总线**传递数据的方式相比更优雅。
244 0
|
存储 Android开发 数据格式
Android Jetpack系列之LiveData
**LiveData是一种可观察的数据存储类**。LiveData 具有生命周期感知能力,遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保 LiveData 仅更新处于活跃生命周期状态的Observer,非活跃状态下的Observer不会受到通知。
108 0
|
Android开发
安卓Jetpack狠活——Lifecycles与LiveData(二)
书接上回,我们看过了LiveData的使用,自然也就明白了这玩意虽然好,但不能处处到位,因为需要你自己去post后才能得到,那如何不用在子线程一直苦苦等待就能给人一种在实时更新的感觉呢?那自然要用到我们的狠活——Lifecycle。
106 0
|
Android开发
安卓Jetpack狠活——Lifecycles与LiveData(一)
今天在工作时,测试突然提了一个Bug给我,要求我将APP中某活动页面的UI界面要根据用户在由此页面跳转的下个页面操作,在返回时要实时更新。
|
存储 自然语言处理 前端开发
Jetpack 系列(2)—— 为什么 LiveData 会重放数据,怎么解决?
Jetpack 系列(2)—— 为什么 LiveData 会重放数据,怎么解决?
996 0
Jetpack 系列(2)—— 为什么 LiveData 会重放数据,怎么解决?
|
设计模式 缓存 自然语言处理
Jetpack 系列(4)—— 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
Jetpack 系列(4)—— 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走
663 0
Jetpack 系列(4)—— 有小伙伴说看不懂 LiveData、Flow、Channel,跟我走