Android面试冲击附答案(六)————LiveData

6 阅读12分钟

Android面试冲击附答案(六)————LiveData


一、面试题与答案

1. LiveData 是什么?核心特性有哪些?

生命周期安全的可观察数据持有容器,核心特性:

  1. 生命周期感知:只给活跃的观察者发送数据,页面销毁自动解除订阅(observeForever 除外)
  2. 粘性:新注册的观察者会立即收到最新的缓存数据(页面重建会自动恢复最新数据)
  3. 单向数据流(ViewModel → View):View 层只订阅,不修改源数据
  4. 主线程安全:数据更新、回调分发默认在主线程,postValue 最终也是切到主线程调用 setValue
  5. 可用全局单例 LiveData 替换 EventBus 的部分场景,更轻量和安全
2. LiveData 是如何感知生命周期的?内部用了什么机制?

观察者模式 + Lifecycle 机制

FragmentActivity 都是 LifecycleOwner,调用 LiveData.observe(this) {} 时,会把 observer 包装成 LifecycleBoundObserver,它同时实现了数据观察和生命周期观察。

  • 内部判断 owner.getLifecycle().getCurrentState(),至少 STARTED 才执行 onChanged(),页面不可见时不回调
  • 收到 ON_DESTROY 事件时自动解绑,不会内存泄露
3. observe() 和 observeForever() 的区别?observeForever 为什么必须手动 removeObserver?
  • observe():绑定生命周期,生命周期不活跃时不回调,页面销毁时自动 removeObserver,不会内存泄露。内部使用 LifecycleBoundObserver
  • observeForever():不绑定生命周期,使用 AlwaysActiveObserver,始终活跃,没有自动解绑逻辑,必须手动调用 removeObserver(),否则会内存泄露。
4. setValue 和 postValue 的区别?短时间内多次调用各自的表现?同一个 LiveData 能混用两者吗?
  • 线程setValue 只能在主线程调用;postValue 可以在子线程调用,内部通过 Handler 切到主线程再执行 setValue
  • 多次调用
    • setValue 多次调用,每次都会更新 mVersion,观察者每次都能收到
    • postValue 多次调用,中间值会被覆盖(mPendingData 被覆盖),主线程任务执行时只取最新值,中间值丢失
  • 混用:不建议混用,会有线程安全和数据顺序错乱问题
5. 什么是粘性事件?LiveData 为什么是粘性的?mVersion 和 mLastVersion 是怎么配合工作的?

粘性事件:后注册的观察者,依然能在注册时立刻收到之前已发送过的最新数据。

版本号机制

  • mVersion:LiveData 全局版本号,初始为 -1,每次 setValue/postValuemVersion++
  • mLastVersion:每个观察者内部的版本号,初始为 -1

considerNotify() 中判断 observer.mLastVersion < mVersion 时才分发数据。新注册的观察者 mLastVersion = -1,只要 LiveData 设置过值(mVersion >= 0),注册时就会立刻触发回调,这就是粘性的根本原因。

observeForever 同样走版本号机制,也是粘性的。

6. 为什么 Google 把 LiveData 设计成粘性的?好处和坏处?如何解决粘性问题?

设计初衷:保证 UI 永远能拿到最新状态(页面销毁重建后自动刷新、生命周期恢复后自动刷新)。

好处

  1. 页面重建无感恢复
  2. 延迟订阅也能拿到最新数据
  3. 生命周期安全联动,后台时不刷新

坏处

  • 一次性事件(弹窗、Toast、页面跳转)会被重复消费,导致路由跳转重复、弹窗重复弹出等问题

注意事项:需要区分状态(适合粘性)和事件(不适合粘性)。

解决方案

  1. 封装 SingleLiveEvent
  2. 改用 SharedFlowreplay = 0
7. 什么是数据倒灌?和粘性事件有什么区别?哪些场景会触发?
粘性事件数据倒灌
本质晚订阅,收到旧数据已订阅,重复收到同一批数据
性质官方设计,正常行为副作用,属于 bug
典型场景新 Observer 注册页面重建、Fragment 复用、重复注册观察者

触发数据倒灌的典型场景

  • 横竖屏切换页面重建,Observer mLastVersion 重置为 -1,立刻触发旧数据回调
  • 多级页面跳转,共享 ViewModel 中的 LiveData 缓存旧事件,新页面订阅后立刻收到
