深入理解Jetpack——LiveData

什么是 LiveData

LiveData 是基于 Lifecycle 框架实现的生命周期感知型数据容器,能够让数据观察者更加安全地应对宿主(Activity / Fragment 等)生命周期变化。LiveData 主要有两个特点:

  1. 会自动取消订阅,当宿主生命周期进入 DESTROYED 状态时,LiveData 会自动移除观察者,避免内存泄漏
  2. 会安全地回调数据,当宿主生命周期状态低于 STAETED 时,LiveData 不会回调数据;当宿主生命周期不低于 STAETED 时,LiveData 就会重新尝试回调最新的数据。

LiveData 的使用

基本使用

一般情况下,我们使用 LiveData 和 MutableLiveData 两个类。区别是 LiveData 是不可变的,而 MutableLiveData 是可变的,我们可以使用 setValue (主线程使用) 和 postValue (子线程使用)来更新值。代码示例如下:

kotlin 复制代码
class MyViewModel: ViewModel() {

    private val _liveData: MutableLiveData<Int> = MutableLiveData()

    val liveData: LiveData<Int>
        get() = _liveData

    fun update(value: Int) {
        _liveData.value = value
    }

    fun postUpdate(value: Int) {
        //在子线程更新值,需要使用 postValue
        _liveData.postValue(value)
    }
}


class MainActivity : ComponentActivity() {  
  
    val myViewModel by viewModels<MyViewModel>()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        myViewModel.liveData.observe(this) {
            println("data = $it")
        }
        //模拟更新数据
        thread {
            Thread.sleep(1000)
            println("set data 1")
            myViewModel.postUpdate(1)
            println("set data 2")
            myViewModel.postUpdate(2)
        }
        // 启动其他 Activity
        ...
    }
}

当 MainActivity 启动其他 Activity 后,我们在子线程里面更新 liveData 里面的值是不会回调的。当从其他 Activity 回到 MainActivity 时,这时才会回调通知。需要注意的是,如果存在多个值,只会回调最新值,上面的示例就只会回调通知 data 为 2。

observeForever

如果你想要当数据发生变化时,无论页面处于什么状态我们都能收到通知,这时可以使用 observeForever 方法。但是,需要注意一定要记得调用 removeObserver 方法来停止对 LiveData 的观察,否则可能造成内存泄漏。

kotlin 复制代码
val observer = { result: Int ->  
    println("data = $result")  
} 
//注册监听
myViewModel.liveData.observeForever(observer)  
//移除监听  
myViewModel.liveData.removeObserver(observer)

MediatorLiveData

MediatorLiveData 是一个可以监听其他 LiveData 数据变化的 LiveData。当其他 LiveData 调用 onChange 方法时,会调用设置的 lambda 函数。代码示例如下:

kotlin 复制代码
val mediatorLiveData = MediatorLiveData<Int>()  
mediatorLiveData.addSource(myViewModel.liveData) {  
    //当 myViewModel.liveData 值变更时,会回调这里
    println("mediatorLiveData addSource data = $it") 
    //主动更新自己的值
    mediatorLiveData.value = it
}
mediatorLiveData.observe(this) {  
    //主动更新自己的值时,才会回调这里
    println("mediatorLiveData data = $it")  
}

虽然开发中 MediatorLiveData 比较少见,但是实际上它用的地方非常多。比如 LiveData 的转换操作 map、switchMap 都是通过 MediatorLiveData 来实现的。

map

map 操作符可以将某种 LiveData 类型的数据转换为另一种类型的。代码示例如下,将学生类型的 LiveData 转化为分数类型的 LiveData,减少不必要的耦合。

kotlin 复制代码
private var _studentLiveData = MutableLiveData<Student>()
val score: LiveData<Int>
    get() = _studentLiveData.map { it.score }   

map 的使用介绍完了,现在直接来看它的源码。如下所示,map 的实现其实很简单,就是使用 MediatorLiveData。当原 LiveData 的数据变更时,就会回调到 transform 方法。当原 LiveData 的 value 只经过 transform 转化后,就将获取的值设置给 MediatorLiveData,通知更新。

kotlin 复制代码
fun <X, Y> LiveData<X>.map(
    transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y)
): LiveData<Y> {
    val result = MediatorLiveData<Y>()
    if (isInitialized) {
        result.value = transform(value as X)
    }
    result.addSource(this) { x -> result.value = transform(x) }
    return result
}

