我们将从它的设计哲学出发,深入到三大核心场景的源码实现,并揭示其内部精妙的"防坑"设计。
第一部分:设计哲学与核心组件
-
1. 核心设计:观察者模式 + 生命周期感知
LiveData
本质上是一个实现了观察者模式的数据容器。你可以往里面放数据,也可以注册观察者来监听数据的变化。- 它的"魔法"在于,它不是一个普通的观察者模式,而是与 Android 的
Lifecycle
组件紧密绑定。这使得它能自动管理订阅关系,避免内存泄漏和空指针异常。
-
2. "演员表":关键类与接口
LiveData<T>
: 主角。它持有数据 (mData
)、一个观察者列表 (mObservers
) 和一个版本号 (mVersion
)。Observer<T>
: 用户实现的接口,只有一个onChanged(T data)
方法,用于接收数据更新。LifecycleOwner
: 通常是Activity
或Fragment
,它提供了生命周期状态。LifecycleBoundObserver
: 最重要的内部类 ,是连接三者的"胶水"。它将你传入的Observer
和LifecycleOwner
包装 起来,同时它自己也实现了LifecycleEventObserver
,从而能"监听"生命周期的变化。
第二部分:三大核心场景源码剖析
场景一:liveData.observe(lifecycleOwner, observer)
- 订阅的建立
当你调用 observe
方法时,一场精密的"绑定仪式"就开始了。
简化源码 & 步骤解析:
java
// LiveData.java
@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
// 1. 安全检查:如果组件已销毁,直接忽略,防止内存泄漏
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
return;
}
// 2. 核心步骤:将你的 Observer 和 LifecycleOwner 打包成一个 LifecycleBoundObserver
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
// 3. 存储:将这个包装好的 wrapper 存入 mObservers 映射表中
// 如果这个 observer 之前已经订阅过,会先移除旧的,再添加新的
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
// ... 处理重复订阅的逻辑 ...
// 4. 关键绑定:让 wrapper 开始观察 LifecycleOwner 的生命周期
owner.getLifecycle().addObserver(wrapper);
}
发生了什么?
- 创建"胶水"对象 :
LiveData
并不直接持有你的Observer
,而是创建了一个LifecycleBoundObserver
包装类。 - 双向绑定 :
LiveData
在自己的mObservers
列表里持有了这个wrapper
。wrapper
通过addObserver
方法,把自己注册为了LifecycleOwner
的一个生命周期观察者。
- 结果 :现在,
LifecycleOwner
的任何生命周期变化(如ON_START
,ON_STOP
),都会通知到LifecycleBoundObserver
,这是实现生命周期感知的关键。
场景二:liveData.setValue(data)
/ postValue(data)
- 数据的分发
-
setValue(T value)
- 主线程更新java// LiveData.java @MainThread protected void setValue(T value) { // 1. 安全检查:必须在主线程调用 assertMainThread("setValue"); // 2. 版本升级:这是防止数据倒灌和重复通知的关键 mVersion++; // 3. 更新数据 mData = value; // 4. 开始分发 dispatchingValue(null); }
-
postValue(T value)
- 任意线程更新java// LiveData.java protected void postValue(T value) { // 将 setValue 操作打包成一个 Runnable,post 到主线程的消息队列 ArchTaskExecutor.getInstance().postToMainThread(new Runnable() { @Override public void run() { setValue(value); } }); }
-
dispatchingValue(@Nullable ObserverWrapper initiator)
- 分发的核心java// LiveData.java void dispatchingValue(@Nullable ObserverWrapper initiator) { // ... 省略了重入检查逻辑 ... // 遍历所有已注册的观察者 wrapper for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator = mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { // 对每个观察者,调用 considerNotify considerNotify(iterator.next().getValue()); } // ... }
-
considerNotify(ObserverWrapper observer)
- 决定是否通知java// LiveData.java private void considerNotify(ObserverWrapper observer) { // 1. 关键检查1:观察者是否处于"活跃"状态? // 对于 LifecycleBoundObserver 来说,这意味着组件至少是 STARTED if (!observer.mActive) { return; } // 2. 关键检查2:观察者的生命周期是否正常? if (!observer.shouldBeActive()) { observer.activeStateChanged(false); // 如果不正常,则将其置为非活跃 return; } // 3. 关键检查3:版本号检查,防止重复通知 if (observer.mLastVersion >= mVersion) { return; } // 4. 更新观察者版本号,并发送通知 observer.mLastVersion = mVersion; observer.mObserver.onChanged((T) mData); }
发生了什么?
setValue
必须在主线程,它会增加版本号mVersion
,然后触发分发。postValue
可以在任何线程,它只是把setValue
的调用"扔"到了主线程去执行。- 分发时,
LiveData
会遍历所有的观察者,但不是无脑通知 ,而是通过considerNotify
进行了三重过滤:- 组件不活跃?不通知! (避免了更新已停止的UI)
- 生命周期已结束?不通知!
- 已经收到过这个版本的数据了?不通知! (防止配置变更等场景下的重复调用)
- 只有通过所有检查的观察者,才会最终调用其
onChanged
方法。
场景三:生命周期感知 - 自动驾驶的实现
LifecycleBoundObserver
是如何知道自己是 active
还是 inactive
的?因为它监听了生命周期事件。
java
// LiveData.java -> LifecycleBoundObserver
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
// 1. 获取当前生命周期状态
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
// 如果已销毁,自动移除订阅
removeObserver(mObserver);
return;
}
// 2. 根据新状态,判断自己是否应该 active
boolean newActiveState = currentState.isAtLeast(STARTED);
// 3. 调用 activeStateChanged,更新自己的状态并通知 LiveData
activeStateChanged(newActiveState);
}
@Override
void activeStateChanged(boolean newActive) {
if (newActive == mActive) {
return;
}
mActive = newActive;
// ... 这里会调用 LiveData.this.mActiveCount += mActive ? 1 : -1; ...
// ... 并触发 LiveData 的 onActive / onInactive 回调 ...
// 如果变为活跃,立即尝试分发一次数据
if (mActive) {
dispatchingValue(this);
}
}
}
发生了什么?
- 当 Activity
onStart()
时,onStateChanged
被调用,newActiveState
变为true
。 activeStateChanged
被调用,mActive
变为true
,同时LiveData
的活跃观察者计数器mActiveCount
会加 1。- 最重要的是,它会立即调用
dispatchingValue(this)
,尝试把最新的数据推送给这个刚刚变为活跃的观察者。这就是为什么你的界面一恢复,总能显示最新数据的原因。 - 当 Activity
onStop()
时,流程相反,mActive
变为false
,mActiveCount
减 1。 - 当 Activity
onDestroy()
时,onStateChanged
会自动调用removeObserver
,彻底断开订阅关系,从根源上防止了内存泄漏。
第三部分:精妙设计 - "防坑"机制
-
1. 版本号机制 (
mVersion
&mLastVersion
)- 解决了什么问题? 防止数据倒灌和重复消费。
- 场景 :一个 Activity 在后台时,
LiveData
的值变化了 5 次 (A -> B -> C -> D -> E
)。当 Activity 恢复到前台时,它不应该 接收A,B,C,D,E
五次onChanged
回调,而只应该接收一次最新的值E
。 - 原理 :
LiveData
有一个全局版本mVersion
,每个观察者有自己的mLastVersion
。只有当mLastVersion < mVersion
时才会通知。当 Activity 恢复时,它的mLastVersion
远小于最新的mVersion
,于是onChanged
被调用一次,同时mLastVersion
更新到最新,后续的重复检查就不会再通过了。
-
2.
onActive()
/onInactive()
回调- 解决了什么问题? 资源管理。
- 场景 :一个
LiveData
需要监听 GPS 位置变化。我们不希望在没有界面显示位置时,还在后台耗电监听 GPS。 - 原理 :
LiveData
内部维护了一个活跃观察者计数器mActiveCount
。- 当
mActiveCount
从0
变为1
时,onActive()
被调用。此时你可以开始监听 GPS。 - 当
mActiveCount
从1
变为0
时,onInactive()
被调用。此时你可以停止监听 GPS,节省资源。
- 当
流程总结:setValue
时序图
如果同时调用 postValue("a")和setValue("b"),一定是值b被值a覆盖 是什么原因?
这个问题问得非常好,它直击了 LiveData
核心的线程调度机制,也是一个非常经典的 LiveData
"陷阱"和面试题。
您观察到的现象 "postValue("a")
之后立即调用 setValue("b")
,最终值是 a
(即值b被值a覆盖)" 是完全正确的。
原因是:setValue
是同步的,而 postValue
是异步的。
我们来深入源码和执行流程,看看这背后到底发生了什么。
核心机制:主线程的 MessageQueue
要理解这个现象,首先必须理解 Android 主线程是如何工作的。主线程有一个 Looper
,它不断地从一个 MessageQueue
(消息队列) 中取出任务(Message
或 Runnable
)来执行。这是一个先进先出(FIFO)的队列。
setValue(T value)
: 这是一个同步 操作。它会立即在当前线程执行。如果当前不是主线程,它会直接抛出异常。postValue(T value)
: 这是一个异步 操作。它做的事情非常简单:把一个"更新值的任务"(一个Runnable
)提交到主线程的MessageQueue
的队尾,然后立即返回。它本身并不执行更新操作。
分步执行流程:一场注定会输的赛跑
假设我们当前就在主线程上执行以下代码:
kotlin
// 假设 liveData 的初始值是 "initial"
Log.d("TEST", "1. 即将调用 postValue")
liveData.postValue("a")
Log.d("TEST", "2. 即将调用 setValue")
liveData.setValue("b")
Log.d("TEST", "3. 所有调用已完成")
下面是代码执行的精确时间线:
-
Log.d("TEST", "1. 即将调用 postValue")
: 日志被打印。 -
liveData.postValue("a")
被调用:LiveData
内部创建了一个Runnable
,这个Runnable
的任务是setValue("a")
。- 这个
Runnable
被ArchTaskExecutor
发送(post) 到了主线程的MessageQueue
的末尾,排队等待执行。 postValue
方法立即返回 ,代码继续往下执行。此时LiveData
的值仍然是 "initial"。
-
Log.d("TEST", "2. 即将调用 setValue")
: 日志被打印。 -
liveData.setValue("b")
被调用:- 这是一个同步调用。
- 代码立即执行
mVersion++
和mData = "b"
。 LiveData
的值立刻、马上 变成了 "b"。dispatchingValue
被触发,所有活跃的观察者都会收到值 "b"。setValue
方法执行完毕并返回。
-
Log.d("TEST", "3. 所有调用已完成")
: 日志被打印。 -
当前代码块执行完毕: 你的这段代码(比如一个点击事件的回调)执行完了。
-
Looper
空闲下来 : 主线程的Looper
说:"好了,我手头上的活干完了,看看我的消息队列里还有什么任务?" -
Looper
取出任务 :Looper
从MessageQueue
的队首取出了在第 2 步被放进去的那个Runnable
。 -
Runnable
被执行:- 这个
Runnable
的核心代码setValue("a")
被执行。 LiveData
的值从 "b" 被覆盖成了 "a"。dispatchingValue
再次被触发,所有活跃的观察者现在会收到值 "a"。
- 这个
结论
setValue("b")
就像是"插队",它在你当前的代码块里被立即执行了。而 postValue("a")
只是拿到了一个"排队号码",必须等到当前所有同步代码都执行完毕,主线程空闲下来之后,才轮到它执行。
因此,无论调用顺序如何,同步的 setValue
总是先于异步的 postValue
的 Runnable
执行,但 postValue
的 Runnable
执行得更晚,所以它的值会覆盖掉 setValue
的值。
一个重要的补充:多次 postValue
会发生什么?
这也是一个常见的面试题。如果你在后台线程 连续、快速地调用 postValue
,会发生什么?
kotlin
// 在后台线程执行
liveData.postValue("a")
liveData.postValue("b")
liveData.postValue("c")
结果是:只有最后一个值 "c" 会被分发。
原因 :LiveData
内部做了一个优化。当 postValue
被调用时,它会向主线程 post 一个 Runnable
。如果在这个 Runnable
还未被执行之前 ,你又调用了 postValue
,它不会 post 一个新的 Runnable
,而是会用新的值覆盖掉等待被执行的那个旧值。
这确保了在密集的后台更新中,只有最新的数据会被推送到 UI,避免了消息队列的拥堵和不必要的 UI 刷新。
LiveData 内部做了一个优化。当 postValue 被调用时,它会向主线程 post 一个 Runnable。如果在这个 Runnable 还未被执行之前,你又调用了 postValue,它不会 post 一个新的 Runnable,而是会用新的值覆盖掉等待被执行的那个旧值。 这一部分的源码在哪里?
当然,这个优化是 LiveData
设计中非常精妙的一部分。相关的源码主要集中在 LiveData.java
文件中的 postValue
方法本身,以及几个关键的成员变量。
源码位置
您可以在 androidx.lifecycle.LiveData
类的源码中找到这段逻辑。关键代码如下:
1. 关键的成员变量
java
// LiveData.java
// 用于同步锁,确保多线程访问 mPendingData 时的安全
final Object mDataLock = new Object();
// 用于标记 mPendingData 是否为空,它是一个内部单例对象
static final Object NOT_SET = new Object();
// 存储等待被 post 到主线程的数据。
// volatile 关键字确保了多线程间的可见性。
volatile Object mPendingData = NOT_SET;
// 这是一个【单例】的 Runnable,整个 LiveData 实例只会创建一次。
// 它的作用就是去主线程调用 setValue。
private final Runnable mPostValueRunnable = new Runnable() {
@Override
public void run() {
Object newValue;
// 同步块,确保安全地取出数据并重置标记
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET; // **关键:执行后立即重置标记**
}
// 在主线程调用 setValue
setValue((T) newValue);
}
};
2. postValue(T value)
方法的实现
java
// LiveData.java
protected void postValue(T value) {
boolean postTask;
// 进入同步块,保证线程安全
synchronized (mDataLock) {
// 检查 mPendingData 是否是初始状态 NOT_SET
// 如果是,说明当前没有等待执行的任务,需要 post 一个新的
postTask = mPendingData == NOT_SET;
// **无论如何,都用新值覆盖 mPendingData**
// 这就是为什么只有最后一个值生效的原因
mPendingData = value;
}
// 如果 postTask 是 false,意味着已经有一个 mPostValueRunnable 在主线程队列里排队了
// 此时我们已经用新值覆盖了 mPendingData,所以直接 return 即可,无需重复 post。
if (!postTask) {
return;
}
// 如果 postTask 是 true,说明这是自上次执行完后的第一次 post
// 于是将那个【单例的】mPostValueRunnable 提交到主线程
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
源码执行流程分析
让我们来模拟一下在后台线程连续调用两次 postValue
的场景:
场景:
java
// 在后台线程
liveData.postValue("A");
liveData.postValue("B");
第 1 步: liveData.postValue("A")
被调用
- 进入
synchronized (mDataLock)
代码块。 - 此时
mPendingData
的值是NOT_SET
。 postTask = (mPendingData == NOT_SET)
,所以postTask
被赋值为true
。mPendingData = "A"
,mPendingData
的值被更新为 "A"。- 退出同步块。
if (!postTask)
条件不成立。ArchTaskExecutor...postToMainThread(mPostValueRunnable)
被调用。一个mPostValueRunnable
任务被放到了主线程的消息队列末尾,等待执行。
第 2 步: liveData.postValue("B")
被立即调用 (假设此时主线程繁忙,之前的 Runnable 还没执行)
- 再次进入
synchronized (mDataLock)
代码块。 - 此时
mPendingData
的值是 "A"。 postTask = (mPendingData == NOT_SET)
,所以postTask
被赋值为false
。mPendingData = "B"
,mPendingData
的值从 "A" 被覆盖成了 "B"。- 退出同步块。
if (!postTask)
条件成立 (!false
->true
)。return;
方法直接返回。没有新的Runnable
被 post!
第 3 步: 主线程空闲,开始执行消息队列中的任务
- 主线程的
Looper
从队列中取出了在第 1 步被 post 的那个mPostValueRunnable
任务。 mPostValueRunnable.run()
方法被执行。- 进入
synchronized (mDataLock)
代码块。 newValue = mPendingData
,此时mPendingData
的值是 "B",所以newValue
被赋值为 "B"。mPendingData = NOT_SET
,标记被重置,为下一次postValue
做好准备。- 退出同步块。
setValue((T) newValue)
,也就是setValue("B")
被调用。- 最终,观察者收到的值是 "B"。
总结: 这个设计的核心就在于用 mPendingData
变量同时承担了"存储待更新数据"和"判断是否有任务在排队"两个角色 ,并结合 synchronized
锁和 volatile
关键字,用一个可重用的 Runnable
实例,高效且线程安全地实现了 postValue
的合并与优化。
LiveData的粘性事件 原理
好的,我们来深入探讨 LiveData
的"粘性事件"这个话题。
首先,需要明确一点:"粘性事件"并不是 LiveData
设计时的一个"功能",而是它内在机制所带来的一个"现象"或"副作用" 。理解了我们之前讨论的 LiveData
源码,就能很自然地理解粘性事件的由来了。
1. 什么是粘性事件?
粘性事件指的是:一个观察者(Observer)在注册到 LiveData
之后,会立即收到 LiveData
中当前持有的、最新的那一份数据,即使这份数据是在这个观察者注册之前就已经发送的。
举个典型的例子:
- 用户在列表页点击了某个商品,然后App发送了一个网络请求去获取商品详情。
- 在网络请求发出后、结果返回前,用户跳转到了详情页。
- 详情页 的
Fragment
创建 (onCreate
),并注册了一个Observer
来监听商品详情LiveData
。 - 一两秒后,网络请求成功,
LiveData
的值被更新。 - 此时,由于详情页的
Observer
已经注册,它会正常收到商品数据并更新UI。(这是正常事件)
粘性事件的场景:
- 用户在列表页点击了某个商品,App发送网络请求。
- 网络请求很快 就成功返回了,
LiveData
的值被更新为商品详情数据。 - 此时用户才 跳转到详情页。
- 详情页 的
Fragment
创建,并注册了一个Observer
来监听这个已经有值 的LiveData
。 - 就在
observe
方法被调用的那一刻 ,Observer
会立即收到之前已经更新好的商品详情数据,并刷新UI。
这个"先更新数据,后注册监听,但监听者依然能收到数据"的现象,就是所谓的"粘性事件"。数据就像被"粘"在了 LiveData
上,等待着下一个新的观察者来消费。
2. 粘性事件的原理:源码再回顾
粘性事件的原理,就藏在我们之前分析过的 LiveData.observe()
和 LifecycleBoundObserver.onStateChanged()
的源码里。
核心流程回顾:
-
调用
liveData.observe(lifecycleOwner, observer)
:LiveData
将lifecycleOwner
和observer
打包成一个LifecycleBoundObserver
(我们称之为wrapper
)。wrapper
被添加到LiveData
的观察者列表mObservers
中。wrapper
把自己注册为lifecycleOwner
的一个生命周期观察者。
-
生命周期变化,触发
onStateChanged
:- 当
Fragment
或Activity
的生命周期推进,例如执行到onStart()
时,它的Lifecycle
状态会变为STARTED
。 - 这个变化会通知到我们注册的
wrapper
,其onStateChanged()
方法被调用。
- 当
-
activeStateChanged(true)
被调用:- 在
onStateChanged()
内部,它会检查当前的生命周期状态。因为STARTED
状态满足isAtLeast(STARTED)
,所以它会调用activeStateChanged(true)
来将自己标记为"活跃"。
- 在
-
关键代码:
dispatchingValue(this)
- 在
activeStateChanged
方法的最后,有这样一段至关重要的代码:
java// LiveData.java -> LifecycleBoundObserver @Override void activeStateChanged(boolean newActive) { if (newActive == mActive) { return; } mActive = newActive; // ... // 如果是从非活跃变为活跃 if (mActive) { // **立即触发一次数据分发!** dispatchingValue(this); } }
- 在
-
considerNotify()
被调用:dispatchingValue
会遍历观察者列表,并对我们这个刚刚激活的wrapper
调用considerNotify
。- 在
considerNotify
内部,会进行版本号检查:if (observer.mLastVersion >= mVersion)
。 - 因为我们这个
wrapper
是全新 的,它的mLastVersion
初始值是-1
。而LiveData
因为之前已经更新过数据,它的mVersion
肯定大于-1
。 - 因此,版本检查通过!
wrapper
的onChanged
方法被调用,粘性事件发生。
原理总结: LiveData
的粘性,源于其**"当观察者从非活跃状态变为活跃状态时,会立即尝试将当前持有的最新数据分发给它"**这一核心设计。这个设计确保了 UI 组件总能与最新的数据状态保持同步,但也因此产生了"粘性"的副作用。
3. 如何处理不想要的粘性事件?
在很多场景下,我们只希望 Observer
接收在它注册之后 才发生的事件,比如"显示一次性的 Toast
"或"执行一次性的导航"。粘性事件在这种情况下会导致问题(例如,屏幕旋转后 Fragment
重建,会再次收到旧的 Toast
事件)。
处理这个问题通常有以下几种方案:
-
SingleLiveEvent
模式 (官方推荐):- 创建一个继承自
LiveData
的自定义类,重写observe
方法。 - 内部使用一个
AtomicBoolean
来标记事件是否已经被消费过。 - 只有在"未被消费"的状态下,才真正调用
onChanged
,并且一旦调用就将事件标记为"已消费"。 - 这是最常见、最稳健的解决方案。
- 创建一个继承自
-
包装类方案:
LiveData
中存储的不是原始数据T
,而是一个包装类Event<T>
。Event
类内部有一个hasBeenHandled
的布尔标记。Observer
收到Event
对象后,先检查hasBeenHandled
标记。如果未被处理,则处理数据并手动将标记置为true
。
-
使用
Flow
:- 在现代 Android 开发中,对于一次性事件,使用 Kotlin Coroutines 的
SharedFlow
或Channel
通常是更好的选择,因为它们在设计上就是为了处理这种"事件流"而不是"状态",能更优雅地解决粘性问题。
- 在现代 Android 开发中,对于一次性事件,使用 Kotlin Coroutines 的
理解了粘性事件的原理,你就能更好地决定何时利用它,何时需要规避它了。