☰
LiveData原理深挖:版本号机制、生命周期与线程切换全解析
2026/10/7 10:58:47 网站建设 项目流程

熟悉我博客的老读者应该记得,之前写过一篇 LiveData 的基础用法,从那篇的评论区就能看出来,很多朋友的困惑不在“怎么用”,而在“为什么是这样”。比如为什么子线程不能直接 setValue?为什么新注册的观察者会立刻收到一条旧数据?为什么连续两次 postValue 最后只会收到最后一个?这些问题,只用 API 是解释不了的,必须把源码翻出来看。

这一篇算是我自己源码阅读的笔记,也是系列第二篇,重点就一个:LiveData 的原理。我尽量不贴大段源码,而是把 LiveData 里最核心的几条链路——数据存储、观察者管理、生命周期绑定、线程切换、版本号比较——一条条拆开讲。读完你再去应付面试里关于 LiveData 原理的追问,或者排查项目里粘性事件、数据丢失这类问题,会明显有底气。

1. LiveData 整体设计:源码里最关键的三个成员变量

LiveData 类本身不大,源码加注释也就四五百行,核心逻辑非常集中。打开源码先别慌,就盯三个成员变量看:mData、mVersion、mObservers。整个 LiveData 的设计,说白了就是围绕这三个东西转的。

1.1 mData 和 mVersion:数据和版本号的“绑定关系”

先看这两行:

private volatile Object mData; private int mVersion;

mData就是当前保存的数据值。注意它是volatile修饰的,为什么?因为在postValue的场景里,后台线程写入、主线程读取,volatile保证读线程能立刻看到最新写入,避免指令重排带来的可见性问题。这一点是 LiveData 线程安全设计的第一道防线。

mVersion是版本号,初始值是START_VERSION,也就是 -1。每调用一次setValue,mVersion就加 1。你可以把版本号理解成一个“数据更新次数计数器”,它不关心数据值本身变化没变化,只关心 setValue 被调用了多少次。哪怕连续 setValue 同一个值,版本号照样递增。

这个设计为什么重要?因为 LiveData 判断“要不要通知观察者”,靠的不是比较数据值是否相等,而是比较版本号。观察者内部也保存了一个mLastVersion,初始同样是 -1。通知的条件就是:observer.mLastVersion < mVersion。

这里面有个隐藏逻辑:第一次给 LiveData setValue 时,mVersion 从 -1 变 0;但此时观察者的 mLastVersion 还是 -1,于是观察者会收到一次回调。这就是“新注册观察者立刻收到旧数据”的根源,也就是社区常说的粘性事件。这个机制具体怎么运作,我在第 4 章单独展开。

1.2 SafeIterableMap:观察者容器为什么不用 HashMap

mObservers的类型是SafeIterableMap<Observer<? super T>, ObserverWrapper>,这不是普通的 HashMap,而是 AndroidX 里一个专用数据结构,主要解决一个痛点:在遍历通知观察者时,如果观察者中途注册了新的观察者,普通容器会抛ConcurrentModificationException,SafeIterableMap 可以容忍这种并发修改。

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

它的底层是双向链表加 HashMap 索引,支持在遍历过程中安全地插入和删除元素。LiveData 遍历它用的是iteratorWithAdditions(),这个方法允许遍历过程中新增的节点也“尽量”被访问到,不会因为集合被修改而崩溃。

实际项目中,我们很少直接感知到这个容器,但它保证了observe回调里再次调用observe不会炸,这在复杂页面里是真实存在的场景。我记得之前有项目在 LiveData 回调里动态注册另一个观察者,如果用普通 List 早就崩了。

1.3 ObserverWrapper 三层结构:观察者如何被“套壳”

LiveData 真正管理的不是外部传入的Observer,而是它的内部包装类ObserverWrapper。ObserverWrapper 又派生出两个子类:

包装类绑定对象shouldBeActive 逻辑
LifecycleBoundObserverLifecycleOwner持有 Lifecycle,状态 >= STARTED 才算活跃
AlwaysActiveObserver无永远返回 true,相当于不感知生命周期

observe(owner, observer)用的是第一种,observeForever(observer)用第二种。所以observeForever才需要手动调用removeObserver,因为没有一个 Lifecycle 帮你在页面销毁时自动解绑。

ObserverWrapper 里还有一个mActive字段,记录当前观察者是否处于活跃状态。mActive为 true 且版本号满足条件时,才会真正回调到外部。这两个条件缺一不可。

