民启特种作业 · 安阳新乡特种作业考证咨询

首页 安阳报考专题 新乡报考专题 报名流程 考试批次 低压电工作业 熔化焊接与热切割 高处安装维护拆除 叉车司机 塔吊司机 新闻资讯 证书查询核验 证书复审 企业团报 在线预约 关于我们 联系我们
电话咨询 18236992212
首页新闻资讯文章详情

资讯详情

考试通知、政策法规、备考经验、行业动态,为安阳、新乡特种作业考证人员提供信息参考。

首页新闻资讯LiveData原理剖析:从观察者注册到生命周期感知的完整机制

LiveData原理剖析:从观察者注册到生命周期感知的完整机制

2026/10/12 13:25:20 民启特种作业 安阳 · 新乡考证资讯
LiveData原理剖析:从观察者注册到生命周期感知的完整机制 LiveData 大概是 Jetpack 里最被低估的一个组件。原因很简单它的 API 太容易了observe()一行setValue()一行用起来像在调一个全局变量导致很少有人愿意追问内部到底发生了什么。但只要线上出现一次“页面从后台切回来同一段代码被重复执行”或者“新进页面的人莫名收到一条不该出现的事件”你就会发现 LiveData 的真实行为跟最初直觉完全不一样。这篇是这个系列第三篇前两篇聊了 Jetpack 家族里状态相关组件这篇把 LiveData 单独拎出来做原理剖析。不聊基本用法直接钻实现从observe()的注册链路、setValue()/postValue()的写入链路到生命周期状态怎么影响数据分发最后聊一聊粘性事件和工程选型。适合已经熟练使用 LiveData、但想搞清楚底层机制的开发者。1. 从 observe() 入口拆起观察者如何被“挂”到 LiveData 上1.1 观察者的容器为什么不是 List而是 SafeIterableMapLiveData 内部保存观察者的字段长这样private final SafeIterableMapObserver? super T, ObserverWrapper mObservers new SafeIterableMap();第一眼可能会奇怪为什么不直接用ArrayList存Observer因为 LiveData 同时需要三种能力按观察者定位、遍历时允许增删、稳定地记住顺序。ArrayList按下标删除是 O(n)HashMap删除是 O(1) 但遍历时一旦有结构性修改很容易抛异常而SafeIterableMap是 AndroidX 自带的迭代安全双向链表专门为“边遍历边修改”这种场景设计。为什么必须边遍历边修改因为当setValue正在通知所有观察者时某个观察者的onChanged里完全可能再去removeObserver或者注册新观察者。如果是普通HashMap大概率当场ConcurrentModificationExceptionSafeIterableMap的双链表迭代器支持在迭代过程中摘掉当前节点并把新节点追加到队尾下一次循环自然就会处理新注册的观察者。这个容器是整个 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 用原始Observervalue 用包装类。这说明 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()。它内部使用的是AlwaysActiveObservershouldBeActive()永远返回 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 (IteratorMap.EntryObserver? super T, ObserverWrapper it mObservers.iteratorWithAdditions(); it.hasNext(); ) { considerNotify(it.next().getValue()); if (mDispatchInvalidated) { break; } } } } while (mDispatchInvalidated); mDispatchingValue false; }如果 LiveData 正在分发数据时又来了一次新的 setValuemDispatchingValue已经是 true新数据不会递归分发而是把mDispatchInvalidated置为 true。外层 do-while 发现标志位被置位会中断当前遍历重新开始一轮完整分发。这样既避免递归调用栈爆炸又保证新数据不会被拖到下一轮。可以把这个机制想象成正在上菜的服务员服务员正在给一桌人上菜厨房突然说来了新菜他不会丢掉手里的菜冲回去而是说“等这轮上完再来一轮新的”。LiveData 用两个 boolean 标志完成了这种“稍后重试”的调度。这段逻辑最大的实战价值是观察者在 onChanged 里可以安全地继续 setValue 新值不会栈溢出也不会数据错乱。配合 SafeIterableMap 的迭代安全LiveData 在数据套娃更新这种极端场景下依然稳定。4. 生命周期如何成为“总开关”从 LifecycleRegistry 到 ObserverWrapper4.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_RESUMELifecycle.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 走到 DESTROYEDLiveData 会立刻把自己观察者从 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 }有了这个 OwnerLiveData 的观察者就能跟任意自定义生命周期同步活跃这也是 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里曾经用同一个LiveDataThrowable承载“网络错误提示”第一次进入页面时因为 LiveData 里还留着上一次的错误对象页面居然弹了一次已经处理过的错误提示屏幕旋转后又弹一次。这就是新观察者注册时版本判断通过把旧事件当成新事件重放了一遍。官方对这个问题的态度很明确不打算在 LiveData 内置一次性事件语义。因为 LiveData 的核心定位是“状态保持”一旦官方加入“消费后清除”的逻辑那些依赖粘性的状态场景都会被打乱。所以社区才出现了各种单次事件方案。5.2 社区常见方案的靠谱程度排序我见过不下五种“SingleLiveEvent”实现但大量实现都有雷。按我的经验可以分三档方案做法可靠性Event 包装 MediatorLiveData给事件加一个hasBeenHandled标记消费后置为已处理逻辑透明推荐SingleLiveEvent 自定义实现重写版本号控制让旧事件在第二次订阅时被跳过有隐藏假设慎用事件与状态分离事件走 Flow用 SharedFlow replay0 或 Channel 做一次性事件最干净推荐新项目SingleLiveEvent的问题在于它默认事件只有一个观察者。如果页面里有多个 Fragment 同时观察同一个事件可能出现 A 消费后B 拿不到的情况Activity 重建后又注册第三个同样难处理。与其修 LiveData 的行为不如在建事件通道时换一种模型。有一点需要提醒不要试图靠黑科技把 LiveData 改成“非粘性”。我见过有团队在 onChanged 里判断“是否是注册后的第一次回调”来跳过事件。一两个地方还好全局打满补丁之后代码会逐渐失控而且每个观察者的实现都可能不一致。5.3 我对事件层选型的最终建议如果需要“多个页面共享同一个事件源”我推荐用 MediatorLiveData 做聚合源事件本身用包装类如果项目正在全面协程化直接用 Flow 的 SharedFlow 更好。如果是老项目、不想大改加一个轻量 Event 包装public class EventT { 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 暴露LiveDataEventObject页面里只用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结果下游直接收到空值。建议不管分支有没有意义都返回一个MutableLiveDataT()占位避免泛型推断和空值问题一起出现。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或在子线程算完再切主线程 setValueonChanged 被调用两次同一个 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线程模型更好也更接近现在社区的主流写法。我自己的取舍是数据层、仓库层全用 FlowViewModel 暴露给 UI 的状态尽量用 StateFlow存量页面还用 LiveData 的也不急着迁只要保证事件不跟状态混在同一个 LiveData 里这套混搭组合足够稳定。源码看完最大的感受是LiveData 的代码量不大但每个细节——版本号、包装类、SafeIterableMap、重入保护——都在为一个目标服务让观察者在安全的时间、以可控的频率收到最新状态。把这条主线理清楚之后再回头处理粘性事件、重复回调、活跃状态判断这类问题方向感会完全不同。
特种作业考证资讯 责任编辑:民启特种作业
FUWU BAOZHANG

看完文章,报名服务了解一下

从咨询到拿证,全程有人对接

条件先核对年龄、学历、体检先对照,能报才报
材料免费预审材料拍来先审,不齐的提前补
批次主动提醒报名截止、考试时间提前通知
费用透明费用报名前逐项列明,确认后再办

看完文章还有疑问?

报考条件、材料清单、考试批次,直接电话或在线咨询,几分钟给你明确答复。