☰
LiveData原理剖析:从观察者注册到生命周期感知的完整机制
2026/10/12 13:24:55 网站建设 项目流程

LiveData 大概是 Jetpack 里最被低估的一个组件。原因很简单:它的 API 太容易了,observe()一行,setValue()一行,用起来像在调一个全局变量,导致很少有人愿意追问内部到底发生了什么。但只要线上出现一次“页面从后台切回来,同一段代码被重复执行”,或者“新进页面的人莫名收到一条不该出现的事件”,你就会发现 LiveData 的真实行为跟最初直觉完全不一样。

这篇是这个系列第三篇,前两篇聊了 Jetpack 家族里状态相关组件,这篇把 LiveData 单独拎出来做原理剖析。不聊基本用法,直接钻实现:从observe()的注册链路、setValue()/postValue()的写入链路,到生命周期状态怎么影响数据分发,最后聊一聊粘性事件和工程选型。适合已经熟练使用 LiveData、但想搞清楚底层机制的开发者。

1. 从 observe() 入口拆起:观察者如何被“挂”到 LiveData 上

1.1 观察者的容器:为什么不是 List,而是 SafeIterableMap

LiveData 内部保存观察者的字段长这样:

private final SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();

第一眼可能会奇怪:为什么不直接用ArrayList存Observer?因为 LiveData 同时需要三种能力:按观察者定位、遍历时允许增删、稳定地记住顺序。ArrayList按下标删除是 O(n),HashMap删除是 O(1) 但遍历时一旦有结构性修改很容易抛异常,而SafeIterableMap是 AndroidX 自带的迭代安全双向链表,专门为“边遍历边修改”这种场景设计。

为什么必须边遍历边修改?因为当setValue正在通知所有观察者时,某个观察者的onChanged里完全可能再去removeObserver或者注册新观察者。如果是普通HashMap,大概率当场ConcurrentModificationException;SafeIterableMap的双链表迭代器支持在迭代过程中摘掉当前节点,并把新节点追加到队尾,下一次循环自然就会处理新注册的观察者。这个容器是整个 LiveData 分发系统的基础设施,理解了它,后面看注册逻辑会顺畅很多。

1.2 observe() 的注册链路:一个包装类引发的“三角关系”

LiveData.observe()的简化实现:

public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { if (owner.getLifecycle().getCurrentState() == DESTROYED) { return; // Owner 已经销毁,注册没有任何意义 } LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing != null) { // 同一个 Observer 实例不能重复绑定到不同的 LifecycleOwner return; } owner.getLifecycle().addObserver(wrapper); }

这里有三个关键点。

第一个关键是 DESTROYED 直接 return。这个设计很温和:Activity 已经销毁,此时注册只会造成内存泄漏或者永远等不到回调,直接忽略反而是最安全的行为,不抛异常也不写入容器。

第二个关键是putIfAbsent的 key 用原始Observer,value 用包装类。这说明 LiveData 内部不直接持有观察者,而是持有ObserverWrapper,生命周期状态、版本号这些都放在包装类里。外部调用removeObserver()时传的还是原始Observer,所以用原始对象做 key 才能快速找到对应的包装类。

第三个关键点是同一个Observer实例如果先绑定到 Activity A,再绑定到 Activity B,第二次putIfAbsent返回 existing,方法会直接 return,不会抛异常。也就是说,第二个 Owner 上的观察者实际没有注册成功,但代码不会立刻暴露问题,很容易造成一种“注册了但没生效”的假象。真实项目里如果遇到某个页面永远收不到更新,但代码里明明 observe 了,优先检查是不是把同一个 observer 实例复用到了多个页面。

1.3 LifecycleBoundObserver:既是数据观察者,也是生命周期监听器

接着看包装类的结构:

class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { final LifecycleOwner mOwner; @Override boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); } @Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { // 第四节细讲 } }

它继承了ObserverWrapper,所以拥有mActive和mLastVersion这些数据分发状态;同时又实现了LifecycleEventObserver,所以能接收生命周期事件。一个类扮演两个角色,这让 LiveData 的自动激活、自动清理都变成了“注册即完成”,不需要开发者额外写管理代码。

注意shouldBeActive()用的是isAtLeast(STARTED),不是 RESUMED,也不是 CREATED。这是 LiveData 对“活跃”的定义:从 onStart 到 onStop 之间都算活跃,界面可见但暂时不可交互时,数据仍然可以分发。这个阈值不是随手定的,背后的权衡在第四节单独展开。

顺便提一句observeForever()。它内部使用的是AlwaysActiveObserver,shouldBeActive()永远返回 true,完全不受生命周期限制。用起来很方便,但代价是不会有 DESTROYED 自动移除的逻辑,一旦忘记手动removeObserver,就等于把一个还活着的引用挂在全局 LiveData 上,内存泄漏是必然的。能用observe()就别用observeForever(),这是我反复跟团队说的一条铁律。

2. setValue 与 postValue 的两条写入通道:一个同步、一个异步

2.1 setValue 的同步路径:主线程限制与版本自增

先看setValue()的实现:

protected void setValue(T value) { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("必须在主线程调用 setValue"); } mVersion++; mData = value; dispatchingValue(null); }

逻辑上就三步:校验线程、版本号加一、写入数据并开始分发。分发动作是同步的,也就是说调用setValue的线程会一直执行到所有活跃观察者的onChanged返回,才会从方法里走出来。如果在界面点击事件里调用,观察者回调和点击事件在同一个调用栈,时序完全可控。

mVersion++这个细节很关键。mVersion 初始值是 -1,每次 setValue 都会自增,观察者内部还有一个mLastVersion记录自己已经消费到哪个版本。分发时只要判断mLastVersion < mVersion,就说明有“新数据”需要通知。这套版本号机制是后面讲粘性事件、新注册观察者立刻收到当前值这些现象的根源。

主线程限制让很多习惯多线程编程的朋友觉得碍事,但这是刻意为之:LiveData 的全部生命周期回调都发生在主线程,把 setValue、分发、观察者回调都放在同一线程,就完全不需要加锁。想从子线程更新数据,官方给了一条专用通道,这就是 postValue。

2.2 postValue 的异步通道:为什么频繁 post 只会收到最后一次

postValue()的真实逻辑比很多人想象中有趣:

protected void postValue(T value) { boolean post; synchronized (mDataLock) { post = mPendingData == null; mPendingData = value; } if (!post) { return; // 已经有一个 Runnable 在等,直接覆盖旧值 } ArchTaskExecutor.getInstance().postToMainThread(() -> { Object newValue; synchronized (mDataLock) { newValue = mPendingData; mPendingData = NOT_SET; } setValue((T) newValue); }); }

用大白话翻译:mPendingData相当于一个“暂存位”。第一次 postValue 时,暂存位是空的,放入数据并往主线程投递一个 Runnable;在这个 Runnable 还没执行前,如果又连续 postValue 了几次,因为暂存位已经被占用,后面的值只会把mPendingData覆盖掉,不会再投递新的 Runnable。等主线程终于执行 Runnaable 时,它把暂存位里“最后一个值”取出来,真正调用 setValue。中间的那些值,就这样被丢弃了。

所以 postValue 的语义是:保证最终观察者能拿到最新状态,但中间过程会被合并。这种“只保留最后状态”的取舍,在绝大多数 UI 场景里其实是优点。举个实际场景:模拟项目X里有一个拖动进度条实时更新的小功能,一秒钟进度可能变化几十次,如果用 setValue 直接同步分发,UI 会在主线程反复刷新,明显掉帧;改成 postValue 后,主线程只处理最后一帧,整个交互就变得非常顺滑。

这里有个经常踩的坑:如果你在子线程 postValue 一个值之后,又立刻在主线程 setValue 另一个值,最终观察者收到的不一定是 setValue 的那个值,因为 pending 的 Runnable 还没执行。我遇到过子线程任务完成后postValue(true),紧接着主线程代码又setValue(false),结果子线程的 true 后来反而把 false 覆盖掉的情况。解决思路很简单:post 和 set 不要混着写,尽量统一走同一条通道。

2.3 postValue 的适用边界与常见误区

很多文章说“postValue 可以在任意线程调用”,这句话没错,但容易让人误以为 postValue 是异步并行执行任务。实际上 postValue 只是在子线程暂存数据,真正的 setValue 仍然发生在主线程,而且是“下一个主线程消息”里。它解决的是“线程安全地传值”,不是“异步跑业务逻辑”。如果你希望子线程计算结果立刻生效,UI 一帧都不能延迟,那 postValue 的表现可能不够及时,需要结合协程或者 Handler 手动切线程。

还需要注意到:postValue 的合并行为只在 pending 状态存在时发生。如果第一个 Runnable 已经开始执行、暂存位已经清空,此时第二个 postValue 会再投递一个新的 Runnable,观察者确实会收到连续两次更新。也就是说,postValue 并不保证严格丢弃所有中间状态,它只保证“在同一个消息循环内会合并”。这个细节不看源码很容易被误解成“postValue 永远只回调一次”。

3. 版本号、活跃标志与分发队列:一次数据更新的完整旅程

3.1 版本号机制:为什么新观察者“订阅即收到当前值”

LiveData 里有两个至关重要的字段:

  • mVersion:当前数据的版本号,初始START_VERSION = -1。
  • ObserverWrapper.mLastVersion:该观察者已经消费到的版本号,初始也是 -1。

每次setValue执行,mVersion变成 0、1、2……而观察者的mLastVersion保持不变,直到真正被通知时才更新。分发函数considerNotify里只有两个 if 判断:

private void considerNotify(ObserverWrapper observer) { if (!observer.mActive) { return; } if (observer.mLastVersion >= mVersion) { return; } observer.mLastVersion = mVersion; observer.mObserver.onChanged((T) mData); }

第一个 if 管“活不活跃”,第二个 if 管“是不是新数据”。两个过滤合在一起,就是 LiveData 最基本的门禁。

现在回头看“注册后立刻收到当前值”这件事。假设 LiveData 已经 setValue 过几次,mVersion = 2;此时一个新页面注册观察者,新包装类的mLastVersion还是 -1,-1 < 2,于是下一轮分发时直接通过版本判断,把当前值回调给新观察者。这就是 LiveData“粘性”的底层来源。

但注意,观察者不活跃期间,它自己的mVersion并不会因为数据变化而增加。如果它在失活前已经消费到版本 9,失活期间 setValue 三次、mVersion 来到 12,等它重新活跃时比较 9 < 12,成立,于是会收到一次最新值。所以“粘性”只在首次注册时体现,不活跃期间的数据不会逐个补发,而是只补最新版本。

3.2 活跃状态切换:onActive/onInactive 与主动分发

ObserverWrapper里还有一个mActive标志。LifecycleBoundObserver收到生命周期事件时会调用activeStateChanged(true/false),并执行onActive/onInactive这两个空方法。MediatorLiveData正是靠重写这两个方法,才知道什么时候该监听上游数据源。

activeStateChanged内部逻辑大致如下:

void activeStateChanged(boolean newActive) { if (newActive == mActive) { return; } mActive = newActive; if (mActive) { onActive(); dispatchingValue(this); } else { onInactive(); } }

注意激活时传的是this,表示只对当前观察者补一次considerNotify。为什么要单独补一次?因为观察者可能刚从失活恢复到 STARTED,而最近一次数据更新发生在这期间。按版本号比较,mLastVersion < mVersion成立,需要主动补发一次。失活时则只调用onInactive,不进行分发。

这个设计解释了另一个高频现象:页面在 onStop 之后不可见,这段时间里数据变化了,观察者的 onChanged 不会立刻触发;等回到前台 onStart 后,如果数据确实变化过,会马上收到一次回调。这个回调不是“补发历史数据”,而是激活动作触发的主动分发测试。

3.3 dispatchingValue 的重入保护:发着发着又来新数据怎么办

setValue的最后一步是dispatchingValue(null),传 null 表示通知所有观察者。但分发过程不是简单遍历一次就结束,里面有一层重入保护:

private void dispatchingValue(ObserverWrapper initiator) { if (mDispatchingValue) { mDispatchInvalidated = true; return; } mDispatchingValue = true; do { mDispatchInvalidated = false; if (initiator != null) { considerNotify(initiator); initiator = null; } else { for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> it = mObservers.iteratorWithAdditions(); it.hasNext(); ) { considerNotify(it.next().getValue()); if (mDispatchInvalidated) { break; } } } } while (mDispatchInvalidated); mDispatchingValue = false; }

如果 LiveData 正在分发数据时,又来了一次新的 setValue,mDispatchingValue已经是 true,新数据不会递归分发,而是把mDispatchInvalidated置为 true。外层 do-while 发现标志位被置位,会中断当前遍历,重新开始一轮完整分发。这样既避免递归调用栈爆炸,又保证新数据不会被拖到下一轮。

可以把这个机制想象成正在上菜的服务员:服务员正在给一桌人上菜,厨房突然说来了新菜,他不会丢掉手里的菜冲回去,而是说“等这轮上完,再来一轮新的”。LiveData 用两个 boolean 标志完成了这种“稍后重试”的调度。

这段逻辑最大的实战价值是:观察者在 onChanged 里可以安全地继续 setValue 新值,不会栈溢出,也不会数据错乱。配合 SafeIterableMap 的迭代安全,LiveData 在数据套娃更新这种极端场景下依然稳定。

4. 生命周期如何成为“总开关”:从 LifecycleRegistry 到 ObserverWrapper

4.1 addObserver 之后发生了什么

observe()的最后一步是owner.getLifecycle().addObserver(wrapper),这一步把 LiveData 的观察者接入了 LifecycleRegistry。LifecycleRegistry 收到新观察者时,并不会只等后续事件,而是会把当前已经经历过的状态补齐一遍。比如现在 Activity 已经在 RESUMED,那新注册的 wrapper 会立刻收到一轮 ON_CREATE 到 ON_RESUME 的事件同步,这样它才能判断当前是否应该激活。

很多开发者第一次接触生命周期状态机时会被 Event 和 State 绕晕:Lifecycle.Event是动作(ON_CREATE、ON_START、ON_RESUME),Lifecycle.State是状态(CREATED、STARTED、RESUMED)。LifecycleRegistry 内部维护一张状态迁移表,事件触发后状态怎么动、状态变化后应该通知什么事件,都定义得很细致。LiveData 其实不关心完整事件流,它只关心最终状态是否isAtLeast(STARTED),所以即使生命周期状态被连续快速切换,它也能稳定地给出“活跃或失活”的结论。

4.2 LifecycleBoundObserver 的 onStateChanged:失活与自动移除的两道闸

public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { if (owner.getLifecycle().getCurrentState() == DESTROYED) { removeObserver(observer); return; } State previousState = mLastState; mLastState = owner.getLifecycle().getCurrentState(); if (previousState == mLastState) { return; } if (!shouldBeActive()) { deactivate(); } else { activeStateChanged(true); } }

最有意思的是 DESTROYED 分支:一旦 LifecycleOwner 走到 DESTROYED,LiveData 会立刻把自己观察者从 mObservers 里移除。也就是说,Activity finish 之后,观察者与 LiveData 的引用链自动断开,不会出现经典的“Activity 被长生命周期对象持有”的内存泄漏问题。

为什么不等到下一个生命周期周期再处理?因为 DESTROYED 之后,这个 Owner 几乎不可能再回到 STARTED,留着包装类没有任何意义。这个自动移除机制是 LiveData 最有价值的设计之一。没有它,每次在页面里注册观察者都要记得在 onDestroy 里清理,稍有遗漏就是事故。

previousState == mLastState的判断是防抖。同一个状态重复进来,比如连续多次 ON_START,直接忽略,避免无意义的活跃判断和分发。

4.3 为什么活跃阈值选 STARTED 而不是 RESUMED

这是 LiveData 设计里被讨论最多的问题之一。有人觉得只有界面真正可交互才算活跃,应该看 RESUMED;有人觉得只要实例还活着就改分发,该看 CREATED。官方选 STARTED,理由非常务实:

  • CREATED 阶段,界面还在创建过程中,控件未必初始化完,这时候贸然回调,容易出现空指针。
  • RESUMED 太苛刻。一个页面被半透明页面遮挡时会退到 STARTED,但业务上它仍然可见、仍然需要数据;如果以 RESUMED 为准,就会出现“界面明明还露着,数据却不刷新了”的诡异表现。
  • STARTED 是“界面真正可见”的通用下限:onStart 后视图已经准备好,onStop 前不再接受绘制,这个区间覆盖绝大多数 UI 更新场景。

LiveData 从诞生起就是面向 UI 更新设计的组件,阈值定在 STARTED 是合理取舍。如果你有数据必须在 onResume 之后才处理,可以在 onChanged 里再做一层状态判断,但不要指望 LiveData 为你的特殊场景放松门槛。

理解了这套机制,自定义LifecycleOwner其实不难:只需要持有LifecycleRegistry,在各生命周期回调里handleLifecycleEvent。精简示例:

class SimpleLifecycleOwner : LifecycleOwner { private val registry = LifecycleRegistry(this) fun onStart() { registry.handleLifecycleEvent(Lifecycle.Event.ON_START) } fun onStop() { registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) } override val lifecycle: Lifecycle = registry }

有了这个 Owner,LiveData 的观察者就能跟任意自定义生命周期同步活跃,这也是 LiveData 能渗透进各种非 Activity 场景的基础。

4.4 配合 ViewModel 时的最后一块拼图

为什么ViewModel + LiveData能活过配置变更?Activity 重建时,ViewModelStore 会把同一个 ViewModel 实例保留下来;LiveData 作为 ViewModel 的字段一直存活,mVersion和mData都原封未动。新 Activity 注册观察者时,新包装类的mLastVersion = -1,而mVersion已经很大,于是注册后立刻收到当前数据,完成 UI 恢复。

如果 LiveData 不是粘性的,配置变更后页面就会变成“已知刷新不了”的空白状态,必须手动重新加载一次。所以在这里,粘性不是 bug,而是维持状态连续性的关键需求。

5. 粘性事件:LiveData 的“特色”也是无数 Bug 的来源

5.1 “一次性事件”难题:为什么同一个事件会被反复消费

前面提到 LiveData 新观察者注册后会立刻收到当前值,这对“状态类”数据非常友好:用户信息、列表数据、开关状态,页面重建后自动恢复,体验极好。但业务里还有一类数据是“一次性事件”:登录成功后跳转首页、操作完成后弹 Toast、埋点上报只上报一次。这些数据的特点是“消费完就应该消失”,下一次进来不应该再收到。

但 LiveData 的粘性会把它变成噩梦。模拟项目X里曾经用同一个LiveData<Throwable>承载“网络错误提示”,第一次进入页面时因为 LiveData 里还留着上一次的错误对象,页面居然弹了一次已经处理过的错误提示;屏幕旋转后,又弹一次。这就是新观察者注册时版本判断通过,把旧事件当成新事件重放了一遍。

官方对这个问题的态度很明确:不打算在 LiveData 内置一次性事件语义。因为 LiveData 的核心定位是“状态保持”,一旦官方加入“消费后清除”的逻辑,那些依赖粘性的状态场景都会被打乱。所以社区才出现了各种单次事件方案。

5.2 社区常见方案的靠谱程度排序

我见过不下五种“SingleLiveEvent”实现,但大量实现都有雷。按我的经验,可以分三档:

方案做法可靠性
Event 包装 + MediatorLiveData给事件加一个hasBeenHandled标记,消费后置为已处理逻辑透明,推荐
SingleLiveEvent 自定义实现重写版本号控制,让旧事件在第二次订阅时被跳过有隐藏假设,慎用
事件与状态分离,事件走 Flow用 SharedFlow replay=0 或 Channel 做一次性事件最干净,推荐新项目

SingleLiveEvent的问题在于它默认事件只有一个观察者。如果页面里有多个 Fragment 同时观察同一个事件,可能出现 A 消费后,B 拿不到的情况;Activity 重建后又注册第三个,同样难处理。与其修 LiveData 的行为,不如在建事件通道时换一种模型。

有一点需要提醒:不要试图靠黑科技把 LiveData 改成“非粘性”。我见过有团队在 onChanged 里判断“是否是注册后的第一次回调”来跳过事件。一两个地方还好,全局打满补丁之后,代码会逐渐失控,而且每个观察者的实现都可能不一致。

5.3 我对事件层选型的最终建议

如果需要“多个页面共享同一个事件源”,我推荐用 MediatorLiveData 做聚合源,事件本身用包装类;如果项目正在全面协程化,直接用 Flow 的 SharedFlow 更好。

如果是老项目、不想大改,加一个轻量 Event 包装:

public class Event<T> { private T content; private boolean hasBeenHandled; public Event(T content) { this.content = content; } public T getContentIfNotHandled() { if (hasBeenHandled) { return null; } hasBeenHandled = true; return content; } }

ViewModel 暴露LiveData<Event<Object>>,页面里只用getContentIfNotHandled()。虽然多写一点样板代码,但行为完全可预期。记住一条铁律:状态用 LiveData,事件用 Event 包装或者 Flow,不要把两类数据混在同一个 LiveData 里。

6. 工程实录:LiveData 使用中的高频问题与选型取舍

6.1 Transformations 的隐藏成本:map/switchMap 都在主线程执行

Transformations.map底层是 MediatorLiveData,每次上游变化时,转换函数会立即在主线程调用。如果转换函数做了比较大的计算,比如把一个几千条的列表做双重过滤,就会卡 UI。遇到重计算,正确姿势是先把数据算完,再塞给 LiveData;或者用 Flow 的map + flowOn(Dispatchers.Default)算完后再转回 LiveData。

switchMap的价值在于可以动态切换上游数据源,但它要求返回值是 LiveData。很多新手在 switchMap 的返回分支里写了null,结果下游直接收到空值。建议不管分支有没有意义,都返回一个MutableLiveData<T>()占位,避免泛型推断和空值问题一起出现。

6.2 MediatorLiveData.addSource 的注册陷阱

MediatorLiveData 是做页面联动时的高频组件。每个 Source 本质上也是一个观察者,内部逻辑会在 MediatorLiveData 活跃时监听上游、不活跃时自动断开。但 addSource 同一个上游源重复注册,会直接抛 IllegalArgumentException。

所以使用 addSource 之前,要保证每次 addSource 的 source 对象是同一个,并且成对 removeSource。如果在一个方法里循环添加源,最好先对 key 做去重。我见过一个很隐蔽的 Bug:同一个 ViewModel 里的 MediatorLiveData 被两个 Fragment 同时观察,其中一个 Fragment 设置了不同的过滤条件,导致 addSource 重复注册,页面一重建就崩溃。排查到最后,根因其实是对同一个 LiveData 实例调了两次 addSource。

6.3 “从未活跃”和“不活跃”是完全不同的命中状态

面试里常问:观察者从未活跃时 setValue,会发生什么?答案是:数据更新了,但观察者不回调,因为 mActive 是 false。等它第一次活跃时,会被自动补发最新值,因为 mLastVersion 没有追上 mVersion。但如果数据在不活跃期间更新了三次,活跃后只会收到最后一次的值,中间值全部跳过。

理解这两者的区别,可以少走很多弯路。页面从后台切回来,发现界面没有自动恢复,不要第一时间怀疑 LiveData 丢数据,先查这个观察者是不是真的在 STARTED 以上状态注册。很多时候是 Fragment 已经 onStop 之后才调用 observe,导致包装类注册时就处于非活跃状态,后续分发自然一概不处理。

6.4 主线程限制、重复观察与内存泄漏:老项目高频三问

现象可能原因处理方式
子线程 setValue 直接崩溃违反主线程限制改用 postValue,或在子线程算完再切主线程 setValue
onChanged 被调用两次同一个 Observer 实例重复 observeobserve 前先 removeObserver;注意 observer 实例不要复用
页面销毁后 LiveData 仍持有了 Activity用 observeForever 但没有手动移除在 onDestroy 中调用 removeObserver,或改回 observe
注册后立刻收到旧事件LiveData 的粘性行为用 Event 包装或 Flow 做事件通道

这张表覆盖了老项目里 LiveData 相关故障的一大半。最后一条“粘性”,很多新人第一次遇到会以为框架出 bug 了,实际上它只是在履行“状态保持”的职责,问题出在事件模型和数据模型被混用了。

6.5 LiveData 与 StateFlow:什么时候该换,什么时候不必换

现在新项目基本都全面协程化,UI 层也随之换成了 StateFlow。两者核心差异就三点:

  • StateFlow 不感知生命周期,通常要配合repeatOnLifecycle,否则后台持续收集会增加开销甚至泄漏;
  • LiveData 自带生命周期感知,onStart 自动激活、onDestroy 自动清理,理解成本更低,Java 代码也能直接用;
  • StateFlow 天然支持协程上下文,可以用flowOn把计算安全地丢到后台;LiveData 的 Transformations 没有这个能力。

如果项目以 Java 为主,或者团队对协程还不熟,继续用 LiveData 完全不是技术债。它有清晰的生命周期边界,调试起来非常直白。如果是新项目、已全面协程化,用 StateFlow + repeatOnLifecycle 替代 LiveData,线程模型更好,也更接近现在社区的主流写法。

我自己的取舍是:数据层、仓库层全用 Flow,ViewModel 暴露给 UI 的状态尽量用 StateFlow;存量页面还用 LiveData 的也不急着迁,只要保证事件不跟状态混在同一个 LiveData 里,这套混搭组合足够稳定。

源码看完最大的感受是,LiveData 的代码量不大,但每个细节——版本号、包装类、SafeIterableMap、重入保护——都在为一个目标服务:让观察者在安全的时间、以可控的频率收到最新状态。把这条主线理清楚之后,再回头处理粘性事件、重复回调、活跃状态判断这类问题,方向感会完全不同。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询