深入理解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 ,还是要看具体的业务场景,根据自己的需要来选择。

参考

相关推荐
鹏程十八少4 分钟前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
Kapaseker21 分钟前
一杯美式搞定 Kotlin 空安全
android·kotlin
三少爷的鞋1 小时前
Android 协程时代,Handler 应该退休了吗?
android
火柴就是我14 小时前
让我们实现一个更好看的内部阴影按钮
android·flutter
FunnySaltyFish18 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
砖厂小工21 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心1 天前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心1 天前
Android 17 来了!新特性介绍与适配建议
android·前端
Kapaseker1 天前
Compose 进阶—巧用 GraphicsLayer
android·kotlin
黄林晴1 天前
Android17 为什么重写 MessageQueue
android