LiveData原理剖析:版本号机制与生命周期感知全解析 Jetpack 里的 LiveData 大家天天用但它可能是被误读最多的一个组件表面是观察者模式底层却藏了一套版本号加生命周期状态机的联动机制。上一篇把用法和常见坑整完了这一篇直接对照源码讲原理把 setValue、postValue、observe 背后发生的事一次性说清楚。适合已经写过一段时间 LiveData、想从会用走向会排查的 Android 开发者也适合准备面试时被问到LiveData 为什么不会泄漏这类问题的同学。1. 从一行observe开始LiveData内部到底维护了什么东西很多同学第一天用 LiveData 时感受是真香但没想过为什么香。我们先回到最常用的一段代码private val status MutableLiveDataBoolean() status.observe(this, Observer { ready - updateButton(ready) }) status.value true运行结果大家都很熟setValue(true) 之后观察者被回调Activity 销毁后不会崩从后台回来如果值又被改过页面恢复时也能拿到最新状态。但这段代码背后其实有一条完整的内部链路setValue把值写进内存并递增一个版本号观察者对象被包装成新的内部节点注册到成员变量mObservers里当数据变化时LiveData 会遍历所有观察者包装节点逐个判断这个观察者当前是否活跃、它上次收到的版本号是否比全局版本号旧条件全部满足才会真正回调用户代码。LiveData 的核心成员变量基本就是下面这些把它们记熟后面的原理全都能串起来private volatile int mVersion; private final Object mDataLock new Object(); private volatile Object mData NOT_SET; private volatile Object mPendingData NOT_SET; private boolean mDispatchingValue false; private boolean mDispatchInvalidated false; private int mActiveCount 0; private final SafeIterableMapObserver? super T, ObserverWrapper mObservers ...mVersion是全局节拍每次 setValue 都会自增mData是当前数据初始 NOT_SET 表示还没有值mPendingData是 postValue 在后台线程暂存、等待主线程结算的数据mDispatchingValue和mDispatchInvalidated组成派发过程中的重入保护mActiveCount统计活跃观察者数量用于触发 onActive/onInactivemObservers则是整个订阅关系的核心容器保存着用户观察者到内部包装对象的映射。容器选择 SafeIterableMap 而不是 HashMap这一点特别值得展开。LiveData 在遍历观察者派发数据时回调里完全可能再次对同一个 LiveData 做removeObserver或observe。普通 Map 在遍历过程中被结构性修改十有八九要抛ConcurrentModificationException。SafeIterableMap 的实现思路类似一个支持安全边遍历边修改的链表专门为遍历中变更这种场景设计。这也解释了为什么你会看到一些看起来有点怪但合法的用法在 onChanged 里收到一次值后立刻 removeObserver把自己从订阅里摘掉实现只消费一次的效果。理解这套成员变量之后后面所有源码都只是回答三个问题数据怎么存、什么时候该通知谁、通知之前要做什么过滤。带着这三个问题去读源码会比直接看方法调用轻松很多。2. mVersionLiveData的版本号机制粘性事件和重复回调都从这来先从 setValue 的源码开始MainThread protected void setValue(T value) { assertMainThread(setValue); mVersion; mData value; dispatchingValue(null); }assertMainThread保证 setValue 只能在主线程调用mVersion给数据打上新版本号mData value写入内存dispatchingValue(null)开始派发。注意这里传的是 null意思是通知所有观察者如果传具体某个包装对象就只通知那一个观察者。派发过程中最关键的是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); }三个判断分别回答了三个问题第一个问题观察者当前活跃吗mActive为 false说明生命周期不在前台LiveData 不会回调这个观察者。第二个问题生命周期状态真的适合回调吗shouldBeActive()返回 false 时说明主人已经退到后台LiveData 会顺手把 observer 切到 inactive避免后续继续无意义派发。第三个问题这个观察者已经消费到哪个版本了如果mLastVersion mVersion说明这条数据它已经见过了不需要再通知。这个设计带来一个很多新手误解的结论LiveData 不会因为两次 setValue 的值相同就去重。比如连续执行liveData.value hello liveData.value hello第一次版本从 -1 变 0第二次从 0 变 1两次都会触发回调。版本机制保证的是有序和最新不是与旧值不同。想实现值去重得自己在 setValue 之前做比较或者用distinctUntilChanged这类组合操作。粘性事件也完全源于版本号。新 observer 包装对象的mLastVersion初始值是START_VERSION -1。如果 LiveData 当前已经 setValue 过若干次mVersion肯定大于等于 0那么一旦considerNotify被触发-1 当前版本条件成立新观察者就会立刻收到一个早于注册时间的数据。这就是后注册的观察者也会收到注册前的最新值的底层原因。同理Activity 从后台回到前台时LiveData 会把恢复活跃的那个观察者单独派发一次代码里对应的是dispatchingValue(this)。如果这个观察者上次消费版本低于当前版本就会收到最新值。这不是玄学是版本比对后的补发。实际项目中粘性事件的坑往往出现在事件型数据上。比如用一个MutableLiveDataBoolean表示操作成功页面还没创建时就 set 过 true之后才 observe观察者一进来就收到 true导致页面做出一次并非用户触发的弹窗。原理清楚了你就知道单纯把 value 清空不可靠还是应该把事件包一层专用结构或者做版本差异化处理。3. setValue与postValue主线程约束、后值覆盖和消息合并setValue 的第一行代码是assertMainThread(setValue)它内部通过ArchTaskExecutor.getInstance().isMainThread()判断当前线程。不在主线程直接抛IllegalStateException: Cannot invoke setValue on a background thread。为什么 LiveData 要这么强硬因为它的观察者回调默认跑在主线程生命周期状态也只允许在主线程变化。如果在子线程直接同步回调且不经过 Handler 队列就会出现两个线程同时操作mVersion、mActiveCount、mData这些状态成员的竞态。为了保持实现简单可靠官方就把 setValue 钉死在主线程后台线程只能走 postValue。postValue 的实现看起来短信息量却很大protected void postValue(T value) { boolean postToMainThread; synchronized (mDataLock) { postToMainThread mPendingData NOT_SET; mPendingData value; } if (!postToMainThread) { return; } ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); }mPostValueRunnable 内部大概是这样的mPostValueRunnable () - { Object newValue; synchronized (mDataLock) { newValue mPendingData; mPendingData NOT_SET; } setValue((T) newValue); };mPendingData由mDataLock保护任何线程都能安全写入。它只保留最后一次 post 进来的值。关键技巧在postToMainThread这个标记上如果已经有值挂起、正在等待主线程结算再次 post 只更新 pending 数据不再重复向主线程投递任务。也就是说只有第一次 post 会真正往主线程队列里放一个 runnable后续 post 都只是在合并。这就决定了 postValue 的几个重要行为连续多次 postValue中间值会被后值覆盖。比如主线程繁忙时快速执行postValue(1); postValue(2); postValue(3)最后观察者很可能只收到 3。postValue 也不能保证后台线程调用顺序和 UI 回调顺序完全一致因为它只是把取最后一个待结算值的任务投递到主线程。如果你在后台线程做了耗时计算最后把结果 postValue 回来这是标准做法但想用 postValue 连续推进进度条就要斟酌了它天然会丢中间进度点。我在真实项目里的做法是后台线程负责计算主线程拿到最终结果后直接 setValue。需要展示过程状态时不要用同一个 LiveData 高频 post 中间值而是把中间状态合并成一个携带当前进度和阶段的结构一次 post 出去UI 层收到后自己处理动画过渡。这样既避开 postValue 的丢值问题又不牺牲流畅度。还有一个容易忽略的细节setValue 因为被钉死主线程数据写入不需要额外加锁主线程本身就是天然的串行点。而 postValue 必须加锁因为它可能从任意一个子线程进来。两者的线程模型完全不同这也能解释为什么你在子线程调用 setValue 会立刻炸、调用 postValue 却很安静。4. 观察者的三层封装observe到底把谁放进了容器observe 调用之后第一件事并不是立刻注册而是做一层很重的封装。看源码MainThread public void observe(NonNull LifecycleOwner owner, NonNull Observer? super T observer) { assertMainThread(observe); if (owner.getLifecycle().getCurrentState() DESTROYED) { return; } 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 with different lifecycles); } if (existing ! null) { return; } owner.getLifecycle().addObserver(wrapper); }有几个点值得细看。如果 owner 已经处于 DESTROYEDobserve 直接返回因为注册一个已经销毁页面的观察者没有任何意义。之后putIfAbsent保证同一个 observer 对象不会重复放进容器。如果同一个 observer 曾经绑定过另一个 LifecycleOwner会抛IllegalArgumentException提示 Cannot add the same observer with different lifecycles。这是很多同学在 Fragment 和 Activity 之间混用同一个观察者时踩到的坑。包装完之后LiveData 把 LifecycleBoundObserver 作为 LifecycleEventObserver 注册到owner.getLifecycle()。然后生命周期状态机每次变化都会回调这个内部观察者的onStateChanged。LifecycleBoundObserver 的shouldBeActive()实现只有一个判断boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); }也就是说生命周期处于 STARTED 或 RESUMED 时观察者活跃处于 INITIALIZED 或 CREATED 时不活跃。这样设计是有讲究的Activity 在 onStart 之后才需要更新 UIonStop 之后已经不可见自然没必要再让 LiveData 反复回调 UI。onStateChanged 的处理更精妙Override public void onStateChanged(NonNull LifecycleOwner source, NonNull Lifecycle.Event event) { if (source.getLifecycle().getCurrentState() DESTROYED) { removeObserver(mObserver); return; } activeStateChanged(shouldBeActive()); }一旦页面销毁LiveData 立刻从自己的观察者容器里移除这个内部包装完成双向解绑。而activeStateChanged负责切换 observer 的 active 状态并维护 mActiveCountvoid activeStateChanged(boolean newActive) { if (newActive mActive) { return; } mActive newActive; boolean wasInactive mActiveCount 0; mActiveCount newActive ? 1 : -1; if (wasInactive newActive) { onActive(); } if (mActiveCount 0 !newActive) { onInactive(); } if (newActive) { dispatchingValue(this); } }mActiveCount 从 0 变 1 时触发onActive()从 1 变 0 时触发onInactive()。这两个回调可以用来控制数据源开关。例如自定义 LiveData 时可以在 onActive 里启动网络监听或定时任务在 onInactive 时停止避免页面都退到后台了还在做无谓消耗。由于 LiveData 维护的是是否还有至少一个活跃观察者多个页面同时 observe 同一个 LiveData 时也不会重复开关上游。observeForever用的是另一种包装AlwaysActiveObserver。它的shouldBeActive()恒为 true不随任何生命周期变化也不会自己解绑。所以用了observeForever的观察者必须手动removeObserver否则就是内存泄漏的标准姿势。5. 生命周期感知的真相自动移除是如何发生的很多文章说 LiveData 有生命周期感知能力但没说清楚感知的机制。其实它几乎没有自己判断生命周期所有状态变化都来自LifecycleOwner。observe(owner, observer)传入的 owner 本身实现了 LifecycleOwner它的lifecycle是一台 LifecycleRegistry 状态机。LiveData 要做的就是往这台状态机上注册一个 LifecycleEventObserver。状态机每次转换比如 Activity 执行 onStart、onStop、onDestroy都会通知所有注册的 LifecycleEventObserver。LiveData 在这个通知里只做两件事非 DESTROYED 时根据当前状态刷新内部观察者的 active 状态DESTROYED 时把自己从观察者容器里移除。这就说明了为什么 observe 里必须传 LifecycleOwner 而不是直接传 Activity 对象。LifecycleOwner 提供的是一个标准、统一、可被测的状态来源让 LiveData 不必关心具体是谁在观察。而且这个设计也避免了开发者在 onDestroy 里手动 removeObserver 时最容易出现的遗漏问题手动解除订阅不是不可以但很容易漏漏一次就泄漏一次。配置变更场景则完全是另一套逻辑。Activity 因为旋转等原因销毁重建时DESTROYED 照样会触发LiveData 会把旧页面的观察者移除。但数据本身放在 ViewModel 里ViewModel 并不随着 Activity 重建而丢弃。所以当新的 Activity 重新 observe 同一个 LiveData 时mVersion 依然延续旧的进度新观察者的 mLastVersion 是 -1会立即拿到当前最新数据。这套配合正是 LiveData 能在页面重建后自动恢复数据、还不用手动恢复的原因。内存泄漏的根源本质上是长生命周期对象持有了短生命周期对象的引用。LiveData 做对的事情就是让观察关系不越过生命周期的边界页面销毁观察关系解除页面重建观察关系重新建立。反观 observeForever因为绕过了生命周期等于主动放弃了这条边界才成为泄漏高发点。这里可以列一个简单对比表方便记忆注册方式包装类型生命周期感知自动移除典型风险observe(owner, observer)LifecycleBoundObserver有DESTROYED 时自动移除较安全observeForever(observer)AlwaysActiveObserver无不自动移除必须手动 removeObserverremoveObserver(observer)无无手动操作漏调用则残留如果还是怕泄漏可以养成一个习惯在页面 onDestroy 或 onCleared 里统一检查还有没有没清理的 observeForever。不过在多数业务场景里优先用 observe 就够用了。6. 从原理反推出来的实操法则和扩展思路最后把这套原理放到真实开发中验证。第一个高频坑在子线程用同一个 LiveData 连续 postValue 更新 UI 状态。初学时常想当然地认为 postValue 会把每一次数据都按顺序送到主线程实际它只会保留最后一次。所以正确姿势是把中间过程的好几个状态合并成一个快照结构一次 post 出去UI 收到后自己处理中间态。第二个坑observeForever 用完不清理。源码层面它就是一个永不失效的观察者。我一度在某个工具类里对全局数据做 observeForever页面退出后仍然收到回调。排查后才发现那个工具类持有的页面引用虽然是弱引用但观察者本身一直被 LiveData 容器强持有。后来统一改成在页面 onStop 里主动调用 removeObserver或者干脆换成带生命周期的 observe。第三个坑把事件型数据硬塞进 LiveData。粘性事件不是 bug是设计。但在登录、弹窗、跳转这类一次性事件上直接用 LiveData就会频繁遇到页面刚打开就弹出上一次残留事件的情况。基于原理我的做法是UI 状态用普通 LiveData一次性事件单独用一个带事件标记的包装数据消费后手动清空或者用 MediatorLiveData 做二次分发。MediatorLiveData 是 LiveData 家族里最有扩展价值的一个类。它内部把多个 LiveData 作为 source在源数据变化时回调你写的合并逻辑再通过 setValue 把结果抛给下游。所有 source 的订阅回收都封装在 onActive/onInactive 中这让组合数据源的开关成本很低。比如val result MediatorLiveDataString() result.addSource(repoData) { data - result.value mapToUiModel(data) }repoData 每次变化都会经过 map 映射再给 result 赋值。因为 result 本身也是 LiveData它的观察者照常走版本号校验粘性和自动清理机制也一并生效。还可以基于这套机制自己写扩展。比如一个简单的防抖 LiveData就是重写 setValue把真实的 set 延后到去抖窗口结束后再执行class DebounceLiveDataT( private val delayMs: Long ) : MutableLiveDataT() { private val handler Handler(Looper.getMainLooper()) private var pendingValue: T? null private val debounceRunnable Runnable { pendingValue?.let { super.setValue(it) } pendingValue null } override fun setValue(value: T) { pendingValue value handler.removeCallbacks(debounceRunnable) handler.postDelayed(debounceRunnable, delayMs) } }虽然只有几十行但因为完全基于 LiveData 的派发模型它的观察者照旧享受生命周期感知主线程回调也照旧成立。这就是理解原理带来的自由度LiveData 并不是一套不能动的黑盒它把数据存储、版本过滤、生命周期状态拆得很开你在任意一环都可以插入自己的逻辑。我在项目里试过给 LiveData 加 distinct 去重、加超时清理、加双写合并核心都只是动版本号和 setValue 时机。你只要把 mVersion、mPendingData、mActiveCount 这几个角色记清楚遇到 LiveData 行为异常基本都能在源码里定位到原因。这也正是花时间啃原理最值得的地方。