深入 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 和每个 ObserverWrappermLastVersion 做对比,防止同一观察者在同一个 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:

  • 数据需要 mapfiltercombine 等链式算子
  • 一次性事件,SharedFlow 天然比各种 LiveData 补丁优雅
  • 配合 Room 的 Flow 返回类型做响应式查询
  • 多数据源 merge,combineMediatorLiveData 直观太多

我在实际项目里更倾向于 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,省下的时间比写出这些补丁方案多得多。