8. 如何解决 LiveData 的数据倒灌问题?各方案优缺点?
方案原理优点缺点
反射修改 version注册时将 Observer 的 mLastVersion 同步为 LiveData 的 mVersion通用,支持多 Observer反射有性能损耗,存在延迟,数据长期驻留内存
SingleLiveEventAtomicBoolean 标记事件是否已消费实现简单只支持单个 Observer,多 Observer 场景失效
Event 包装器用包装类标记 hasBeenHandled无侵入同样不能解决多 Observer 问题
UnPeekLiveData为每个 Observer 维护独立消费标识(HashMap)支持多 Observer,消费后自动清理内存多 Observer 时有一定内存消耗,只适合低频推送
改用 SharedFlowreplay = 0,天然非粘性彻底解决,功能强大需要配合 repeatOnLifecycle 使用
9. MutableLiveData 和 LiveData 的区别?为什么对外暴露 LiveData 而不是 MutableLiveData?

MutableLiveDataLiveData 的子类,将 setValuepostValue 的可见性从 protected 改为 public

对外暴露 LiveData开闭原则 + 单向数据流的体现:

  • ViewModel 内部用 MutableLiveData 修改数据
  • 对外只暴露 LiveData,确保 View 层只能观察数据,不能修改数据,防止数据被外部随意篡改
class MyViewModel : ViewModel() {
    private val _data = MutableLiveData<String>()
    val data: LiveData<String> = _data  // 对外只读
}
10. MediatorLiveData 是什么?适用于哪些场景?

MediatorLiveDataLiveData 的子类,可以监听多个 LiveData 数据源,并对数据进行拦截、转发、合并处理。

适用场景

  1. 同时监听多个数据源,任意一个更新就触发回调(如合并本地缓存和网络数据)
  2. 数据源变化频繁,只需监听前 N 次后取消订阅
  3. distinctUntilChanged()map()switchMap() 等操作符的底层实现都依赖它
11. LiveData 的 observe() 源码流程是什么?LifecycleBoundObserver 做了什么?

observe() 流程

  1. 检测当前生命周期,若已 DESTROYED 直接 return
  2. 将 observer 包装成 LifecycleBoundObserver(绑定生命周期)
  3. 存入 LiveData 内部的 mObservers 集合
  4. LifecycleBoundObserver 注册到 LifecycleRegistry,监听生命周期事件
  5. 注册完成后主动触发一次数据分发(considerNotify()),实现粘性

LifecycleBoundObserver 的作用

  • 同时实现了 LifecycleEventObserver(感知生命周期)和 ObserverWrapper(持有数据 Observer)
  • 收到 ON_DESTROY 时自动 removeObserver
  • observeForever 使用的是 AlwaysActiveObserver,不具备生命周期感知能力
12. LiveData 的 setValue() 源码流程是什么?considerNotify() 里做了哪些判断?

setValue() 流程

setValue()
  → 检测主线程(非主线程抛异常)
  → mVersion++,mData = value
  → dispatchingValue(null)  // null 表示遍历所有观察者
      → 逐个调用 considerNotify(observer)

considerNotify() 三层判断

  1. observer.mActive:观察者是否活跃(未被移除)
  2. observer.shouldBeActive():宿主生命周期是否 ≥ STARTED
  3. observer.mLastVersion >= mVersion:版本号是否已是最新

三层全部通过后:更新 mLastVersion,回调 observer.mObserver.onChanged(mData)

13. 生命周期从不活跃变为活跃时,LiveData 是如何补发数据的?
LifecycleBoundObserver.onStateChanged()
  → activeStateChanged(shouldBeActive())
      → mActive = true
      → dispatchingValue(this)  // this 表示只分发给当前 observerconsiderNotify(observer)
              → 三层校验通过
              → onChanged(mData)

这就是为什么页面从后台回到前台、或者 Fragment 从不可见变为可见时,会自动收到最新数据。

