在Android开发中,LiveData作为Jetpack组件的一部分,承担着数据持有与通知的角色。它基于观察者模式构建,但相比传统观察者模式多了生命周期感知能力。理解LiveData的观察者机制,有助于写出更稳定、更简洁的数据驱动UI代码。我们从观察者注册、数据变更通知到UI刷新的完整链路进行分析。

一、观察者模式在LiveData中的源码映射
传统观察者模式包含两个核心角色:被观察者(Subject)和观察者(Observer)。被观察者维护一个观察者列表,当数据变化时遍历列表并调用每个观察者的更新方法。LiveData的源码实现并没有另起炉灶,而是沿用了这一思路,同时加入了版本号与生命周期状态判断。打开LiveData类可以看到,内部使用SafeIterableMap保存观察者,这个容器在遍历时可以安全地处理添加和移除操作,避免并发修改异常。
每个观察者并不会直接存入SafeIterableMap,而是被包装成ObserverWrapper对象。ObserverWrapper中记录了两个关键字段:mActive表示当前观察者是否处于活跃状态,mLastVersion表示观察者最后一次接收数据时的版本号。LiveData本身维护一个mVersion字段,每次setValue被调用时,mVersion会递增。当数据分发时,LiveData会遍历所有观察者,通过considerNotify方法判断观察者是否满足接收条件。条件包括观察者必须活跃、观察者绑定的LifecycleOwner必须处于STARTED或RESUMED状态,以及观察者的mLastVersion必须小于LiveData的mVersion。只有同时满足这些条件,观察者才会收到新数据并更新mLastVersion。
下面这段简化源码展示了LiveData的核心分发逻辑,其中去掉了部分线程同步细节,保留最关键的版本比较和活跃判断。
public class LiveData<T> {
private int mVersion = 0;
private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
protected void postValue(T value) {
ArchTaskExecutor.getInstance().postToMainThread(() -> {
setValue(value);
});
}
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);
}
}
从这段代码可以看出,观察者模式在LiveData中被细化成了版本驱动的通知机制。这种设计解决了传统观察者模式中常见的两个问题:一是重复通知,版本号可以确保同一个数据不会被同一个观察者接收两次;二是粘性事件,新的观察者注册时会因为初始mLastVersion为-1而立即收到最新数据,这正是LiveData粘性特性的来源。理解这一点对后续避免事件导航中的重复监听非常重要。
二、setValue与postValue的数据分发链路
LiveData提供两种修改数据的方式:setValue和postValue。setValue要求必须在主线程调用,内部会先检查当前线程,如果不是主线程直接抛出异常。这个限制保证了UI相关的数据变更一定发生在主线程,观察者回调中可以直接操作View而无需担心线程切换。setValue执行时会先将mVersion加1,然后把新数据保存到mData字段,最后调用dispatchingValue方法遍历观察者并触发considerNotify。
postValue的设计初衷是让子线程也能安全地更新数据。当子线程调用postValue时,LiveData会把这次更新请求投递到主线程的任务队列中,最终在主线程执行setValue。postValue还有一个重要特性:如果主线程还没来得及处理当前任务,子线程又连续调用了多次postValue,LiveData只会保留最后一次的值。这是因为postValue内部使用了一个pending变量暂存最新数据,当主线程任务真正执行时,只会取出pending中的最新值,中间的值会被覆盖丢弃。这个机制有效避免了短时间高频更新导致UI线程被大量无效刷新任务阻塞。
在ViewModel中,通常使用MutableLiveData来暴露可变数据,对外只暴露不可变的LiveData实例,保证数据只能由ViewModel修改。下面是一个典型的Kotlin示例,展示从ViewModel更新数据到Activity刷新UI的完整链路。
class UserViewModel : ViewModel() {
private val _userName = MutableLiveData<String>()
val userName: LiveData<String> get() = _userName
fun updateUser(name: String) {
_userName.value = name
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(UserViewModel::class.java)
viewModel.userName.observe(this) { name ->
findViewById<TextView>(R.id.tvName).text = name
}
viewModel.updateUser("张三")
}
}
观察者注册使用observe方法,第一个参数是LifecycleOwner,通常是Activity或Fragment。LiveData内部会把观察者包装成LifecycleBoundObserver,并监听LifecycleOwner的生命周期变化。当LifecycleOwner进入DESTROYED状态时,LiveData会自动移除这个观察者,因此使用observe注册的观察者不会造成内存泄漏。当LifecycleOwner从非活跃状态回到活跃状态时,如果LiveData的数据版本已经发生变化,观察者会立刻收到最新数据,确保UI与数据保持一致。
三、数据驱动UI实践中的粘性事件与合并数据源
粘性事件是LiveData最常见的坑之一。所谓粘性,指的是当一个新的观察者注册时,会立即收到LiveData中最后一次保存的数据。对于普通的状态展示场景,这个特性非常有用,例如屏幕旋转后重新创建的Activity可以立刻恢复到旋转前的数据状态。但在事件导航、Toast提示、页面跳转等一次性事件场景中,粘性会导致同一个事件被重复消费。例如用户点击按钮触发跳转,LiveData保存了跳转事件,此时旋转屏幕,新注册的观察者会再次收到跳转事件,导致页面被跳转两次。
解决粘性事件的一种常见做法是自定义一个只处理一次性事件的LiveData。核心思路是在事件被分发一次之后,将内部的事件内容置空,或者通过原子布尔标记来阻止再次分发。另一种做法是使用事件包装类,将事件标记为已处理状态。不过更简单的方案是直接使用MediatorLiveData或者Transformations对数据进行变换,避免直接暴露原始事件数据。下面示例展示了如何通过MediatorLiveData合并两个独立数据源,并生成一个新的数据流。
val liveData1 = MutableLiveData<String>()
val liveData2 = MutableLiveData<Int>()
val mediator = MediatorLiveData<String>()
mediator.addSource(liveData1) { value ->
mediator.value = "name: $value"
}
mediator.addSource(liveData2) { value ->
mediator.value = "age: $value"
}
MediatorLiveData本身继承自MutableLiveData,它可以同时观察多个LiveData数据源。任一数据源发生变化,MediatorLiveData都会收到通知,并且可以在回调中基于多个数据源的值计算出一个新的结果。这种能力非常适合将多个ViewModel状态合并后驱动复杂UI。另外,Transformations.map和Transformations.switchMap也是数据驱动UI中常用的工具。map适合对单个数据做同步转换,switchMap适合根据一个数据源切换到另一个LiveData,常用于搜索框触发网络请求的场景。
在实践数据驱动UI时,还应该注意避免在观察者回调中做耗时操作。LiveData的观察者回调默认运行在主线程,如果直接在回调中执行数据库查询或网络请求,会导致界面卡顿甚至ANR。正确做法是使用ViewModel配合协程或RxJava将耗时操作放到后台线程,最终通过postValue或setValue把结果传回主线程。观察者模式让数据变化自动触发UI更新,但并不意味着可以忽略线程模型。只有把数据流与生命周期、线程调度结合起来,才能真正发挥出LiveData在数据驱动UI中的价值。