3、LiveData:LiveData核心原理、数据倒灌问题、MediatorLiveData与Transformations

LiveData 这个东西,说白了就是 Jetpack 里最接地气的组件之一。我刚开始接触它的时候,觉得它就是个“能感知生命周期的观察者模式”。用久了才发现,它的设计远比我想象的巧妙,坑也比我想象的多。

今天咱们就把它掰开揉碎,从原理到实战,把 LiveData 的里里外外聊透。

3.1 LiveData 的核心原理

LiveData 的本质是什么?一个持有数据、可被观察、且能感知组件生命周期的容器

你想想看,传统的观察者模式,比如 EventBus,你注册了就得手动注销,否则内存泄漏。LiveData 不需要。为什么?因为它跟 LifecycleOwner 绑定了。

3.1.1 观察者注册与生命周期绑定

当你调用 liveData.observe(this, observer) 时,LiveData 内部做了三件事:

  1. 把 observer 包装成 ObserverWrapper,带上生命周期状态
  2. 把包装后的对象存到 mObservers 这个 SafeIterableMap 里
  3. 通过 Lifecycle 添加回调,监听组件的状态变化

这里有个关键点:只有处于 ACTIVE 状态(STARTED 或 RESUMED)的观察者,才会收到数据更新。组件销毁时,观察者自动移除。

核心源码片段(简化版)

// LiveData.java
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    // 存入集合
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    // 绑定生命周期
    owner.getLifecycle().addObserver(wrapper);
}

我个人习惯把 LiveData 的观察者分为两类:“活跃观察者”和“非活跃观察者”。只有活跃观察者才会在数据变化时收到通知。这个设计太聪明了——避免了界面不可见时做无谓的更新。

3.1.2 数据分发机制:setValue vs postValue

这两个方法有什么区别?我直接说结论:

方法 调用线程 行为
setValue 主线程 立即更新数据,同步通知所有活跃观察者
postValue 任意线程 通过 Handler 切到主线程,再调用 setValue

嗯,这里有个坑。我在项目中遇到过:多次调用 postValue,只有最后一次的值会生效。为什么?因为 postValue 内部做了“值覆盖”。

避坑指南

我曾经在子线程里连续 postValue 了三次,结果 UI 只收到了最后一次的值。后来看了源码才发现,postValue 的 Runnable 里只保存了最新的 mPendingData。如果你需要每个值都通知到,请用 setValue 或者自己加锁。

3.2 数据倒灌问题

说到 LiveData,绕不开的就是“数据倒灌”。这几乎是每个 Android 开发者都会踩的坑。

3.2.1 什么是数据倒灌?

简单说就是:当 Activity 重建时(比如屏幕旋转),LiveData 会把之前已经消费过的数据再次发送给新的观察者

为什么会这样?因为 LiveData 的设计原则是:“只要观察者变为 ACTIVE 状态,就把当前持有的最新数据发给他”。这个设计本身没问题,但在 Activity 重建的场景下就出问题了。

你想想看:

  1. Activity 旋转前,LiveData 持有数据 "A"
  2. Activity 销毁重建,新的 Activity 注册观察者
  3. 新观察者变为 ACTIVE,LiveData 把 "A" 又发了一遍

这就导致 UI 上可能重复弹窗、重复加载、重复跳转……

3.2.2 解决方案:EventWrapper

我自己的项目里,最常用的方案是 EventWrapper(也叫 SingleLiveEvent)。核心思想是:让事件只被消费一次

// 事件包装类
public class Event<T> {
    private T content;
    private boolean hasBeenHandled = false;

    public Event(T content) {
        this.content = content;
    }

    public T getContentIfNotHandled() {
        if (hasBeenHandled) return null;
        hasBeenHandled = true;
        return content;
    }

    public T peekContent() {
        return content;
    }
}

// 使用方式
val liveData = MutableLiveData<Event<String>>()
liveData.observe(this) { event ->
    event.getContentIfNotHandled()?.let { data ->
        // 这里只会执行一次
        showToast(data)
    }
}

说白了,就是加了一层“消费标记”。数据被取走后,标记为已消费,下次再取就返回 null。

个人建议

如果你用的是 Kotlin,可以封装成扩展函数,用起来更顺手。我一般会在项目里建一个 LiveDataExt.kt,专门放这些工具方法。

