
这个问题我在刚接触 LiveData 的时候也纠结过很久明明只是套了个观察者模式怎么它就“感知”生命周期了怎么注册之后立刻能收到一条旧数据为什么 postValue 连续调用两次回调却只触发一次这些现象如果不看源码你很难真正讲清楚。本文是 LiveData 系列的第二篇上一篇已经把基本用法和常见场景过了一遍这篇专门啃原理。我会直接对着 LiveData 的源码层androidx.lifecycle 2.x 版本逐段拆把注册、分发、生命周期感知、版本号机制、粘性事件这些核心逻辑一次讲明白。适合已经会基本使用 LiveData、但对内部实现好奇或者面试前想系统梳理一遍的 Android 开发者。1. 先从使用体验说起LiveData 到底藏了哪些秘密1.1 一个简单的例子为什么注册就收到旧数据先看一个非常经典的使用场景。我在项目里维护一个用户信息的数据仓库登录成功后通过 LiveData 把用户昵称抛出去class UserRepository { val userName MutableLiveDataString() fun login() { // 模拟登录成功后设置数据 userName.value 老王 } }然后在 Activity 里这样观察userRepository.userName.observe(this) { name - Log.d(test, name $name) }你要是运行这段代码会发现一个现象即使登录方法已经执行过了此时注册 observer回调里依然会立刻打印一次“name 老王”。这条回调并不是因为数据“更新”了而是因为 LiveData 的粘性特性——新注册的观察者一旦处于活跃状态就会立刻收到当前持有的数据。很多人把这种现象称为“粘性事件”并且经常把它当成 LiveData 不适合做事件总线的理由。但从源码看这根本不是一个 Bug而是版本号机制必然导致的结果。后面我会专门用一节来讲版本机制这里先埋个引子。1.2 核心字段解密从源码看到的三件套打开 LiveData 的源码去掉注释和常量真正参与核心逻辑的字段其实就那么几个private volatile int mVersion START_VERSION; // 当前数据版本号 private final Object mDataLock new Object(); // 数据读写锁 private volatile Object mData; // 当前持有的数据 private int mDispatchInvocationCounter; // 分发调用计数器旧版本 private volatile boolean mDispatchValueChanged; // 分发中的数据是否已变化旧版本 private volatile boolean mDispatchingValue false; // 是否正在分发新版本 private boolean mDispatchInvalidated false; // 分发过程中是否有新数据写入 private final SafeIterableMapObserver? super T, ObserverWrapper mObservers;这里最容易忽视的是mVersion。它默认值是START_VERSION也就是-1。每调用一次setValuemVersion就加一。每一个注册进去的 ObserverWrapper 内部还维护着自己的mLastVersion初始值也是START_VERSION。数据要不要回调给某个观察者核心就是比较这两个版本号的大小。SafeIterableMap是用来存储观察者的容器。它本质是个有序链表实现的 Map特殊之处在于它支持在遍历的过程中安全地插入和删除元素。这个设计不是随手写的因为在分发数据的过程中观察者完全可能在 onChanged 回调里再次注册新观察者或者移除自己如果用的是普通的 HashMap早就抛出 ConcurrentModificationException 了。1.3 观察者模式的“安全”实现 SafeIterableMapLiveData 没有直接用现成的 HashMap 或者 LinkedHashMap而是自己写了一个 SafeIterableMap这个点很值得说。它的实现思路是内部通过双向链表维护 key-value 节点同时维护一个 HashMap 用于快速查找节点。遍历时它返回一个特殊的迭代器这个迭代器允许在迭代过程中把新增的节点也遍历到并且删除节点不会导致指针错乱。为什么不用 ConcurrentHashMap因为 LiveData 的观察者数量通常不会太大OnChanged 回调里注册/移除观察者属于高频操作使用一个针对小数据量优化的链表结构反而能在保证顺序性的同时避免并发修改的崩溃问题。而且观察者的遍历顺序是固定的按注册先后顺序回调这一点对于 UI 场景很有意义用户通常期望先注册的先收到数据。2. 生命周期是怎么“感知”的2.1 observe 注册流程从 LifecycleBoundObserver 说起LiveData 对外暴露的核心注册方法是observe(LifecycleOwner, Observer)注意它必须运行在主线程源码第一行就是assertMainThread(observe)。紧接着做了一次判断如果当前 LifecycleOwner 的 Lifecycle 状态已经是 DESTROYED直接 return连观察者都不注册。然后进入关键部分——LiveData 并不会直接把你的 Observer 存进那个 SafeIterableMap而是先包一层LifecycleBoundObserverLifecycleBoundObserver wrapper new LifecycleBoundObserver(owner, observer); observerWrapper mObservers.putIfAbsent(observer, wrapper); if (observerWrapper ! null !observerWrapper.isAttachedTo(owner)) { throw new IllegalArgumentException(Cannot add the same observer with different lifecycles); } if (observerWrapper ! null) return; owner.getLifecycle().addObserver(wrapper);LifecycleBoundObserver同时实现了LifecycleEventObserver接口这意味着它本身就是个 Lifecycle 观察者。当 LifecycleOwner 的生命周期状态发生变化时系统会回调它的onStateChanged方法这是 LiveData 能感知生命周期的第一个关键点。第二个关键点是LiveData 保存的 key 是你传入的原始 Observervalue 是包装后的 LifecycleBoundObserver。这样设计的好处是外部调用 removeObserver 时传入原始 Observer就能精确移除对应的包装类。2.2 onStateChanged 的四个分支LifecycleBoundObserver 的onStateChanged是整个生命周期感知的“发动机”源码逻辑是这样的Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { Lifecycle.State state source.getLifecycle().getCurrentState(); if (state Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this); return; } Lifecycle.State prevState getState(); if (prevState ! state) { activeStateChanged(shouldBeActive()); } }注意这里的顺序很有讲究。先判断是否 DESTROYED如果是就直接移除自己连后续的状态判断都不做。因为 LifecycleOwner 已经走到销毁流程再做数据分发已经没有意义而且有可能操作已经销毁的 View。接着是一个很细的点它拿到的是Lifecycle.State而不是Event。当前状态做过一次去重比较只有状态确实变化了才会触发 active 状态更新。比如从 RESUMED 变到 STARTED虽然对 LiveData 来说两者都处于“活跃”状态但状态值确实不同因此依然会触发一次 activeStateChanged 的逻辑只是内部发现 active 值没变就不做额外处理。2.3 active 与 inactive 的切换逻辑activeStateChanged方法在 ObserverWrapper 基类里它做的事情很单纯void activeStateChanged(boolean newActive) { if (newActive mActive) return; mActive newActive; boolean wasInactive LiveData.this.mActiveCount 0; LiveData.this.mActiveCount mActive ? 1 : -1; if (wasInactive mActive) onActive(); if (LiveData.this.mActiveCount 0 !mActive) onInactive(); if (mActive) dispatchingValue(this); }这里有两个细节值得关注。第一LiveData 内部维护了一个mActiveCount它统计的是“活跃观察者”的数量。当活跃观察者从 0 变成 1 时会触发onActive()也就是 LiveData 变为活跃状态当活跃观察者从 1 变成 0 时触发onInactive()。在 MediatorLiveData 中这两个回调是管理上游数据源订阅的关键钩子。第二如果观察者变为活跃状态最后一行会立即调用dispatchingValue(this)把自己作为启动分发器的“initiator”。注意这里没有做版本号判断就直接触发分发。说明在活跃状态切换时LiveData 总是尝试跑一遍完整的分发流程至于观察者能不能收到数据是后续considerNotify里版本号比较决定的。这也就引出了 LiveData 最核心的版本机制。3. 版本机制LiveData 最精妙的设计3.1 mVersion 与 mLastVersion 的博弈LiveData 的数据分发规则用一句话概括就是谁的 mLastVersion 落后于 mVersion谁就要补一次回调。mVersion是全局的每次 setValue 都会自增mLastVersion是每个 ObserverWrapper 私有的只有这个观察者真正收到回调时才会更新。看considerNotify源码private void considerNotify(ObserverWrapper observer) { if (!observer.mActive) return; if (!observer.shouldBeActive()) { observer.activeStateChanged(false); return; } if (observer.mLastVersion mVersion) return; observer.mLastVersion mVersion; observer.mObserver.onChanged((T) mData); }这个方法的执行流程非常清晰观察者必须活跃、生命周期状态必须满足要求、最后是版本号必须落后三个条件缺一不可最终才会回调数据。这里有一个容易被忽略的细节observer.mLastVersion mVersion是在调用onChanged之前执行的。也就是说一旦开始回调这个观察者的版本号立即同步到最新即使 onChanged 内部抛出了异常也不会造成同一个数据被反复回调。版本号机制解释了粘性事件为什么必然存在新注册的 ObserverWrapper 的 mLastVersion 默认是 -1而只要 LiveData 之前被 setValue 过一次mVersion 就大于等于 0新观察者一注册就处于版本落后的状态自然立刻补一次回调。这不是设计缺陷而是保证“数据完整性”的副产品。3.2 considerNotify分发前的三道关卡我把 considerNotify 里的逻辑拆解成三道关卡这样面试时讲起来会更清晰第一道关卡是mActive标志位。只有观察者处于活跃状态才继续往下走。一个在后台的 Fragment 对应的观察者可能已经变成 inactive数据来了也不会立刻收到而是等回到前台时再补发。第二道关卡是shouldBeActive()。这个方法在 LifecycleBoundObserver 中实现为mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED)。为什么要单独做一次判断而不是直接信任 mActive因为 LifecycleOwner 的状态可能刚刚变化但 LifecycleBoundObserver 的 onStateChanged 还没有被回调此时 mActive 还是旧值。这个检查保证分发时的状态是实时的避免在状态已经到 STARTED 以下的瞬间还把数据分发出去。第三道关卡才是版本号比较。observer.mLastVersion mVersion说明该观察者已经收到过这条数据了直接跳过。只有版本落后时才触发真正的 onChanged。3.3 粘性事件究竟是不是 Bug网上关于“LiveData 粘性事件是 Bug 吗”的讨论一直很多说说我的结论它是设计使然但确实给事件场景带来了坑。在状态管理场景下粘性特性反而是个优点——屏幕旋转后重建 Activity新注册的观察者能立刻拿到当前 UI 状态ViewModel 中的数据能无缝恢复用户根本察觉不到界面重新加载过。但在事件场景下粘性事件就成了问题。比如一个“弹 Toast”事件你发送完事件后页面发生重建新的观察者注册后立刻收到旧事件Toast 会被重复弹出。这就是为什么社区里出现了各种各样的“事件包装类”方案。它们本质上都是给 LiveData 加一个“只消费一次”的开关背后的思路不是修改 LiveData 的版本机制而是在 Observer 内部做消费标记。我个人不建议用反射去修改 LiveData 源码里的版本逻辑风险太高而且升级 androidx 库时可能直接挂掉。更稳妥的思路是用 Event 包装类或者干脆换 SharedFlow 处理纯事件场景。后面章节我会给一个具体的封装方案。4. setValue 与 postValue数据写入的正确姿势4.1 为什么 setValue 必须在主线程setValue 的源码非常短但每一行都有讲究protected void setValue(T value) { assertMainThread(setValue); mVersion; mData value; dispatchingValue(null); }第一行强制要求主线程调用否则直接抛异常。原因很朴素onChanged 回调最终必然发生在主线程生命周期回调、主线程分发都是主线程上下文如果允许子线程直接 setValue就会存在多线程同时修改 mData 和 mVersion 的竞态问题观察者拿到的数据可能不一致。所以干脆在源头卡死线程。第二三行先自增版本号再赋值数据。这个顺序是刻意的如果先赋数据再自增版本极端情况下可能有一个正在执行的分发流程读到了新数据但版本号还没变导致它误判数据已消费过。先改版本号再放数据能保证版本号和数据变更的“可见顺序”是一致的。dispatchingValue(null)是给所有观察者发数据。传 null 表示不是从某个特定观察者发起的而是全量分发。4.2 postValue 的“值合并”陷阱postValue 的作用是允许在子线程设置数据源码设计得比 setValue 复杂一些protected void postValue(T value) { boolean postTask; synchronized (mDataLock) { postTask mPendingData NOT_SET; mPendingData value; } if (!postTask) return; ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); }这里需要重点讲清楚mPendingData的作用。LiveData 内部通过一个 Object 类型的mPendingData暂存待提交的数据初始值是NOT_SET这个哨兵对象。第一次调用 postValue 时mPendingData 还是 NOT_SETpostTask 为 true于是向主线程抛出一个 Runnable第二次再调用 postValue 时mPendingData 已经不是 NOT_SET 了postTask 变为 false直接 return不会再次向主线程提交任务。主线程的 Runnable 执行时会从 mPendingData 中取出最新的那个值然后交给 setValue 走正常分发流程。这带来一个非常容易被踩到的坑连续多次 postValue中间的中间值会被丢弃观察者只会收到最后一次设置的值。这在业务上的表现是什么你从子线程里循环发送进度数据UI 可能只会收到其中一部分最终回调的是最后一个值。对于进度条这类场景中间值丢失一般可以接受但如果每个值都很关键就需要自己加队列或者改用其他方案不能盲目依赖 postValue。4.3 分发状态机防止重入与无效循环把 dispatchingValue 的整个状态机讲清楚LiveData 原理就通了八成。看源码void dispatchingValue(Nullable ObserverWrapper initiator) { if (mDispatchingValue) { mDispatchInvalidated true; return; } mDispatchingValue true; do { mDispatchInvalidated false; if (initiator ! null) { considerNotify(initiator); initiator null; } else { for (IteratorMap.Entry... iterator mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { considerNotify(iterator.getValue()); if (mDispatchInvalidated) break; } } } while (mDispatchInvalidated); mDispatchingValue false; }这个设计解决的是“分发过程中再次设置数据”的递归问题。如果在某个观察者的 onChanged 回调里再次调用了 setValue此时 mDispatchingValue 已经是 true内层 setValue 调用 dispatchingValue 会走第一行分支只把 mDispatchInvalidated 置为 true 然后返回。外层分发循环发现 mDispatchInvalidated 为 true会重新执行一次全量遍历但这次遍历中已经消费过数据的观察者版本号已经更新不会再被重复回调。这里最妙的地方在于它把潜在的递归调用巧妙转化成了迭代轮询既不会栈溢出也不会重复通知已消费的观察者。我看到不少自定义的观察者框架一发数据就是 for 循环通知完全没有处理重入问题如果回调里再发数据就是递归崩溃。LiveData 这个状态机的思路非常值得借鉴。5. MediatorLiveData 与更多使用场景5.1 MediatorLiveData 的多源汇聚原理MediatorLiveData 继承自 MutableLiveData但内部多了一个成员SafeIterableMapLiveData?, Source? mSources用来管理多个上游 LiveData 源。它重写了 onActive 和 onInactive实现“活跃时订阅不活跃时断开”的效果。addSource 的核心流程是这样的简化版public S void addSource(LiveDataS source, Observer? super S onChanged) { SourceS e new Source(source, onChanged); mSources.putIfAbsent(source, e); if (hasActiveObservers()) { e.plug(); } }Source 内部会调用source.observeForever(mObserver)来订阅上游数据这个 observeForever 是永活观察者上游数据一变Source 就收到回调然后在回调里调用setValue()把新数据透传给下游 MediatorLiveData 的观察者。关键点是插拔时机只有 MediatorLiveData 本身有活跃观察者时Source 才会真正插上去订阅上游当 MediatorLiveData 从活跃变不活跃时所有 Source 都会被拔掉取消对上游的订阅。这个设计的价值在于避免后台状态下还持续接收上游数据更新浪费资源。5.2 Transformations.map 的源码真相Transformations.map 和 switchMap 是建立在 MediatorLiveData 之上的语法糖。map 的内部实现几乎是一目了然的public static X, Y LiveDataY map(NonNull LiveDataX source, NonNull final FunctionX, Y mapFunction) { final MediatorLiveDataY result new MediatorLiveData(); result.addSource(source, x - result.setValue(mapFunction.apply(x))); return result; }所以 map 的本质就是创建一个 MediatorLiveData 作为输出然后 addSource 订阅上游数据上游每次变化时把数据经过 mapFunction 转换后 setValue 到输出。switchMap 的原理类似但区别在于它返回的是一个 LiveData而不是直接转换值。源码核心片段result.addSource(source, x - { LiveDataY newLiveData switchMapFunction.apply(x); result.setValue(newLiveData.getValue()); });它在回调里先通过 switchMapFunction 得到一个新的 LiveData然后取这个新 LiveData 的当前值 setValue 到输出。也就是说 switchMap 是“根据上游的值切换数据源”比 map 更适合处理依赖不同 LiveData 源的场景。理解了 MediatorLiveData 的原理这两个转换工具就不再是黑盒了。5.3 observeForever 的正确打开方式observeForever 是很多人用得比较随意的 API但它背后有陷阱。它的实现是public void observeForever(NonNull Observer? super T observer) { observe(AlwaysActiveObserver, observer); }注意第二个参数是AlwaysActiveObserver它重写了 shouldBeActive 方法永远返回 true。也就是说通过 observeForever 注册的观察者不会跟随任何生命周期只要 LiveData 里的数据一变它就会收到回调哪怕页面已经销毁。这个 API 的典型使用场景是单例仓库对象持有一个 LiveData而你需要在一个不受生命周期影响的地方观察它比如后台服务、Application 级别的数据同步逻辑。但必须在合适的时候调用 removeObserver否则会造成内存泄漏或者多余的回调。最稳妥的做法是在 onDestroy 或者不再需要接收数据时调用同一 Observer 对象的 removeObserver。如果你写的是 observeForever 又要做页面数据展示我建议再加一次“活跃标记”自行判断否则很容易出现销毁后仍然执行 UI 更新的崩溃。这个 API 不是不能用而是要非常克制地用每次使用前先问自己真的无法使用 observe 吗6. 实战中的坑与排查方法6.1 数据不回调的排查清单排查 LiveData 数据不回调的问题我建议按这个顺序来定位检查是否在子线程调用了 setValue如果抛异常说明线程违规用 postValue 替代。检查观察者所在 LifecycleOwner 的状态如果页面当前处于 STOPPED比如被完全遮挡数据不会立刻回调这是正常行为等恢复到 STARTED 状态会自动补发。检查是否用了 observeForever 但数据没变化因为 observeForever 没有生命周期限制只要 setValue 就会回调没回调说明注册时机有问题。检查是否是同一个 Observer 对象注册了不同 LifecycleOwnerLiveData 会抛 IllegalArgumentException如果 catch 掉了可能出现一个观察者失效的情况。最后再看版本号如果自行实现过 ObserverWrapper 或者用了第三方增强库优先怀疑 mLastVersion 是否被错误更新。我在项目里遇到最诡异的一类问题是observe 了正确的 LiveData但数据来源在多线程里用的是 postValue而主线程 UI 还没恢复时LiveData 已经把值合并丢了。这种情况其实不是 LiveData 的 Bug而是 postValue 值合并的副作用需要评估业务是否需要每个值都展示。6.2 数据多次回调问题分析和“不回调”对应的是“重复回调”。这里有几个常见原因第一个是注册了多个观察者没注意去重。比如在 BaseActivity 的 onCreate 里和某个 Fragment 里都 observe 了同一个 LiveData且传入了不同的 Observer这两个观察者都会收到回调这是符合预期的但不一定是业务想要的。排查时先看共有几个地方注册了 observer。第二个是生命周期重复进入活跃状态导致补发。LiveData 设计上允许在每次由非活跃变为活跃时把版本落后的数据补发给观察者。如果一个页面频繁在前后台切换你会看到观察者反复收到同一条数据。解决方案是在业务层做去重不能指望 LiveData 自己只回调一次。第三个是 SafeIterableMap 的遍历特性。它在分发过程中允许添加新观察者且新观察者如果正好在分发循环中被加入可能在同一轮内就被遍历到并收到数据造成“刚注册就回调”的现象。这个不是并发问题而是设计如此需要留意注册时机。6.3 常用改进方案与事件封装思路如果你要用 LiveData 做纯事件我最推荐的还是 Event 包装类。核心思路是给事件加一个“已消费”标记并且只有在未被消费时才会真正回调。一个极简实现open class Eventout T(private val content: T) { private var hasBeenHandled false fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled true content } } }使用时观察到的 T 是 Event 回调里调用 getContentIfNotHandled()如果返回 null 说明这个事件已经被消费过直接忽略。这种方法能够解决大多数粘性事件带来的重复消费问题而且不需要反射升级 androidx 版本也不会出兼容问题。如果你项目已经全面切到 Kotlin 协程也可以考虑在纯事件场景用 SharedFlow 替代 LiveData把 replay 设为 0从根源上避免粘性。不过这会引入架构组件的切换成本一般项目不必为了事件场景全面推翻 LiveData。组合使用的思路是UI 状态用 LiveData一次性事件用 SharedFlow 或者 Event 包装类各取所长。最后分享一点实战体会看 LiveData 源码最值钱的收获不是背下几个方法而是理解它那套“版本号 生命周期 安全迭代”的架构思想。这之后再看其他观察者框架基本一眼就能抓重点。我自己在项目里如果把 LiveData 和协程 Flow 做对比简单诉求还是优先 LiveData因为它和 Lifecycle 的绑定是系统级的省心。Flow 的强大之处体现在复杂数据流和背压场景这两个工具不见得非此即彼完全可以在一个项目里共存。关键是每次用之前想清楚这个数据是需要跟随界面生命周期的状态还是一次性的临时事件想清楚了很多坑自然就绕开了。