所以 LiveData 的整体设计可以概括成:一个用版本号管理数据的容器,一套能感知生命周期的观察者包装器,一个能安全并发修改的观察者集合。接下来看这套机制在生命周期绑定上是怎么转起来的。

2. 生命周期绑定链路:从 observe() 到自动收放

LiveData 最核心的卖点就是“自动感知生命周期,页面不可见时不回调”。这层能力不是 LiveData 自己实现的,而是建立在 Lifecycle 组件之上的。每一次observe,背后都藏着一次完整的注册和绑定过程。

2.1 observe() 里发生了什么:DESTROYED 状态直接返回

先看observe的简化逻辑:

public void observe(LifecycleOwner owner, Observer<? super T> observer) { if (owner.getLifecycle().getCurrentState() == DESTROYED) { return; } LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing != null && existing != wrapper) { return; } owner.getLifecycle().addObserver(wrapper); wrapper.activeStateChanged(true); }

有个细节值得注意:如果页面已经 DESTROYED,observe会直接静默返回,不注册、不回调。这意味着你在 onDestroy 之后调 observe 是无效的,这算是一个隐形的保护。紧接着,LiveData 用putIfAbsent把包装对象放进去,如果这个 Observer 之前已经注册过(哪怕绑定的 owner 不同),就直接忽略。

之后调用lifecycle.addObserver(wrapper),让包装对象监听 LifecycleOwner 的状态变化。最后还有一个wrapper.activeStateChanged(true),这一步不是拍脑袋加的,它的作用是让 LiveData 马上根据当前生命周期状态计算一次“是否活跃”,如果当前页面已经处于 STARTED 以上,观察者立刻变成活跃状态,这就为后续立即回调旧数据做好了准备。

2.2 LifecycleRegistry 如何驱动状态回调

LifecycleBoundObserver继承了LifecycleEventObserver,它的onStateChanged很简单:

public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { Lifecycle.State currentState = source.getLifecycle().getCurrentState(); if (currentState == DESTROYED) { lifecycle.removeObserver(this); return; } ensureActiveState(shouldBeActive()); }

shouldBeActive()比较的是当前状态是否至少是 STARTED。这句话翻译成人话就是:Activity 走到 onStart 之后,观察者才“活跃”;退到 onStop 之后,观察者就“不活跃”了。

那这些状态是谁通知的?答案是LifecycleRegistry。它是 Lifecycle 的默认实现,内部有一个mState表示当前状态,还有一个mObserverMap保存所有观察者。状态变化时,LifecycleRegistry 会遍历观察者,逐个派发同步好的事件。LiveData 的包装对象收到事件后,再决定自己是激活还是休眠。

重点理解:状态迁移是同步逐级发生的,不会直接从 STARTED 跳到 DESTROYED 而漏掉中间状态。这点保证了下述活/休眠逻辑的可靠性。

2.3 activeStateChanged 与观察者“冷启动”

activeStateChanged是观察者状态变化的入口:

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();从 1 变 0 时,触发onInactive()。onActive/onInactive是 protected 方法,真正有实际意义的是子类MediatorLiveData,它靠这两个回调去管理上游数据源的开关。

最后一句if (mActive) dispatchingValue(this)是让这个观察者马上参与一次数据分发。实时场景:页面从后台切回前台,onStart 触发,观察者重新活跃,这时如果 LiveData 里已经存有最新数据,会立刻再收到一次回调。

所以你会发现一个现象:页面从后台回到前台,有时候 LiveData 会重复回调一次。这不是 bug,是“活跃时立即同步最新值”的设计。要处理这种情况,通常是在回调里做去重判断,或者改用 Flow 的distinctUntilChanged思路自己封装一层。

3. setValue/postValue 数据刷新链路:主线程与后台线程的正确姿势

数据更新最终都会落到 setValue,区别只在于 postValue 多了一个线程切换的中间步骤。我先讲 setValue,再讲 postValue 的切换细节,这两条链路的差异是面试的高频考点。

3.1 setValue:主线程更新的执行路径

@MainThread protected void setValue(T value) { assertMainThread("setValue"); mVersion++; mData = value; dispatchingValue(null); }

三句话,每句都有讲究。assertMainThread是主线程校验,不是主线程直接抛异常,这解释了“为什么子线程 setValue 会崩”。mVersion++是版本号递增,mData = value更新数据。

