3、LiveData:LiveData核心原理、数据倒灌问题、MediatorLiveData与Transformations
LiveData 这个东西,说白了就是 Jetpack 里最接地气的组件之一。我刚开始接触它的时候,觉得它就是个“能感知生命周期的观察者模式”。用久了才发现,它的设计远比我想象的巧妙,坑也比我想象的多。
今天咱们就把它掰开揉碎,从原理到实战,把 LiveData 的里里外外聊透。
3.1 LiveData 的核心原理
LiveData 的本质是什么?一个持有数据、可被观察、且能感知组件生命周期的容器。
你想想看,传统的观察者模式,比如 EventBus,你注册了就得手动注销,否则内存泄漏。LiveData 不需要。为什么?因为它跟 LifecycleOwner 绑定了。
3.1.1 观察者注册与生命周期绑定
当你调用 liveData.observe(this, observer) 时,LiveData 内部做了三件事:
- 把 observer 包装成
ObserverWrapper,带上生命周期状态 - 把包装后的对象存到
mObservers这个 SafeIterableMap 里 - 通过 Lifecycle 添加回调,监听组件的状态变化
这里有个关键点:只有处于 ACTIVE 状态(STARTED 或 RESUMED)的观察者,才会收到数据更新。组件销毁时,观察者自动移除。
核心源码片段(简化版)
// LiveData.java
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer super T> 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 重建的场景下就出问题了。
你想想看:
- Activity 旋转前,LiveData 持有数据 "A"
- Activity 销毁重建,新的 Activity 注册观察者
- 新观察者变为 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 提供了两个静态方法:map 和 switchMap。
- 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 分成了三大块:核心原理、数据倒灌、进阶用法。你可以对照着看,哪个部分不熟就重点补哪个。
3.5 写在最后
LiveData 看起来简单,但用好了能省不少事。我个人觉得,它的设计哲学就是“让数据跟着生命周期走”。你不需要手动管理注册和注销,不需要担心内存泄漏,只需要关注数据本身。
当然,数据倒灌是个烦人的问题。但理解了它的成因,用 EventWrapper 或者官方推荐的 SavedStateHandle 都能解决。至于 MediatorLiveData 和 Transformations,它们是 LiveData 的“瑞士军刀”,能帮你写出更声明式的代码。
嗯,今天就聊到这儿。下次你写 ViewModel 的时候,可以试试用 switchMap 替代那些繁琐的 LiveData 监听逻辑——你会发现代码清爽很多。