LiveData 是 Android 中基于观察者模式、并与 Lifecycle 组件绑定的数据容器,能自动管理订阅关系。它的核心价值在于生命周期感知和自动资源管理:界面不可见时自动停止分发,恢复时立即同步最新数据。三大关键机制包括:版本号机制(防止数据倒灌和重复消费)、setValue 同步与 postValue 异步(很多陷阱的根源)、以及粘性事件(注册后立即收到最新数据)。它非常适合在 Activity/Fragment 中承载 UI 状态,但处理一次性事件时需借助 SingleLiveEvent、Event 包装类或 SharedFlow 等方案。
一、核心设计:观察者模式 + 生命周期感知
LiveData 本质上是一个数据容器 ,你可以往里面放数据,也可以注册观察者监听变化。但它和普通观察者模式最大的区别是:与 Lifecycle 组件绑定,能自动管理订阅关系。
关键角色
| 类/接口 | 职责 |
|---|---|
LiveData<T> |
持有数据 mData、观察者列表 mObservers、版本号 mVersion |
Observer<T> |
用户实现的接口,只有 onChanged(T data) 方法 |
LifecycleOwner |
通常是 Activity/Fragment,提供生命周期状态 |
LifecycleBoundObserver |
最重要的内部类,连接三者的"胶水" |
LifecycleBoundObserver 既包装了你的 Observer 和 LifecycleOwner,又实现了 LifecycleEventObserver,从而能监听传入的 LifecycleOwner(通常是 Activity/Fragment)生命周期变化。
二、三大核心场景
场景 1:订阅的建立 ------ observe()
当你调用 observe() 时,发生了这些事:
-
安全检查:如果组件已 DESTROYED,直接返回,防止内存泄漏
-
创建对象 :把 Observer 和 LifecycleOwner 打包成
LifecycleBoundObserver -
双向绑定:
-
LiveData 的
mObservers持有这个 wrapper -
wrapper 通过
addObserver把自己注册为 LifecycleOwner 的生命周期观察者
-
结果:Activity 的任何生命周期变化都会通知到 wrapper,这是生命周期感知的基础。
场景 2:数据分发 ------ setValue() / postValue()
setValue(T value) ------ 主线程更新
java
@MainThread
protected void setValue(T value) {
assertMainThread("setValue"); // 必须在主线程
mVersion++; // 版本号升级(关键!)
mData = value; // 更新数据
dispatchingValue(null); // 开始分发
}
postValue(T value) ------ 任意线程更新
它只是把 setValue 打包成 Runnable,post 到主线程消息队列,然后立即返回。
considerNotify() ------ 决定是否通知(三重过滤)
java
private void considerNotify(ObserverWrapper observer) {
// 检查1:观察者是否活跃?不活跃不通知(避免更新已停止的UI)
if (!observer.mActive) return;
// 检查2:生命周期是否正常?
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
// 检查3:版本号检查,防止重复通知
if (observer.mLastVersion >= mVersion) return;
// 通过所有检查,发送通知
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}
场景 3:生命周期感知
LifecycleBoundObserver 通过监听生命周期事件来切换活跃状态:
java
@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver); // 发现 Activity destroy 了,自动移除订阅,防内存泄漏
return;
}
boolean newActiveState = currentState.isAtLeast(STARTED);
activeStateChanged(newActiveState);
}
关键点 :当从非活跃变为活跃时,会立即触发一次数据分发。这就是为什么界面一恢复就能显示最新数据。(也称粘性事件)
三、精妙设计
1. 版本号机制(mVersion & mLastVersion)
解决的问题:防止数据倒灌和重复消费。
场景:Activity 在后台时,LiveData 变化了 5 次(A→B→C→D→E)。恢复前台时,不应该收到 5 次回调,而只应收到一次最新的 E。
原理 :LiveData 有全局版本 mVersion,每个观察者有 mLastVersion。只有 mLastVersion < mVersion 时才通知。恢复时 mLastVersion 远小于最新 mVersion,只触发一次回调后更新版本号,后续检查就不通过了。
2. onActive() / onInactive() 回调
解决的问题:资源管理。
场景:监听 GPS 位置时,不希望界面不可见还在后台耗电。
原理 :LiveData 维护活跃观察者计数器 mActiveCount:
-
从 0→1 时,调用
onActive(),开始监听 GPS -
从 1→0 时,调用
onInactive(),停止监听,节省资源
四、经典陷阱:postValue 和 setValue 的"赛跑"
问题
java
liveData.postValue("a")
liveData.setValue("b")
// 最终值是 "a"!
原因:setValue 同步,postValue 异步
执行流程:
-
postValue("a"):创建一个 Runnable(任务是setValue("a")),放入主线程消息队列尾部,然后立即返回 -
setValue("b"):同步执行,LiveData 的值立刻变成 "b" -
当前代码块执行完毕
-
Looper 取出队列中的 Runnable,执行
setValue("a"),值从 "b" 被覆盖成 "a"
结论 :setValue 像"插队",在当前代码块里立即执行;postValue 只是"拿了个排队号",要等同步代码执行完才轮到它。
多次 postValue 的合并优化
java
liveData.postValue("a")
liveData.postValue("b")
liveData.postValue("c")
// 只有 "c" 会被分发
源码原理 (LiveData.java):
java
volatile Object mPendingData = NOT_SET;
private final Runnable mPostValueRunnable = new Runnable() {
@Override
public void run() {
Object newValue;
synchronized (mDataLock) {
newValue = mPendingData;
mPendingData = NOT_SET; // 执行后重置标记
}
setValue((T) newValue);
}
};
protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value; // 无论如何都用新值覆盖
}
if (!postTask) return; // 已有任务在排队,直接返回
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}
核心 :mPendingData 同时承担"存储待更新数据"和"判断是否有任务排队"两个角色。如果已有 Runnable 在排队,新的 postValue 只覆盖 mPendingData,不重复 post。
五、粘性事件:原理与解决方案
什么是粘性事件?
观察者注册后,会立即收到 LiveData 当前持有的最新数据,即使这份数据是在注册之前发送的。
原理
在 activeStateChanged 中:
java
if (mActive) {
dispatchingValue(this); // 立即触发一次数据分发
}
新观察者的 mLastVersion 初始值是 -1,而 LiveData 的 mVersion 更大,版本检查通过,onChanged 被调用。
总结:粘性源于"观察者从非活跃变为活跃时,立即分发最新数据"这一设计。它确保 UI 与最新状态同步,但产生了"粘性"副作用。
解决方案
| 方案 | 说明 |
|---|---|
| SingleLiveEvent | 继承 LiveData,用 AtomicBoolean 标记事件是否已消费,官方推荐 |
| Event 包装类 | LiveData 存储 Event<T>,内部有 hasBeenHandled 标记 |
| SharedFlow/Channel | 现代 Android 开发中处理一次性事件的更好选择 |
六、进阶知识点
1. MediatorLiveData:合并多个数据源
MediatorLiveData 是 LiveData 的子类,可以同时观察多个 LiveData 数据源,并在任一数据源变化时触发回调。它常用于合并多个数据源、根据条件动态添加或移除数据源等场景。
java
MediatorLiveData<String> mediator = new MediatorLiveData<>();
LiveData<String> source1 = ...;
LiveData<String> source2 = ...;
mediator.addSource(source1, value -> mediator.setValue("source1: " + value));
mediator.addSource(source2, value -> mediator.setValue("source2: " + value));
注意 :当某个数据源不再需要时,应调用 removeSource() 移除,避免内存泄漏。
2. Transformations:数据转换与映射
Transformations 提供两个常用方法,用于对 LiveData 进行声明式转换:
-
map():对 LiveData 的值做同步转换,返回新的 LiveData -
switchMap():根据源 LiveData 的值动态切换到一个新的 LiveData,常用于依赖参数变化的请求
java
// map:把 User 对象映射为用户名
LiveData<String> userName = Transformations.map(userLiveData, user -> user.getName());
// switchMap:根据 userId 动态切换数据源
LiveData<User> user = Transformations.switchMap(userIdLiveData, id -> repository.getUser(id));
注意 :Transformations 的转换是惰性的,只有在有活跃观察者时才会执行,这有助于节省资源。
3. 与 ViewModel 配合使用
LiveData 最常见的实践是放在 ViewModel 中,由 ViewModel 持有数据并通过 LiveData 暴露给 UI。这样做的核心好处是:配置变更(如屏幕旋转)时数据不丢失,因为 ViewModel 在 Activity/Fragment 重建后仍然存活。
java
public class UserViewModel extends ViewModel {
private final MutableLiveData<User> userLiveData = new MutableLiveData<>();
public LiveData<User> getUser() {
return userLiveData;
}
public void loadUser(String id) {
// 异步加载后调用 userLiveData.setValue(user);
}
}
最佳实践 :对外暴露不可变的 LiveData,内部使用 MutableLiveData 修改数据,避免外部直接篡改。
4. LiveData 与协程 / Flow 的对比
在 Kotlin 项目中,Flow 和 StateFlow 逐渐成为 LiveData 的替代方案。它们的核心区别:
| 维度 | LiveData | StateFlow / Flow |
|---|---|---|
| 生命周期感知 | 内置,自动管理 | 需要借助 repeatOnLifecycle 手动处理 |
| 线程切换 | 主线程分发,需自行处理 | 天然支持协程,flowOn 灵活切换 |
| 粘性行为 | 默认粘性 | StateFlow 粘性,普通 Flow 不粘性 |
| 数据变换 | Transformations | 丰富的操作符(map、filter、flatMap 等) |
结论:LiveData 简单易用、生命周期感知开箱即用,适合中小型项目;Flow 功能更强大、更灵活,适合复杂异步场景和 Kotlin 优先的新项目。
七、常见问题与排查
问题 1:数据倒灌(粘性事件)
现象:页面刚打开或从后台恢复时,观察者立即收到一条旧数据,导致 UI 被错误刷新,例如弹窗重复弹出、列表被重置。
排查思路:
-
确认是否在
onCreate中调用observe(),此时 LiveData 已持有旧值,新观察者会立即收到一次分发。 -
检查数据是否属于「一次性事件」(如 Toast、导航、弹窗),这类数据不应使用普通 LiveData 承载。
-
确认是否在 Activity 重建(如屏幕旋转)后重新订阅,导致再次触发粘性分发。
解决方案:
java
// 方案一:Event 包装类,标记事件是否已消费
public class Event<T> {
private final T content;
private boolean hasBeenHandled;
public Event(T content) {
this.content = content;
}
public T getContentIfNotHandled() {
if (hasBeenHandled) return null;
hasBeenHandled = true;
return content;
}
}
// 使用:LiveData<Event<String>>,观察时调用 getContentIfNotHandled()
若使用 Kotlin,推荐改用 SharedFlow 并配置 extraBufferCapacity = 0 或使用 Channel,从根源上避免粘性分发。
问题 2:内存泄漏
现象:Activity/Fragment 销毁后,观察者仍被持有,导致页面无法被回收,出现内存泄漏。
排查思路:
-
检查是否使用了
observeForever()且未在onDestroy中调用removeObserver()。 -
检查是否在非 LifecycleOwner(如自定义 View、工具类)中直接持有 LiveData 的引用。
-
检查
MediatorLiveData的addSource()是否在页面销毁后仍保留数据源引用。
解决方案:
java
// 错误:observeForever 未移除,导致泄漏
liveData.observeForever(observer);
// 正确:在 onDestroy 中移除
@Override
protected void onDestroy() {
super.onDestroy();
liveData.removeObserver(observer);
}
// 推荐:使用 observe() 绑定 LifecycleOwner,自动管理订阅
liveData.observe(this, observer);
最佳实践 :优先使用 observe() 并传入 LifecycleOwner,让 LiveData 自动处理订阅与解绑;仅在确实需要全局监听时使用 observeForever(),并务必在合适的时机手动移除。
总结
LiveData 的核心价值在于生命周期感知 和自动资源管理。理解它的关键机制:
-
LifecycleBoundObserver 是连接观察者和生命周期的胶水
-
版本号机制 防止重复通知和数据倒灌
-
setValue 同步、postValue 异步 是很多"陷阱"的根源
-
粘性事件 是设计带来的副作用,需要根据场景选择处理方案