Android面试冲击附答案(六)————LiveData
一、面试题与答案
1. LiveData 是什么?核心特性有哪些?
生命周期安全的可观察数据持有容器,核心特性:
- 生命周期感知:只给活跃的观察者发送数据,页面销毁自动解除订阅(
observeForever除外) - 粘性:新注册的观察者会立即收到最新的缓存数据(页面重建会自动恢复最新数据)
- 单向数据流(ViewModel → View):View 层只订阅,不修改源数据
- 主线程安全:数据更新、回调分发默认在主线程,
postValue最终也是切到主线程调用setValue - 可用全局单例 LiveData 替换 EventBus 的部分场景,更轻量和安全
2. LiveData 是如何感知生命周期的?内部用了什么机制?
观察者模式 + Lifecycle 机制。
Fragment 和 Activity 都是 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/postValue后mVersion++mLastVersion:每个观察者内部的版本号,初始为-1
considerNotify() 中判断 observer.mLastVersion < mVersion 时才分发数据。新注册的观察者 mLastVersion = -1,只要 LiveData 设置过值(mVersion >= 0),注册时就会立刻触发回调,这就是粘性的根本原因。
observeForever 同样走版本号机制,也是粘性的。
6. 为什么 Google 把 LiveData 设计成粘性的?好处和坏处?如何解决粘性问题?
设计初衷:保证 UI 永远能拿到最新状态(页面销毁重建后自动刷新、生命周期恢复后自动刷新)。
好处:
- 页面重建无感恢复
- 延迟订阅也能拿到最新数据
- 生命周期安全联动,后台时不刷新
坏处:
- 一次性事件(弹窗、Toast、页面跳转)会被重复消费,导致路由跳转重复、弹窗重复弹出等问题
注意事项:需要区分状态(适合粘性)和事件(不适合粘性)。
解决方案:
- 封装
SingleLiveEvent - 改用
SharedFlow(replay = 0)
7. 什么是数据倒灌?和粘性事件有什么区别?哪些场景会触发?
| 粘性事件 | 数据倒灌 | |
|---|---|---|
| 本质 | 晚订阅,收到旧数据 | 已订阅,重复收到同一批数据 |
| 性质 | 官方设计,正常行为 | 副作用,属于 bug |
| 典型场景 | 新 Observer 注册 | 页面重建、Fragment 复用、重复注册观察者 |
触发数据倒灌的典型场景:
- 横竖屏切换页面重建,Observer
mLastVersion重置为-1,立刻触发旧数据回调 - 多级页面跳转,共享 ViewModel 中的 LiveData 缓存旧事件,新页面订阅后立刻收到
8. 如何解决 LiveData 的数据倒灌问题?各方案优缺点?
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 反射修改 version | 注册时将 Observer 的 mLastVersion 同步为 LiveData 的 mVersion | 通用,支持多 Observer | 反射有性能损耗,存在延迟,数据长期驻留内存 |
| SingleLiveEvent | 用 AtomicBoolean 标记事件是否已消费 | 实现简单 | 只支持单个 Observer,多 Observer 场景失效 |
| Event 包装器 | 用包装类标记 hasBeenHandled | 无侵入 | 同样不能解决多 Observer 问题 |
| UnPeekLiveData | 为每个 Observer 维护独立消费标识(HashMap) | 支持多 Observer,消费后自动清理内存 | 多 Observer 时有一定内存消耗,只适合低频推送 |
| 改用 SharedFlow | replay = 0,天然非粘性 | 彻底解决,功能强大 | 需要配合 repeatOnLifecycle 使用 |
9. MutableLiveData 和 LiveData 的区别?为什么对外暴露 LiveData 而不是 MutableLiveData?
MutableLiveData 是 LiveData 的子类,将 setValue 和 postValue 的可见性从 protected 改为 public。
对外暴露 LiveData 是开闭原则 + 单向数据流的体现:
- ViewModel 内部用
MutableLiveData修改数据 - 对外只暴露
LiveData,确保 View 层只能观察数据,不能修改数据,防止数据被外部随意篡改
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<String>()
val data: LiveData<String> = _data // 对外只读
}
10. MediatorLiveData 是什么?适用于哪些场景?
MediatorLiveData 是 LiveData 的子类,可以监听多个 LiveData 数据源,并对数据进行拦截、转发、合并处理。
适用场景:
- 同时监听多个数据源,任意一个更新就触发回调(如合并本地缓存和网络数据)
- 数据源变化频繁,只需监听前 N 次后取消订阅
distinctUntilChanged()、map()、switchMap()等操作符的底层实现都依赖它
11. LiveData 的 observe() 源码流程是什么?LifecycleBoundObserver 做了什么?
observe() 流程:
- 检测当前生命周期,若已
DESTROYED直接 return - 将 observer 包装成
LifecycleBoundObserver(绑定生命周期) - 存入 LiveData 内部的
mObservers集合 - 将
LifecycleBoundObserver注册到LifecycleRegistry,监听生命周期事件 - 注册完成后主动触发一次数据分发(
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() 三层判断:
observer.mActive:观察者是否活跃(未被移除)observer.shouldBeActive():宿主生命周期是否 ≥ STARTEDobserver.mLastVersion >= mVersion:版本号是否已是最新
三层全部通过后:更新 mLastVersion,回调 observer.mObserver.onChanged(mData)
13. 生命周期从不活跃变为活跃时,LiveData 是如何补发数据的?
LifecycleBoundObserver.onStateChanged()
→ activeStateChanged(shouldBeActive())
→ mActive = true
→ dispatchingValue(this) // this 表示只分发给当前 observer
→ considerNotify(observer)
→ 三层校验通过
→ onChanged(mData)
这就是为什么页面从后台回到前台、或者 Fragment 从不可见变为可见时,会自动收到最新数据。
14. lifecycleOwner 和 viewLifecycleOwner 的区别?Fragment 中应该用哪个?
lifecycleOwner(即 this) | viewLifecycleOwner | |
|---|---|---|
| 生命周期范围 | 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 - 疯狂
postValue:mPendingData被不断覆盖,中间值全部丢失,导致状态错乱
解决方案:
- 生产端限流(防抖、节流、
debounce) - 改用 Flow(支持背压策略:
buffer、conflate、collectLatest等)
16. LiveData 重复 setValue 相同的值,观察者会收到回调吗?如何实现防抖?
会收到。每次 setValue 都会 mVersion++,considerNotify 只判断版本号,不比较值是否相同。
防抖方案:使用官方的 distinctUntilChanged() 扩展函数,内部原理是用 MediatorLiveData 保存上一次的值,新值与旧值相同则拦截,不触发 onChanged。
val filteredLiveData = liveData.distinctUntilChanged()
17. 为什么要把 LiveData 放在 ViewModel 中,而不是直接放在 Activity/Fragment 里?
- 数据存活:ViewModel 在页面销毁重建(横竖屏切换)时不会销毁,LiveData 中的数据得以保留
- 职责分离:避免 View 层臃肿,数据逻辑收敛到 ViewModel
- 代码复用:ViewModel 可跨 Fragment 共享,LiveData 数据可被多个页面订阅
- 避免内存泄露:LiveData 持有 ViewModel 引用而非 Activity,不会因 Activity 重建导致泄露
18. LiveData 和 Flow 的区别?什么场景下选哪个?
| LiveData | Flow(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:
| LiveDataBus | EventBus | |
|---|---|---|
| 实现复杂度 | 简单,一个类搞定 | 需引入第三方库 |
| 生命周期感知 | ✅ 自动解绑 | ❌ 需手动注册/反注册 |
| 内存泄露风险 | 低 | 高(忘记反注册) |
| 跨进程 | ❌ 不支持 | ❌ 不支持 |
| 粘性事件 | 默认粘性(需处理) | 可选粘性 |
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 一直缓存旧事件。
- 粘性事件:C 页面订阅后立刻收到上一级残留的旧事件,触发意外逻辑
- 数据倒灌:多级页面层层订阅同一 LiveData,一条事件导致所有页面都触发更新
- 内存泄露:共享 ViewModel + 长期常驻 LiveData + 未及时解绑
根本原因:LiveData 的设计定位是 UI 状态容器,不是跨页面事件总线,不适合一次性事件和多级页面栈场景。
解决方案:
- 一次性事件改用
SharedFlow(replay = 0)+repeatOnLifecycle - 或使用
UnPeekLiveData解决粘性问题 - 页面间通信优先考虑
Fragment Result API或Navigation的SavedStateHandle
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 ✅