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 项目中,Flow 和 StateFlow 逐渐成为 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. 检查 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 的核心价值在于生命周期感知 和自动资源管理。理解它的关键机制:

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

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

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

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

相关推荐
木白CPP3 小时前
SELinux 介绍和基本使用
嵌入式硬件·安卓·安全架构
火柴就是我3 小时前
Flutter 项目本地 Maven 引入 AAR 找不到?一次 Gradle 路径问题排查
android
火柴就是我4 小时前
Android 打包报错 25.0.3
android·前端
码上有光4 小时前
Linux进程通信——共享内存、消息队列和信号量
android·linux·运维·共享内存·通信
Android打工仔8 小时前
Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?
android·kotlin
打工仔折腾 AI8 小时前
从BPE到SentencePiece:Transformer分词原理与Python实战对比
android·人工智能·python·深度学习·langchain·transformer·ai agent 实战
Dovis(誓平步青云)9 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
恋猫de小郭13 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter
执明wa13 小时前
Android RecyclerView 实战:点击频道栏切换对应内容
android·开发语言·windows·microsoft·android studio
新鲜势力呀13 小时前
PHP 定时数据同步实战:从第三方接口超时到异步同步 + 增量更新架构优化全过程
android