14. lifecycleOwner 和 viewLifecycleOwner 的区别?Fragment 中应该用哪个?
lifecycleOwner(即 thisviewLifecycleOwner
生命周期范围onAttach ~ onDestroy(Fragment 实例)onCreateView ~ onDestroyView(Fragment 视图)
Fragment replace 时实例不销毁,生命周期不结束视图销毁,生命周期结束

Fragment 中应使用 viewLifecycleOwner,原因:

  • 使用 this 时,Fragment replace 后视图已销毁,但 Observer 仍存活,视图重建后会重复注册,导致内存泄露重复回调
  • viewLifecycleOwner 随视图销毁自动解绑,更安全

注意:子线程中不能直接获取 viewLifecycleOwner,会抛异常。Dialog 没有 viewLifecycleOwner,但有自己的 lifecycleOwner,可用来绑定 LiveData 实现自动解绑。

15. LiveData 不支持背压是什么意思?会导致什么问题?如何解决?

背压:数据生产速度 > 消费速度时,消费者来不及处理的问题。

LiveData 不支持背压的表现:

  • 疯狂 setValue:每次都更新版本号并遍历所有 Observer 分发,可能导致主线程过载、ANR
  • 疯狂 postValuemPendingData 被不断覆盖,中间值全部丢失,导致状态错乱

解决方案

  1. 生产端限流(防抖、节流、debounce
  2. 改用 Flow(支持背压策略:bufferconflatecollectLatest 等)
16. LiveData 重复 setValue 相同的值,观察者会收到回调吗?如何实现防抖?

会收到。每次 setValue 都会 mVersion++considerNotify 只判断版本号,不比较值是否相同。

防抖方案:使用官方的 distinctUntilChanged() 扩展函数,内部原理是用 MediatorLiveData 保存上一次的值,新值与旧值相同则拦截,不触发 onChanged

val filteredLiveData = liveData.distinctUntilChanged()
17. 为什么要把 LiveData 放在 ViewModel 中,而不是直接放在 Activity/Fragment 里?
  1. 数据存活:ViewModel 在页面销毁重建(横竖屏切换)时不会销毁,LiveData 中的数据得以保留
  2. 职责分离:避免 View 层臃肿,数据逻辑收敛到 ViewModel
  3. 代码复用:ViewModel 可跨 Fragment 共享,LiveData 数据可被多个页面订阅
  4. 避免内存泄露:LiveData 持有 ViewModel 引用而非 Activity,不会因 Activity 重建导致泄露
18. LiveData 和 Flow 的区别?什么场景下选哪个?
LiveDataFlow(StateFlow/SharedFlow)
生命周期感知✅ 自带❌ 需配合 repeatOnLifecycle
背压支持❌ 不支持✅ 支持多种策略
粘性控制❌ 默认粘性,不可关闭✅ 可配置(replay = 0 非粘性)
操作符少(map/switchMap)丰富(debounce/filter/combine 等)
线程切换自动主线程需手动 flowOn
多订阅者支持SharedFlow 支持
数据缓存只缓存最新一个可配置缓存数量

选择建议

  • LiveData:简单 UI 状态展示,团队熟悉度高的场景
  • Flow:复杂异步流、连续数据流、需要防抖/限流/合并、一次性事件等场景
19. 如何用 LiveData 实现 LiveDataBus?优缺点?和 EventBus 相比如何?

实现原理:用 HashMap 维护事件名到 MutableLiveData 的映射,通过单例对外提供订阅和发布能力。

object LiveDataBus {
    private val bus = HashMap<String, MutableLiveData<Any>>()

    fun <T> with(key: String): MutableLiveData<T> {
        if (!bus.containsKey(key)) bus[key] = MutableLiveData()
        @Suppress("UNCHECKED_CAST")
        return bus[key] as MutableLiveData<T>
    }
}
// 发送:LiveDataBus.with<String>("event").postValue("data")
// 订阅:LiveDataBus.with<String>("event").observe(this) { }

注意:需解决粘性问题(新订阅者会收到旧消息),可结合反射修改 version 或 UnPeekLiveData。

对比 EventBus

LiveDataBusEventBus
实现复杂度简单,一个类搞定需引入第三方库
生命周期感知✅ 自动解绑❌ 需手动注册/反注册
内存泄露风险高(忘记反注册)
跨进程❌ 不支持❌ 不支持
粘性事件默认粘性(需处理)可选粘性
20. Transformations.map() 和 Transformations.switchMap() 的区别和使用场景?
map()switchMap()
返回值同步转换,返回新值返回新的 LiveData
数据源固定可动态切换
异步支持
旧订阅处理自动取消旧 LiveData 订阅
// map:同步转换数据
val userName: LiveData<String> = Transformations.map(userLiveData) { it.name }

// switchMap:根据输入动态切换数据源(如搜索)
val result: LiveData<List<Item>> = Transformations.switchMap(queryLiveData) { query ->
    repository.search(query)  // 返回新的 LiveData
}

switchMap 适合数据请求、数据库查询等异步场景,每次输入变化时自动取消上一次请求。

21. 在 ViewModel 中用 LiveData 做页面通信,多级页面跳转 A→B→C 时会有什么问题?如何解决?

问题:共享 ViewModel 不会随页面销毁,LiveData 一直缓存旧事件。

  1. 粘性事件:C 页面订阅后立刻收到上一级残留的旧事件,触发意外逻辑
  2. 数据倒灌:多级页面层层订阅同一 LiveData,一条事件导致所有页面都触发更新
  3. 内存泄露:共享 ViewModel + 长期常驻 LiveData + 未及时解绑

根本原因:LiveData 的设计定位是 UI 状态容器,不是跨页面事件总线,不适合一次性事件和多级页面栈场景。

解决方案

  • 一次性事件改用 SharedFlow(replay = 0) + repeatOnLifecycle
  • 或使用 UnPeekLiveData 解决粘性问题
  • 页面间通信优先考虑 Fragment Result APINavigationSavedStateHandle
22. LiveData 在 onCreate 里注册了观察者,在 onResume 时发射数据,能收到吗?

能收到。两种情况都能正常接收:

  • onCreate 注册 + onResume 发数据:生命周期已活跃,setValue 直接触发 onChanged,正常收到
  • onCreate 发数据 + 后续生命周期走到前台:数据先缓存在 mData,等生命周期变为活跃(onStart)时,LifecycleBoundObserver.onStateChanged 触发补发,靠粘性机制也能收到

二、LiveData 原理

核心类关系

类名职责
LiveData<T>抽象基类,持有数据 mData、版本号 mVersion、观察者集合 mObservers
MutableLiveData<T>子类,将 setValue/postValue 改为 public
MediatorLiveData<T>子类,支持监听多个 LiveData 数据源
LifecycleBoundObserver内部类,同时实现数据观察和生命周期观察,observe() 使用
AlwaysActiveObserver内部类,始终活跃,observeForever() 使用
ObserverWrapper抽象内部类,持有 Observer 引用和 mLastVersion

observe() 注册流程

LiveData.observe(owner, observer)
    │
    ├── owner 已 DESTROYED → return(防止泄露)
    │
    ├── 包装成 LifecycleBoundObserver(绑定生命周期 + 数据观察)
    │
    ├── mObservers.putIfAbsent(observer, wrapper)  // 存入观察者集合
    │
    └── owner.getLifecycle().addObserver(wrapper)  // 注册生命周期监听
            │
            └── 触发 onStateChanged → activeStateChanged → considerNotify
                    └── 粘性补发(若 mVersion > mLastVersion)

setValue() 分发流程

setValue(value)
    │
    ├── 检测主线程(非主线程抛 IllegalStateException)
    ├── mVersion++
    ├── mData = value
    └── dispatchingValue(null)  // null = 遍历所有 observer
            │
            └── for each observer → considerNotify(observer)
                    │
                    ├── !observer.mActive → skip(观察者已移除)
                    ├── !shouldBeActive() → skip(生命周期 < STARTED)
                    ├── mLastVersion >= mVersion → skip(已是最新)
                    │
                    └── mLastVersion = mVersion
                        observer.onChanged(mData)  ✅ 回调

生命周期补发流程

页面从后台回到前台(onStart)
    │
    └── LifecycleBoundObserver.onStateChanged(ON_START)
            │
            └── activeStateChanged(true)
                    │
                    └── dispatchingValue(this)  // this = 只分发给当前 observer
                            │
                            └── considerNotify → onChanged(mData)  ✅ 补发

粘性事件版本号机制

LiveData 初始状态:mVersion = -1,mData = null

setValue("hello")
    → mVersion = 0,mData = "hello"

新 Observer 注册(mLastVersion = -1)
    → considerNotify:mLastVersion(-1) < mVersion(0)
    → 立刻回调 onChanged("hello")  ← 粘性触发
    → mLastVersion = 0

再次 setValue("world")
    → mVersion = 1
    → considerNotify:mLastVersion(0) < mVersion(1)
    → 回调 onChanged("world")

postValue 丢值原理

子线程快速调用:
postValue("A") → mPendingData = "A",post Runnable 到主线程
postValue("B") → mPendingData = "B"(覆盖 A)
postValue("C") → mPendingData = "C"(覆盖 B)

主线程 Runnable 执行:
    → 取 mPendingData = "C"
    → setValue("C")
    → 观察者只收到 "C","A" 和 "B" 丢失

LiveData vs Flow 选型

简单 UI 状态(登录状态、用户信息)
    └── LiveData ✅(简单、自带生命周期感知)

一次性事件(Toast、导航、弹窗)
    └── SharedFlow(replay=0) ✅(非粘性,不重复消费)

复杂数据流(搜索、分页、实时更新)
    └── Flow + collectLatest ✅(背压支持、丰富操作符)

需要防抖/节流
    └── Flow + debounce/throttle ✅