switchMap

switchMap 的源码如下所示:

kotlin 复制代码
fun <X, Y> LiveData<X>.switchMap(
    transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData<Y>)?
): LiveData<Y> {
    val result = MediatorLiveData<Y>()
    var liveData: LiveData<Y>? = null
    if (isInitialized) {
        val initialLiveData = transform(value as X)
        if (initialLiveData != null && initialLiveData.isInitialized) {
            result.value = initialLiveData.value
        }
    }
    result.addSource(this) { value: X ->
        val newLiveData = transform(value)
        if (liveData !== newLiveData) {
            if (liveData != null) {
                result.removeSource(liveData!!)
            }
            liveData = newLiveData
            if (liveData != null) {
                result.addSource(liveData!!) { y -> result.setValue(y) }
            }
        }
    }
    return result
}

相比于 map 扩展函数,switchMap 函数的源码就比较难懂了。我这里画了一个草图,方便理解。其实 switchMap 方法创建的 MediatorLiveData 监听了两个 LiveData,一个是原来的LiveData,一个是通过 transform 函数生成的 LiveData(这里叫做A LiveData)。

可以看到,MediatorLiveData 在两种情况下会通知更新:一种是原 LiveData 更新导致生成新的 A LiveData ,还有一种就是 A LiveData 自身的 Value 更新。

distinctUntilChanged

当我们往 LiveData 设置相同的数据时,LiveData 不会进行重复判断。因此即使是相同的值,它也会调用 onChange 方法来通知监听。

如果我们需要过滤掉重复的数据怎么办?这时就可以使用 distinctUntilChanged 方法。它内部会使用 MediatorLiveData 来过滤掉重复的值。代码示例如下:

kotlin 复制代码
val stateLiveData: LiveData<Int>
    get() = _liveData.distinctUntilChanged()

自定义操作符

从上面可以看到使用 MediatorLiveData,可以方便做很多转化操作。下面我们就可以使用 MediatorLiveData 实现一个组合效果。代码如下所示:

kotlin 复制代码
fun <X> LiveData<X>.combine(otherLiveData: LiveData<X>): LiveData<X> {
    val outputLiveData = MediatorLiveData<X>()
    outputLiveData.addSource(this, outputLiveData::setValue)
    outputLiveData.addSource(otherLiveData, outputLiveData::setValue)
    return outputLiveData
}

SavedStateHandle

ViewModel + LiveData 的组合想必大家都使用过。该方案解决了在 Android 中因配置更改界面销毁时,数据的保存问题。但是对于系统资源限制而导致的 Activity 被销毁时,该方案就无能为力了。一般情况下遇到这种问题,我们只能通过使用 onSaveInstanceStateonRestoreInstanceState 方法来保存和恢复数据。不过 Google 早就想到了这个问题,提供了 SavedStateHandle 来完善这最后一块拼图。

代码示例如下:

kotlin 复制代码
//一般情况下,我们使用 SavedStateHandle 作为 MyViewModel 的入参
class MyViewModel(val savedStateHandle: SavedStateHandle): ViewModel() {

    companion object {
        const val KEY = "SAVED_STATE_KEY"
    }

    //获取对应 value 的LiveData
    private val savedStateLiveData
        get() = savedStateHandle.getLiveData<String>(KEY)

    
    fun setSavedStateLiveData(value: String) {
        //SavedStateHandle 通过key-value的形式保存数据
        savedStateHandle[KEY] = value
    }
}

//对于入参为 SavedStateHandle 的 ViewModel,viewModels 委托做了处理,
//不需要我们自定义 Factory
val myViewModel by viewModels<MyViewModel>()
//保存数据
myViewModel.setSavedStateLiveData("test_data")

SavedStateHandle 会在 Activity 被销毁时通过onSaveInstanceState(Bundle)方法将数据保存在 Bundle 中,在重建时又将数据从 onCreate(Bundle?)中取出,开发者只负责向 SavedStateHandle 存取数据即可,并不需要和 Activity 直接做交互,从而简化了整个开发流程

LiveData 的缺陷

数据丢失

LiveData 有两种情况会导致数据丢失。一种是观察者绑定的生命周期处于非活跃状态时,连续使用 setValue() / postValue() 设置数据时,观察将无法接收到中间的数据。这时造成了数据丢失。