注意执行顺序不能换:先版本号自增,再写入数据。因为后面通知观察者时,观察者看到的是“新版本号 + 新数据”,保证读到的数据和通知到的版本是对应的。如果先写数据再改版本,极端情况下观察者可能用旧版本号读到新数据,逻辑就乱了。

3.2 dispatchingValue 遍历:mDispatchInvalidated 的作用

dispatchingValue是数据分发的核心:

void dispatchingValue(ObserverWrapper initiator) { if (mDispatchingValue) { mDispatchInvalidated = true; return; } mDispatchingValue = true; do { mDispatchInvalidated = false; if (initiator != null) { considerNotify(initiator); initiator = null; } else { for (Iterator<...> iterator = mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { considerNotify(iterator.next().getValue()); if (mDispatchInvalidated) { break; } } } } while (mDispatchInvalidated); mDispatchingValue = false; }

初看有点绕,其实是在解决“回调过程中再次触发数据更新”的并发重入问题。mDispatchingValue像是分发锁,如果分发过程中又来了新的 setValue,不会立刻重新遍历,而是把mDispatchInvalidated置为 true,表示“刚才那轮分发作废,需要再来一轮”。

这个设计保证了所有观察者最终能看到最新数据,但你也要知道:一个观察者可能在一轮分发里被多次回调。比如回调里调了setValue,被 invalidation 打断后,外层循环会重新触发。所以不要在 LiveData 回调里无脑 setValue,容易造成循环刷新,甚至肉眼可见的数据震荡。

considerNotify才是真正决定“要不要回调外部 Observer”的地方:

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); }

先判断观察者是否活跃、是否满足生命周期状态,再比较版本号,三项都通过才回调。这套判断保证了:主线程 setValue 后,即使遍历到休眠中的观察者,也不会触发回调。

3.3 postValue 的线程切换:锁、NOT_SET 与丢失更新

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是待提交的数据,初始值是NOT_SET,这是一个永远不会被业务数据撞上的私有对象标记。mDataLock保证了多线程同时调用 postValue 时,对mPendingData的读写是互斥的。

一旦发现 mPendingData 不是 NOT_SET,说明主线程的投递任务已经在队列里了,这次 postValue 就不再重复投递,直接返回。这意味着什么?如果在一次投递任务执行前连续 postValue 三次,最终只会保留最后一个值,中途的数据被覆盖丢弃。

再看主线程上执行的 Runnable:

private final Runnable mPostValueRunnable = new Runnable() { @Override public void run() { Object newValue; synchronized (mDataLock) { newValue = mPendingData; mPendingData = NOT_SET; } setValue((T) newValue); } };

Runnable 先从锁里取出 mPendingData,把 mPendingData 重置为 NOT_SET,然后调用 setValue,走主线程的完整分发链路。锁的作用是保证 Runnabler 取到的值是最新且完整的,避免读到写了一半的数据。

这个机制里有几个容易踩坑的细节,我在第 5 章会结合实战场景展开。先把版本号这一块说完,因为它解释了 LiveData 最著名的“粘性事件”。

4. 版本号机制:粘性事件的前世今生与绕开技巧

版本号是 LiveData 原理里最值得花时间理解的部分,因为它能解释很多“反直觉”的现象。数据值一样、setValue 没调用、甚至刚注册,都会触发回调,这些现象全都能用版本号推出来。

4.1 版本号比较规则与 observer 初始化

先列一个对比表,把版本号的变化过程理清楚:

节点LiveData mVersion观察者 mLastVersion是否会回调
初始化-1-1否
setValue 第一次0-1是
setValue 第二次10是
新观察者注册后1-1是(立刻收到旧数据)
已对账的观察者11否(后续数据到才回调)

结论很清晰:观察者的mLastVersion是“我已消费到的版本”,LiveData 的mVersion是“目前最新的版本”。只要前者小于后者,就说明有没消费过的数据,于是触发一次同步。

4.2 粘性事件的成因:旧版本号被新观察者继承

“粘性事件”的官方定义是:先发事件,后注册观察者,新观察者能收到之前的事件。套用版本号机制,原因也简单:

观察者注册时,它的mLastVersion是初始值 -1。如果 LiveData 之前已经 setValue 过两次,mVersion 是 1。那么新观察者的 -1 小于 1,就立刻走一次回调,把当前 mData 发出去。

这个机制是 Google 有意为之,用来保证“界面恢复时能拿到最新数据”。但在实际项目里,它经常变成麻烦:比如登录成功后发一个事件,页面跳转后注册观察者,结果触发了一个不该执行的逻辑(比如重复弹 toast、重复跳转)。社区里管这个叫粘性事件问题,本质上就是版本号机制带来的副作用。