3.3 MediatorLiveData 与 Transformations

这两个东西,是 LiveData 的“进阶玩法”。它们能帮你把多个数据源合并、转换,写出更干净的代码。

3.3.1 MediatorLiveData:合并多个 LiveData

MediatorLiveData 可以同时观察多个 LiveData 源,任何一个源的数据变化,都会触发 MediatorLiveData 的更新。

我在项目中遇到过这样的场景:一个页面需要同时监听用户信息和网络状态,两者都准备好才能展示内容

val userLiveData = MutableLiveData<User?>()
val networkLiveData = MutableLiveData<Boolean>()

val combinedLiveData = MediatorLiveData<CombinedData>().apply {
    var user: User? = null
    var isOnline: Boolean = false

    addSource(userLiveData) { u ->
        user = u
        if (user != null && isOnline) {
            value = CombinedData(user!!, isOnline)
        }
    }

    addSource(networkLiveData) { online ->
        isOnline = online
        if (user != null && isOnline) {
            value = CombinedData(user!!, isOnline)
        }
    }
}

你看,这样就不用写一堆 if-else 了。MediatorLiveData 帮你做了“合并”的工作。

3.3.2 Transformations:数据转换利器

Transformations 提供了两个静态方法:mapswitchMap

  • map:对 LiveData 的数据做一对一转换
  • switchMap:根据 LiveData 的值,切换到另一个 LiveData

举个例子:

// map:把 User 转换成显示用的字符串
val displayName: LiveData<String> = Transformations.map(userLiveData) { user ->
    "${user.firstName} ${user.lastName}"
}

// switchMap:根据 userId 切换到不同的数据源
val userDetail: LiveData<UserDetail> = Transformations.switchMap(userIdLiveData) { id ->
    repository.getUserDetail(id)  // 返回 LiveData
}

switchMap 特别适合“参数变化后重新请求数据”的场景。我习惯在 ViewModel 里这么用:

class UserViewModel : ViewModel() {
    private val userIdLiveData = MutableLiveData<String>()

    val userDetail: LiveData<UserDetail> = Transformations.switchMap(userIdLiveData) { id ->
        repository.getUserDetail(id)
    }

    fun loadUser(userId: String) {
        userIdLiveData.value = userId
    }
}

每次调用 loadUser,userIdLiveData 的值变了,switchMap 就会自动取消旧的请求,发起新的请求。这个机制在源码里是通过 MediatorLiveData 实现的——switchMap 内部其实就是一个 MediatorLiveData,它监听源 LiveData,每次变化时移除旧源、添加新源。

避坑指南

我曾经在 switchMap 里直接返回了一个 MutableLiveData,结果每次切换都创建新实例,导致旧数据丢失。正确的做法是:让 repository 返回同一个 LiveData 实例,或者使用 LiveData 的缓存机制

3.4 知识体系总览

下面这张图,是我自己总结的 LiveData 核心知识结构。你可以把它当作一个“速查地图”。

LiveData 核心原理 生命周期感知 setValue / postValue 活跃观察者机制 数据倒灌 Activity重建重复触发 EventWrapper方案 SingleLiveEvent 进阶用法 MediatorLiveData Transformations.map Transformations.switchMap 总结 LiveData = 数据容器 + 生命周期感知 + 观察者模式

这张图把 LiveData 分成了三大块:核心原理、数据倒灌、进阶用法。你可以对照着看,哪个部分不熟就重点补哪个。

3.5 写在最后

LiveData 看起来简单,但用好了能省不少事。我个人觉得,它的设计哲学就是“让数据跟着生命周期走”。你不需要手动管理注册和注销,不需要担心内存泄漏,只需要关注数据本身。

当然,数据倒灌是个烦人的问题。但理解了它的成因,用 EventWrapper 或者官方推荐的 SavedStateHandle 都能解决。至于 MediatorLiveData 和 Transformations,它们是 LiveData 的“瑞士军刀”,能帮你写出更声明式的代码。

嗯,今天就聊到这儿。下次你写 ViewModel 的时候,可以试试用 switchMap 替代那些繁琐的 LiveData 监听逻辑——你会发现代码清爽很多。


公众号:蓝海资料掘金营,微信deep3321