ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

LiveData原理深挖:版本号机制、生命周期与线程切换全解析

LiveData原理深挖:版本号机制、生命周期与线程切换全解析 熟悉我博客的老读者应该记得之前写过一篇 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。每调用一次setValuemVersion就加 1。你可以把版本号理解成一个“数据更新次数计数器”它不关心数据值本身变化没变化只关心 setValue 被调用了多少次。哪怕连续 setValue 同一个值版本号照样递增。这个设计为什么重要因为 LiveData 判断“要不要通知观察者”靠的不是比较数据值是否相等而是比较版本号。观察者内部也保存了一个mLastVersion初始同样是 -1。通知的条件就是observer.mLastVersion mVersion。这里面有个隐藏逻辑第一次给 LiveData setValue 时mVersion 从 -1 变 0但此时观察者的 mLastVersion 还是 -1于是观察者会收到一次回调。这就是“新注册观察者立刻收到旧数据”的根源也就是社区常说的粘性事件。这个机制具体怎么运作我在第 4 章单独展开。1.2 SafeIterableMap观察者容器为什么不用 HashMapmObservers的类型是SafeIterableMapObserver? super T, ObserverWrapper这不是普通的 HashMap而是 AndroidX 里一个专用数据结构主要解决一个痛点在遍历通知观察者时如果观察者中途注册了新的观察者普通容器会抛ConcurrentModificationExceptionSafeIterableMap 可以容忍这种并发修改。private SafeIterableMapObserver? 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); }有个细节值得注意如果页面已经 DESTROYEDobserve会直接静默返回不注册、不回调。这意味着你在 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 三次最终只会保留最后一个值中途的数据被覆盖丢弃。再看主线程上执行的 Runnableprivate 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(LiveDataT liveData, LifecycleOwner owner, ObserverT 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 threadsetValue 里的assertMainThread会在非主线程直接抛异常。原因不复杂LiveData 的设计假设是主线程上是单线程串行更新不需要额外加锁。如果放开了子线程写就必须在全链路加锁性能会明显变差。所以 Google 直接做成了硬性限制。子线程要更新就统一走postValue。但如果一个方法既可能主子线程调用、又可能子线程调用那统一用 postValue 也不会错无非是中间值可能被丢弃。关键看业务到底需不需要每一帧都展示不需要就把这一节开头那个判断写法当作标准答案。5.3 内存泄漏边界observeForever 与手动清理LiveData 本身设计上是不会泄漏 Activity 的因为 LifecycleBoundObserver 在 DESTROYED 时会自动removeObserver。但代码里直接用了observeForever就绕过了生命周期绑定观察者会一直存活必须手动 removeprivate final ObserverString 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 之后调 observeLiveData 直接忽略注册没有回调也没有错误日志。针对第二点自定义 LifecycleOwner 时需要确保生命周期状态切换被正确上报。我在项目里见过一个 View 层自定义 LifecycleOwner忘了在 onDetachedFromWindow 时上报 DESTROYED结果 LiveData 一直持有 View 引用界面销毁后回调还在执行最后靠内存快照才定位到问题。排查这类问题建议先在onStateChanged打点确认状态流转是否符合预期。5.5 用 MediatorLiveData 合并数据源的注意点MediatorLiveData 是 LiveData 的增强子类它的存在意义是“我可以观察多个 LiveData 源再统一向外分发”。原理上它靠onActive/onInactive去动态开关上游观察MediatorLiveDataString 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 组件里的“为什么”都会迎刃而解。
返回列表