4.3 反射改写 mLastVersion 实现非粘性封装

绕开粘性事件,常规做法有 EventWrapper 包装、SingleLiveEvent 等。如果不想改动架构,也可以从版本号机制本身下手:让新注册的观察者“对齐”当前版本号,不触发旧数据回调。

在不改动 LiveData 源码的情况下,可以反射操作:

public static <T> void observeWithoutSticky(LiveData<T> liveData, LifecycleOwner owner, Observer<T> observer) { try { Field observersField = LiveData.class.getDeclaredField("mObservers"); observersField.setAccessible(true); Object observers = observersField.get(liveData); Method getMethod = observers.getClass().getDeclaredMethod("get", Object.class); getMethod.setAccessible(true); Object wrapper = getMethod.invoke(observers, observer); Field lastVersionField; if (wrapper != null) { lastVersionField = wrapper.getClass().getSuperclass().getDeclaredField("mLastVersion"); lastVersionField.setAccessible(true); Field versionField = LiveData.class.getDeclaredField("mVersion"); versionField.setAccessible(true); lastVersionField.setInt(wrapper, versionField.getInt(liveData)); } } catch (Exception ignored) { } liveData.observe(owner, observer); }

这段代码先注册观察者,再通过反射拿到它的包装对象,把 mLastVersion 强行改成和 LiveData 当前 mVersion 一样,这样第一轮 considerNotify 就会因为版本号相等而跳过。

但这只是“黑科技”,要特别注意版本兼容。AndroidX 源码一旦调整字段名或类结构,反射就失效了。我更推荐的做法是在基类里统一封装:用一个非粘性 observe 方法,内部通过 MediatorLiveData 或 Event 包装处理,不让业务代码直接接触裸 LiveData。反射适合临时救火,不适合写进核心库长期维护。

5. 原理之外的实战避坑:5 个高频问题实录

原理看得再多,不落到实际项目里都是虚的。我把这些年排查过的 LiveData 相关问题按频率排了个序,挑出 5 个最有代表性的,直接给结论和解决方案。

5.1 连续 postValue 只收到最后一个数据

这个坑其实是 postValue 的“丢中间值”设计导致的。不断有朋友反馈:子线程里循环 postValue 10 次,主线程只收到最后一次回调。

原因回看 postValue 逻辑:只要主线程队列里已经有一个 Runnable 在等待,后续所有 postValue 都只会更新 mPendingData,不会重复投递。等 Runnable 执行时,mPendingData 只剩最后一次写入的值,前面的值全被覆盖了。

要注意的是,这个行为和 setValue 不一样:setValue 走的是同步更新,值不会丢。所以如果业务对每一次数据更新都敏感,比如进度条,就不能无脑 postValue,建议改成:

if (Looper.myLooper() == Looper.getMainLooper()) { liveData.setValue(progress); } else { liveData.postValue(progress); }

这个判断既能避免子线程 setValue 崩溃,又能让主线程更新走不丢失数据的路径。虽然 LiveData 内部 postValue 本来就会切主线程,但“是否需要保留每一次更新”的取舍权应该明确握在自己手里。

还有一个进阶坑:先 postValue 后 setValue,顺序可能反直觉。比如postValue(a)之后立刻setValue(b),由于 Runnable 还没执行,实际上是 b 先被分发,a 随后被 Runnable 里的 setValue 调起再分发,回调顺序是 b、a。如果对顺序敏感,一定不要混用这两套 API。

5.2 子线程 setValue 直接崩溃:正确姿势

这个报错信息很明确:

IllegalStateException: Cannot invoke setValue on a background thread

setValue 里的assertMainThread会在非主线程直接抛异常。原因不复杂:LiveData 的设计假设是,主线程上是单线程串行更新,不需要额外加锁。如果放开了子线程写,就必须在全链路加锁,性能会明显变差。所以 Google 直接做成了硬性限制。

子线程要更新就统一走postValue。但如果一个方法既可能主子线程调用、又可能子线程调用,那统一用 postValue 也不会错,无非是中间值可能被丢弃。关键看业务到底需不需要每一帧都展示,不需要就把这一节开头那个判断写法当作标准答案。

5.3 内存泄漏边界:observeForever 与手动清理

LiveData 本身设计上是不会泄漏 Activity 的,因为 LifecycleBoundObserver 在 DESTROYED 时会自动removeObserver。但代码里直接用了observeForever,就绕过了生命周期绑定,观察者会一直存活,必须手动 remove:

private final Observer<String> observer = value -> { ... }; @Override protected void onCreate(Bundle savedInstanceState) { viewModel.data.observeForever(observer); } @Override protected void onDestroy() { viewModel.data.removeObserver(observer); super.onDestroy(); }

注意 removeObserver 用同一个对象引用才有效,因为 LiveData 的 mObservers 是按 Observer 对象的 hashCode/equals 定位的。最稳妥的做法还是尽量用observe,让 LifecycleOwner 替我们管理解绑。

5.4 LifecycleOwner 状态异常导致不回调

有时候页面明明在前台,LiveData 就是不回调。排查路径一般是这样的:先确认LifecycleRegistry的状态是不是异常。常见原因包括:

  • Fragment 的 viewLifecycleOwner 在 onCreateView 之前使用,生命周期还没初始化到可观察状态。
  • 自定义 LifecycleOwner 没有正确执行handleLifecycleEvent,导致状态一直停在 INITIALIZED。
  • 在 onDestroy 之后调 observe,LiveData 直接忽略注册,没有回调,也没有错误日志。

针对第二点,自定义 LifecycleOwner 时需要确保生命周期状态切换被正确上报。我在项目里见过一个 View 层自定义 LifecycleOwner,忘了在 onDetachedFromWindow 时上报 DESTROYED,结果 LiveData 一直持有 View 引用,界面销毁后回调还在执行,最后靠内存快照才定位到问题。排查这类问题,建议先在onStateChanged打点,确认状态流转是否符合预期。

5.5 用 MediatorLiveData 合并数据源的注意点

MediatorLiveData 是 LiveData 的增强子类,它的存在意义是“我可以观察多个 LiveData 源,再统一向外分发”。原理上它靠onActive/onInactive去动态开关上游观察:

MediatorLiveData<String> mediator = new MediatorLiveData<>(); mediator.addSource(sourceA, value -> mediator.setValue(value)); mediator.addSource(sourceB, value -> mediator.setValue(value));

有几个细节值得注意。第一,addSource注册完成后,如果 sourceA 已有数据,会立刻把旧值转发到 mediator,这就是粘性事件的“传染”。想避免,要在 addSource 前先给上游做一个版本对齐,或者用 4.3 节的反射方案。

第二,不要忘记移除不需要的 source。如果你只在某一页面需要合并两个数据源,页面销毁时没有走 removeObserver,上游 source 还引用着 MediatorLiveData,容易出现“数据源泄漏”的假象。虽然 MediatorLiveData 作为 ViewModel 的一员,生命周期和 ViewModel 对齐,不会直接泄漏 Activity,但如果你在 Activity 里直接 new 了一个 MediatorLiveData 并且 addSource,就需要在 onDestroy 时主动removeSource清理。

第三,MediatorLiveData 的回调里直接 setValue 是安全的,因为它最终还是走 LiveData 的主线程校验,能保证数据在正确线程分发。但我见过回调里同时往两个数据源 setValue 的代码,看起来没什么毛病,实际上如果这两个数据源又被同一个 MediatorLiveData 观察,就会形成循环刷新,一跑起来 CPU 直接拉满。这种循环链要靠架构层面避免,不能靠 LiveData 本身的 mDispatchInvalidated 兜底。

最后分享一点我在项目里的实践心得

LiveData 这套源码说实话不算难读,但每次重读都会有新体会。它真正厉害的地方,是选择了一种极其克制的设计:不做跨线程复杂同步、不做数据相等性比较、不做事件过滤,而是用三个基础机制组合出“生命周期感知 + 数据驱动”的能力。这种“用简单机制组合出复杂能力”的思路,比堆功能更有价值。

在我自己的项目里,现在的习惯是:页面与 ViewModel 的通信默认用 LiveData,因为简单、稳定、没有学习成本;但涉及高频事件、一次性事件、或者需要做复杂数据流变换的场景,我会优先考虑 Flow,必要时用 Flow 转 LiveData 的桥接层接入 UI。LiveData 和 Flow 本身也不是对立关系,关键还是想清楚每种能力的边界在哪里。

如果你正要深入 Jetpack 源码,我建议从 LiveData 开始,比 Lifecycle、ViewModel 更容易上手。把版本号机制和 dispatchingValue 这两块吃透,很多 Jetpack 组件里的“为什么”都会迎刃而解。

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

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

立即咨询