深入 Android LiveData 全链路:从 Lifecycle 感知机制到数据倒灌陷阱与 Flow 迁移策略的源码级解析
去年接手一个电商项目的维护工作,QA 提了个让人摸不着头脑的 bug:从商品详情页返回列表页后,列表自动刷新并跳到了顶部。排查下来发现,列表页通过 LiveData 观察了一个全局 Repository 里的数据,每次 Fragment 从回退栈恢复,LiveData 都重新把上次的数据推给观察者。
这就是数据倒灌(Data Backflow)——LiveData 社区里最常见的抱怨,但根因比表面上”粘性事件”四个字复杂得多。
Lifecycle 感知的三层协作
LiveData 的生命周期感知依赖三个组件协作:observe 做绑定入口、LifecycleBoundObserver 充当状态监听器、activeStateChanged 触发分发。
调用 observe(lifecycleOwner, observer) 时,LiveData 内部会创建包装类:
// LiveData.java core logic (simplified)
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && !existing.isAttachedTo(owner)) {
throw new IllegalArgumentException("Cannot add the same observer...");
}
owner.getLifecycle().addObserver(wrapper);
}
每个 LifecycleOwner 只能绑定一个同类型观察者。LifecycleBoundObserver 继承了 ObserverWrapper 同时实现了 LifecycleEventObserver,后者让它能感知 Lifecycle 状态变迁:
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver); // auto cleanup
return;
}
activeStateChanged(shouldBeActive());
}
}
shouldBeActive() 的判断条件很简单:Lifecycle 高于或等于 STARTED 就是 active。一旦进入 DESTROYED,观察者自动解绑——LiveData 防内存泄漏的底线。
activeStateChanged 是如何把旧数据推出去的
ObserverWrapper.activeStateChanged 是真正的分发点:
void activeStateChanged(boolean newActive) {
if (newActive == mActive) return;
mActive = newActive;
if (mActive) {
dispatchingValue(this); // 立即向该观察者分发当前值
}
}
从 inactive 回到 active 时,LiveData 无条件调用 dispatchingValue(this),传入刚变活跃的那个观察者。dispatchingValue 检查版本号后执行 observer.onChanged(mData)——mData 是 LiveData 内部持有的最新值,无论它是一秒前还是三小时前设置的。
粘性事件的源头就在这里:只要观察者从 inactive 回到 active,LiveData 默认认为你需要最新的数据。
屏幕旋转重建时这很合理,但 Fragment 返回栈恢复时,那个”最新数据”可能已经在之前消费过了,再推一次就是灾难。
数据倒灌的三层根因
1. 粘性设计本身
LiveData 的设计定位是”可观察的数据持有者”,不是”事件总线”。它始终持有当前值,新订阅者立即收到这个值。横竖屏旋转场景下,这种设计避免了重新加载数据的开销。但同一个观察者反复因 Lifecycle 切换而重新订阅时,设计初衷就成了 bug 来源。
2. ViewModel 和 Repository 放大了问题范围
ViewModel 的生命周期跨越 Fragment 的销毁重建,Repository 的生命周期往往覆盖整个进程。LiveData 放在这两层里,数据的存活时间远超 UI 层。我在那个电商项目里看到的正是这种情况:
object UserRepository {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
fun login(name: String) {
_user.value = User(name) // 这个值永远存在
}
}
一旦设置,任何后续订阅 user 的观察者都会收到这条数据——无论 UI 当时需不需要。
3. 版本号只防重复,不防倒灌
setValue 的源码揭示了 LiveData 对”消费”的认知局限:
@MainThread
protected void setValue(T value) {
mVersion++; // 只有版本号递增
mData = value;
dispatchingValue(null); // 分发给所有活跃观察者
}
mVersion 和每个 ObserverWrapper 的 mLastVersion 做对比,防止同一观察者在同一个 active 周期内收到重复的版本数据。但它解决不了”新观察者不该收到历史值”的问题——LiveData 里没有”消费后销毁”的概念,mData 永远存在。
三种补丁方案与它们的代价
SingleLiveEvent:够用但有短板
Google 官方 Samples 提供的 SingleLiveEvent,核心用 AtomicBoolean 控制事件投放:
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(value: T) {
mPending.set(true)
super.setValue(value)
}
}
compareAndSet 只会成功一次——如果有两个观察者同时监听同一个 SingleLiveEvent,只有一个能收到事件。
Event Wrapper:把消费权交给 View 层
把事件包进可消费的容器,View 层显式调用 getContentIfNotHandled():
class Event<out T>(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) null
else { hasBeenHandled = true; content }
}
fun peekContent(): T = content
}
解决了多观察者问题,但每次都要在 View 侧调用包装方法,团队里总有人忘记。
手动跳过首次值:简单粗暴但埋雷
var isFirstObserve = true
viewModel.data.observe(viewLifecycleOwner) {
if (isFirstObserve) {
isFirstObserve = false
return@observe
}
// handler
}
能救命。但如果页面确实需要首次加载的数据,这个标志位和初始化逻辑纠缠在一起,很快变脏。
StateFlow 的范式转换:数据不因观察者到来而改变
与其给 LiveData 打补丁,不如直接换思路。StateFlow 作为热流,不关心观察者何时到来——它的 value 永远是同步可查的,但收集(collect)行为取决于 Flow 的启动时机。
基础迁移长这样:
// 旧写法
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<Result>()
val data: LiveData<Result> = _data
}
// 新写法
class MyViewModel : ViewModel() {
private val _data = MutableStateFlow<Result>(Result.Loading)
val data: StateFlow<Result> = _data
}
Fragment 侧不能用 observe 了,StateFlow 没有生命周期感知:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect { result ->
// 处理数据
}
}
}
repeatOnLifecycle(STARTED) 的行为是:达到 STARTED 时启动协程并开始收集,低于 STARTED 时取消协程。和 LiveData 的 active/inactive 切换结果类似,但取消就是取消,恢复时不会自动重放旧值——StateFlow 没有被消费过的历史包袱,它只保证”此刻你是谁、你收到什么”的严格对应关系。
嫌写法啰嗦的话,封装一个扩展函数:
fun <T> Flow<T>.observeWithLifecycle(
owner: LifecycleOwner,
state: Lifecycle.State = Lifecycle.State.STARTED,
action: (T) -> Unit
) {
owner.lifecycleScope.launch {
owner.repeatOnLifecycle(state) {
this@observeWithLifecycle.collect { action(it) }
}
}
}
一次性事件(导航、Toast)必须用 SharedFlow 而非 StateFlow:
private val _navigation = MutableSharedFlow<NavDirection>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val navigation: SharedFlow<NavDirection> = _navigation
replay = 0 让新订阅者零历史数据,从根本上消灭倒灌。
保留还是迁移:一个实用决策框架
LiveData 和 Flow 在项目中可以共存,边界定清楚就好:
保留 LiveData:
- 简单单字段绑定配合 DataBinding 的
@{}语法糖,比 Flow 少写几行收集代码 - XML DataBinding 只支持 LiveData 的自动生命周期管理
改用 Flow:
- 数据需要
map、filter、combine等链式算子 - 一次性事件,SharedFlow 天然比各种 LiveData 补丁优雅
- 配合 Room 的
Flow返回类型做响应式查询 - 多数据源 merge,
combine比MediatorLiveData直观太多
我在实际项目里更倾向于 ViewModel 层全量使用 Flow 处理业务逻辑,只在 View 层需要 DataBinding 时桥接出去:
val uiState: LiveData<UiState> = combine(
userRepo.observeUser(),
settingsRepo.observeSettings()
) { user, settings ->
UiState(user, settings)
}.asLiveData()
这是渐进式迁移的关键——底层是 Flow 的强大组合能力,外层保持 LiveData 兼容。将来切换到 Compose 时,删掉 .asLiveData(),改为直接 collectAsState(),业务逻辑零改动。
几个不值得踩的坑
别在 ViewModel 中用 viewModelScope 手动收集自己的 StateFlow。 stateIn 已经帮你订阅了,重复收集只会多一个订阅者:
val data: StateFlow<Result> = flowDataSource
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), Result.Loading)
WhileSubscribed(5000) 在订阅者全离开后等待 5 秒再取消上游——配置变更时数据不丢失,也不会像 Eagerly 那样一直占着资源。
别在 init 块里给 MutableLiveData 设初值然后 observe 里跳过。 代码意图模糊,维护成本高。如果不需要初始值,直接用 SingleLiveEvent 或者迁移到 SharedFlow。
回头看 LiveData,它在 2017 年那个回调地狱的时代很体面——自带生命周期管理的数据持有者,比手动 removeCallbacks 强太多了。但当协程和 Flow 成熟后,它的粘性语义变成了陷阱。理解源码不是为了接着打补丁,而是为了做出更理性的技术选型:能用 Flow 的场景就别迁就 LiveData,省下的时间比写出这些补丁方案多得多。