另一种就是调用 postValue 造成的数据丢失。要了解为什么 postValue 方法会造成数据丢失,我们就需要先看看 LiveData 的源码,如下所示:

java 复制代码
final Object mDataLock = new Object();

static final Object NOT_SET = new Object();

// 临时变量
volatile Object mPendingData = NOT_SET;

private final Runnable mPostValueRunnable = new Runnable() {
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        Object newValue;//注释1
        synchronized (mDataLock) {
            newValue = mPendingData;
            // 重置临时变量
            mPendingData = NOT_SET;
        }
        // 真正修改数据的地方,也是统一到 setValue() 设置数据
        setValue((T) newValue);
    }
};

protected void postValue(T value) {
    boolean postTask;
    synchronized (mDataLock) {
        // 临时变量被重置时,才会发送修改的 Message,这是出现背压的第 1 种情况
        postTask = mPendingData == NOT_SET;
        mPendingData = value;
    }
    if (!postTask) {//注释2
        return;
    }
    ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

假设当主线程执行到注释1 时,这时 mPendingData 不为 NOT_SET,这个时候调用 postValue 方法,postTask 为 false,执行到注释2,退出,这个数据就丢失了。假设当子线程执行到注释2时,其他子线程执行postValue 也会丢掉数据。

数据重放问题

LiveData 的数据重放问题也叫作数据倒灌、粘性事件。它是指新观察者监听一个 LiveData 时,LiveData 会重新分发数据给这个新观察者。代码示例如下:

javascript 复制代码
myViewModel.liveData.observe(this) {
    println("liveData observe1 data = $it")
}
myViewModel.update(1)
myViewModel.liveData.observe(this) {
    println("liveData observe2 data = $it")
}

打印结果为:

ini 复制代码
liveData observe1 data = 1
liveData observe2 data = 1

为什么会产生这种情况呢,这是因为 LiveData 和观察者各自会持有一个版本号 version,每次调用 setValue 或 postValue 方法后,LiveData 持有的版本号会自增 1。在 LiveData 尝试分发数据时,会判断观察者持有版本号是否小于 LiveData 的版本号,如果成立则说明这个观察者还没有消费最新的数据版本。而观察者的持有的初始版本号是 -1,因此当注册新观察者并且正好宿主的生命周期是大于等于可见状态(STARTED)时,就会尝试分发数据,这就是造成数据重放的原因。

总结

当 Activity/Fragment 不处于活跃状态时,LiveData 不会回调数据,而当 Activity/Fragment 重新活跃时,LiveData 就会重新尝试回调最新的数据;当宿主生命周期进入 DESTROYED 状态时,LiveData 也会自动移除观察者,避免内存泄漏。通过 LiveData ,我们在处理数据时可以减少其生命周期的关心,把重心放在具体的业务上来。

但是 LiveData 也有一些缺陷,比如数据丢失数据重放问题。因此在 Android 中,目前是更推荐使用 Flow (ShareFlow、StateFlow)来替代 LiveData。不过至于要不要使用 LiveData ,还是要看具体的业务场景,根据自己的需要来选择。

参考

相关推荐
rising_chain2 小时前
Uniapp 引入 Android aar 包 和 Android 离线打包
android·uni-app·uniapp 离线打包
.生产的驴5 小时前
Docker 部署Nacos 单机部署 MYSQL数据持久化
android·运维·spring boot·sql·mysql·docker·容器
找藉口是失败者的习惯6 小时前
Android adb 指令大全
android·adb
初学者-Study6 小时前
Android Osmdroid + 天地图 (二)
android·osmdroid地图点击·定位监听·marker配置
喜欢踢足球的老罗7 小时前
RN开发搬砖经验之—React Native(RN)应用转原生化-Android 平台
android·react native·react.js
红米饭配南瓜汤7 小时前
Android Binder通信02 - 驱动分析 - 架构介绍
android·架构·binder
️ 邪神7 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】启动页
android·flutter·ios·鸿蒙·reactnative
zhangphil8 小时前
Android从Drawable资源Id直接生成Bitmap,Kotlin
android·kotlin
HenCoder8 小时前
【泛型 Plus】Kotlin 的加强版类型推断:@BuilderInference
android·java·开发语言·kotlin
虾球xz8 小时前
游戏引擎学习第12天
android·学习·游戏引擎