Jetpack / MVVM / Compose — 面试题篇
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
Q1: ViewModel 的原理?配置变更时为什么不会销毁?
考察点:ViewModel 存储机制
完整回答:
ViewModel 存储在 ViewModelStore(本质是 HashMap)中。Activity 实现了 ViewModelStoreOwner 接口,持有 ViewModelStore。
配置变更时,Activity 通过 onRetainNonConfigurationInstance() 将 ViewModelStore 保存到 ActivityClientRecord 中(AMS 不会清除这个数据)。新 Activity 创建后通过 getLastNonConfigurationInstance() 恢复 ViewModelStore,从而恢复所有 ViewModel。
ViewModel 的 onCleared() 只在 Activity 真正 finish 时调用(不是配置变更)。判断依据是 isChangingConfigurations() 返回 false 且 isFinishing() 返回 true。
追问:ViewModel 能保存多大的数据?
ViewModel 存在内存中,理论上没有大小限制,但要注意不要存过大的数据导致内存问题。大数据应该用 Room 或文件存储。
追问:进程被杀后 ViewModel 数据还在吗?
不在。进程被杀后 ViewModel 丢失。需要用 SavedStateHandle 将关键数据保存到 Bundle 中,进程恢复时可以恢复。Bundle 有 1MB 大小限制,只适合保存轻量数据(如 ID、搜索关键词)。
Q2: LiveData 的粘性事件问题是什么?怎么解决?
考察点:LiveData 源码理解
完整回答:
LiveData 内部有版本号机制。每次 setValue 时 mVersion++。新观察者注册时 mLastVersion 初始为 -1,小于 mVersion,所以会立即收到最后一次数据。
这在状态场景下是正确的(如 UI 状态恢复),但在事件场景下会导致问题。比如 ViewModel 中发了一个 Toast 事件,旋转屏幕后新 Observer 注册又收到这个事件,Toast 重复弹出。
解决方案:
- SharedFlow(replay=0)(推荐):不缓存历史数据,新收集者不会收到旧事件
- Channel:一次性消费,适合导航、Toast 等事件
- Event 包装类:用
content+hasBeenHandled标记是否已消费 - 反射修改 observer 的 mLastVersion(hack,不推荐)
追问:LiveData 的 postValue 和 setValue 区别?
setValue 只能在主线程调用,立即分发。postValue 可以在任意线程调用,通过 Handler post 到主线程执行。注意:短时间内多次 postValue,只有最后一次的值会被分发(中间值被覆盖)。
Q3: MVVM 和 MVI 的区别?你在项目中怎么选择?
考察点:架构模式理解
完整回答:
MVVM:View 观察 ViewModel 中的多个 LiveData/StateFlow,数据流可以是多向的。
MVI:单向数据流。View 发送 Intent → ViewModel 处理 → 产生新的不可变 State → View 渲染。所有 UI 状态集中在一个 State 对象中。
| 维度 | MVVM | MVI |
|---|---|---|
| 状态管理 | 分散(多个 LiveData) | 集中(单一 State) |
| 数据流 | 多向 | 单向 |
| 可预测性 | 一般 | 强 |
| 调试 | 需要追踪多个数据源 | 状态变化清晰可追溯 |
| 复杂度 | 低 | 较高 |
选择建议:
- 简单页面(列表展示、表单):MVVM 足够
- 复杂交互(多状态联动、需要状态回溯):MVI 更合适
- 团队规范统一比选哪个更重要
追问:MVI 的缺点?
- State 对象可能很大,每次都创建新对象有性能开销(可以用 data class copy 优化)
- 简单页面用 MVI 过度设计
- 需要定义大量 Intent 和 State 类
Q4: Compose 的重组原理?怎么优化性能?
考察点:Compose 底层机制
完整回答:
Compose 使用 Slot Table 存储组合树的状态。当 State 变化时,Compose 标记受影响的重组作用域(Scope),只重新执行这些 Composable 函数,比较新旧参数决定是否更新 UI。
重组的关键:
- Compose 编译器为每个 Composable 生成一个 group key(基于源码位置)
- 参数不变的 Composable 会被跳过(skip)
- 只有
@Stable或@Immutable标记的类型,或基本类型,才能被正确比较
性能优化:
- 缩小重组范围:将读取 State 的代码放在尽可能小的 Composable 中
- 使用 remember:缓存计算结果,避免重组时重复计算
- derivedStateOf:将多个 State 合并为一个派生 State,减少不必要的重组
- key():在列表中为 item 指定稳定的 key,避免不必要的重组
- @Stable/@Immutable:标记数据类,让 Compose 知道可以安全跳过
- 避免在 Composable 中创建 lambda:lambda 每次创建新对象会导致子 Composable 认为参数变了
// ❌ 每次重组都创建新 lambda
items.forEach { item ->
ItemView(onClick = { viewModel.onItemClick(item) })
}
// ✅ 使用 remember 缓存 lambda
items.forEach { item ->
val onClick = remember(item.id) { { viewModel.onItemClick(item) } }
ItemView(onClick = onClick)
}追问:Compose 和 View 系统可以混用吗?
可以。AndroidView 在 Compose 中嵌入传统 View,ComposeView 在传统布局中嵌入 Compose。适合渐进式迁移。
Q5: Lifecycle 的原理?怎么实现生命周期感知的?
考察点:Lifecycle 组件
完整回答:
Activity/Fragment 实现了 LifecycleOwner 接口,内部持有 LifecycleRegistry(Lifecycle 的实现类)。
在 Activity 中,通过注入一个无 UI 的 ReportFragment 来监听生命周期回调。ReportFragment 在各个生命周期方法中调用 LifecycleRegistry.handleLifecycleEvent(),分发事件给所有注册的 Observer。
LifecycleRegistry 维护了一个 Observer 列表和当前 State。添加新 Observer 时,会将其状态同步到当前 State(依次分发中间事件)。
追问:为什么用 ReportFragment 而不是直接在 Activity 中分发?
为了兼容不继承 AppCompatActivity 的场景。ReportFragment 是透明的,不影响 UI,且可以在任何 Activity 中注入。
Q6: Room 和 SharedPreferences 的区别?什么时候用 Room?
考察点:存储方案选型
完整回答:
SharedPreferences:
- 键值对存储,适合少量简单配置
- 全量读写 XML 文件,数据量大时性能差
- 不支持复杂查询
- ANR 风险:
apply()在 Activity onStop 时可能同步写入
Room:
- SQLite 的抽象层,适合结构化数据
- 编译期 SQL 验证,类型安全
- 支持 Flow/LiveData 观察数据变化
- 支持复杂查询、关联表、Migration
选择:
- 用户设置、token、简单标志位 → MMKV(替代 SP)
- 结构化数据、需要查询/排序/关联 → Room
- 大量键值对 → MMKV
加分点:提到 DataStore 是 Google 推荐的 SP 替代方案,基于 Flow,支持 Proto 序列化,线程安全。
Q7: WorkManager 的原理?和 Service 有什么区别?
考察点:后台任务调度
完整回答:
WorkManager 用于可延迟的、需要保证执行的后台任务。即使应用退出或设备重启,任务也会执行。
原理:
- 任务信息持久化到 Room 数据库
- 根据 API 级别选择底层实现:API 23+ 用 JobScheduler,低版本用 AlarmManager + BroadcastReceiver
- 支持约束条件(网络、电量、存储空间)
- 支持链式任务、周期任务
和 Service 的区别:
- Service 是四大组件,有自己的生命周期,适合需要立即执行的前台任务
- WorkManager 是任务调度框架,适合可延迟的后台任务
- WorkManager 保证任务最终执行,Service 可能被系统杀死
- WorkManager 自动处理 Doze 模式和省电限制
追问:WorkManager 的任务类型?
- OneTimeWorkRequest:一次性任务
- PeriodicWorkRequest:周期任务(最小间隔 15 分钟)
- 支持 ExistingWorkPolicy:REPLACE/KEEP/APPEND
Q8: Compose 中的 remember 和 rememberSaveable 的区别?
考察点:Compose 状态管理
完整回答:
remember:在重组时保持状态,但配置变更(旋转屏幕)时丢失rememberSaveable:在重组和配置变更时都保持状态(内部使用 Bundle 保存)
// remember:旋转屏幕后 count 重置为 0
var count by remember { mutableStateOf(0) }
// rememberSaveable:旋转屏幕后 count 保持
var count by rememberSaveable { mutableStateOf(0) }rememberSaveable 只能保存 Bundle 支持的类型。自定义类型需要实现 Saver:
val userSaver = Saver<User, Bundle>(
save = { bundleOf("name" to it.name, "age" to it.age) },
restore = { User(it.getString("name")!!, it.getInt("age")) }
)
var user by rememberSaveable(stateSaver = userSaver) { mutableStateOf(User("", 0)) }Q9: Compose 的 LazyColumn 和 RecyclerView 有什么区别?
考察点:Compose 列表
完整回答:
LazyColumn 是 Compose 中的懒加载列表,类似 RecyclerView:
| 维度 | LazyColumn | RecyclerView |
|---|---|---|
| 声明方式 | 声明式 | 命令式(Adapter + ViewHolder) |
| 复用机制 | 组合复用(Slot Table) | ViewHolder 复用(四级缓存) |
| 布局管理 | 内置(LazyColumn/LazyRow/LazyGrid) | LayoutManager |
| 动画 | animateItemPlacement | ItemAnimator |
| 性能 | 略低(Compose 开销) | 略高(成熟优化) |
LazyColumn 性能优化:
- 为 item 指定
key(避免不必要的重组) - 避免在 item 中创建复杂的 lambda
- 使用
contentType帮助复用 - 大列表考虑
@Stable标记数据类
LazyColumn {
items(
items = users,
key = { it.id }, // 稳定的 key
contentType = { "user" } // 内容类型,帮助复用
) { user ->
UserItem(user)
}
}Q10: DataBinding 和 ViewBinding 的区别?
考察点:视图绑定
完整回答:
| 维度 | ViewBinding | DataBinding |
|---|---|---|
| 功能 | 类型安全的 findViewById 替代 | ViewBinding + 数据绑定表达式 |
| 布局文件 | 普通 XML | 需要 <layout> 标签包裹 |
| 编译速度 | 快 | 慢(需要处理绑定表达式) |
| 双向绑定 | 不支持 | 支持(@={}) |
| 表达式 | 不支持 | 支持(android:text="@{user.name}") |
| 推荐度 | 推荐(简单场景) | 逐渐被 Compose 替代 |
现在的趋势:新项目用 Compose,老项目用 ViewBinding 替代 findViewById,DataBinding 逐渐不再推荐(复杂度高、调试困难)。
实习面试补充:Jetpack 与 MVVM 入门高频题
实习面试通常不要求你手写完整架构,但会问 ViewModel 为什么能解决问题、LiveData/Flow 怎么观察、项目为什么用 MVVM。
Q11: ViewModel 的作用是什么?
考察点:生命周期感知、页面状态管理
完整回答:
ViewModel 用来保存和管理页面相关的数据,特点是生命周期比 Activity/Fragment 的 View 更长,配置变更(如旋转屏幕)时不会立即销毁,因此可以避免页面重建后数据丢失。
典型职责:
- 保存 UI 状态。
- 调用 Repository 获取数据。
- 对外暴露 LiveData/StateFlow 给 UI 观察。
- 避免把业务逻辑全部写在 Activity/Fragment 中。
追问:ViewModel 能持有 Activity 引用吗?
不应该持有 Activity、Fragment、View 这类生命周期较短的对象引用,否则可能导致内存泄漏。如果确实需要 Context,可以谨慎使用 AndroidViewModel 的 Application Context。
Q12: LiveData 和 StateFlow 有什么共同点?
考察点:响应式 UI 状态
完整回答:
LiveData 和 StateFlow 都可以用来向 UI 暴露可观察状态,数据变化时通知界面刷新。
- LiveData 生命周期感知能力强,和传统 Android 组件结合简单。
- StateFlow 属于 Kotlin Flow 体系,适合协程和单向数据流,必须有初始值。
实习项目中如果是 Java/老项目,用 LiveData 很常见;如果是 Kotlin + 协程项目,StateFlow 更常见。
加分点:在 Fragment 中收集 Flow 时,要结合 repeatOnLifecycle,避免页面不可见时仍然收集。
Q13: MVVM 中 Model、View、ViewModel 分别负责什么?
考察点:架构分层
完整回答:
- View:Activity/Fragment/Compose UI,负责展示状态和接收用户操作。
- ViewModel:保存 UI 状态,处理页面逻辑,调用数据层。
- Model:数据来源,包括 Repository、网络、数据库、本地缓存等。
MVVM 的核心价值是降低 UI 和数据逻辑耦合,让 Activity/Fragment 更薄,代码更容易测试和维护。
追问:MVVM 是不是一定要用 Repository?
不是语法强制,但实际项目中推荐使用 Repository 隔离数据来源,让 ViewModel 不直接关心数据来自网络还是数据库。
