LiveData 源码解析

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() 时,发生了这些事:

  1. 安全检查:如果组件已 DESTROYED,直接返回,防止内存泄漏

  2. 创建对象 :把 Observer 和 LifecycleOwner 打包成 LifecycleBoundObserver

  3. 双向绑定

    • 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 异步

执行流程

  1. postValue("a"):创建一个 Runnable(任务是 setValue("a")),放入主线程消息队列尾部,然后立即返回

  2. setValue("b")同步执行,LiveData 的值立刻变成 "b"

  3. 当前代码块执行完毕

  4. 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 项目中,FlowStateFlow 逐渐成为 LiveData 的替代方案。它们的核心区别:

维度 LiveData StateFlow / Flow
生命周期感知 内置,自动管理 需要借助 repeatOnLifecycle 手动处理
线程切换 主线程分发,需自行处理 天然支持协程,flowOn 灵活切换
粘性行为 默认粘性 StateFlow 粘性,普通 Flow 不粘性
数据变换 Transformations 丰富的操作符(map、filter、flatMap 等)

结论:LiveData 简单易用、生命周期感知开箱即用,适合中小型项目;Flow 功能更强大、更灵活,适合复杂异步场景和 Kotlin 优先的新项目。


七、常见问题与排查

问题 1:数据倒灌(粘性事件)

现象:页面刚打开或从后台恢复时,观察者立即收到一条旧数据,导致 UI 被错误刷新,例如弹窗重复弹出、列表被重置。

排查思路

  1. 确认是否在 onCreate 中调用 observe(),此时 LiveData 已持有旧值,新观察者会立即收到一次分发。

  2. 检查数据是否属于「一次性事件」(如 Toast、导航、弹窗),这类数据不应使用普通 LiveData 承载。

  3. 确认是否在 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 销毁后,观察者仍被持有,导致页面无法被回收,出现内存泄漏。

排查思路

  1. 检查是否使用了 observeForever() 且未在 onDestroy 中调用 removeObserver()

  2. 检查是否在非 LifecycleOwner(如自定义 View、工具类)中直接持有 LiveData 的引用。

  3. 检查 MediatorLiveDataaddSource() 是否在页面销毁后仍保留数据源引用。

解决方案

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 的核心价值在于生命周期感知自动资源管理。理解它的关键机制:

  1. LifecycleBoundObserver 是连接观察者和生命周期的胶水

  2. 版本号机制 防止重复通知和数据倒灌

  3. setValue 同步、postValue 异步 是很多"陷阱"的根源

  4. 粘性事件 是设计带来的副作用,需要根据场景选择处理方案

相关推荐
hai_android1 小时前
FusibleFlow:Kotlin Flow 的融合机制原理
android·kotlin
龚寿生2 小时前
23-敏捷开发实战:从Scrum到看板的团队协作方法论
android·学习·scrum·敏捷流程
zhangguojia73 小时前
Activity启动流程(四):从setContentView到View树创建与Window挂载
android
三少爷的鞋5 小时前
Kotlin 2026:裁员、AI、Rust——黄金时代结束了吗?Jake Wharton 为你解答
android
alexhilton6 小时前
从零开始的架构测试套件
android·kotlin·android jetpack
finhaz7 小时前
台电x98plus在安卓x86的使用
android·平板·安卓x86
事圆则缓7 小时前
遗留项目改造实战指南:从可读性、代码质量到性能和 AI 约束
android·ai·代码规范
mmsx8 小时前
Android 手簿 ADB 无线调试全攻略:USB 转 WiFi 一键连接
android·前端