Skip to content

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 重复弹出。

解决方案:

  1. SharedFlow(replay=0)(推荐):不缓存历史数据,新收集者不会收到旧事件
  2. Channel:一次性消费,适合导航、Toast 等事件
  3. Event 包装类:用 content + hasBeenHandled 标记是否已消费
  4. 反射修改 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 对象中。

维度MVVMMVI
状态管理分散(多个 LiveData)集中(单一 State)
数据流多向单向
可预测性一般
调试需要追踪多个数据源状态变化清晰可追溯
复杂度较高

选择建议:

  • 简单页面(列表展示、表单):MVVM 足够
  • 复杂交互(多状态联动、需要状态回溯):MVI 更合适
  • 团队规范统一比选哪个更重要

追问:MVI 的缺点?

  1. State 对象可能很大,每次都创建新对象有性能开销(可以用 data class copy 优化)
  2. 简单页面用 MVI 过度设计
  3. 需要定义大量 Intent 和 State 类

Q4: Compose 的重组原理?怎么优化性能?

考察点:Compose 底层机制

完整回答

Compose 使用 Slot Table 存储组合树的状态。当 State 变化时,Compose 标记受影响的重组作用域(Scope),只重新执行这些 Composable 函数,比较新旧参数决定是否更新 UI。

重组的关键:

  • Compose 编译器为每个 Composable 生成一个 group key(基于源码位置)
  • 参数不变的 Composable 会被跳过(skip)
  • 只有 @Stable@Immutable 标记的类型,或基本类型,才能被正确比较

性能优化:

  1. 缩小重组范围:将读取 State 的代码放在尽可能小的 Composable 中
  2. 使用 remember:缓存计算结果,避免重组时重复计算
  3. derivedStateOf:将多个 State 合并为一个派生 State,减少不必要的重组
  4. key():在列表中为 item 指定稳定的 key,避免不必要的重组
  5. @Stable/@Immutable:标记数据类,让 Compose 知道可以安全跳过
  6. 避免在 Composable 中创建 lambda:lambda 每次创建新对象会导致子 Composable 认为参数变了
kotlin
// ❌ 每次重组都创建新 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 保存)
kotlin
// remember:旋转屏幕后 count 重置为 0
var count by remember { mutableStateOf(0) }

// rememberSaveable:旋转屏幕后 count 保持
var count by rememberSaveable { mutableStateOf(0) }

rememberSaveable 只能保存 Bundle 支持的类型。自定义类型需要实现 Saver:

kotlin
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:

维度LazyColumnRecyclerView
声明方式声明式命令式(Adapter + ViewHolder)
复用机制组合复用(Slot Table)ViewHolder 复用(四级缓存)
布局管理内置(LazyColumn/LazyRow/LazyGrid)LayoutManager
动画animateItemPlacementItemAnimator
性能略低(Compose 开销)略高(成熟优化)

LazyColumn 性能优化:

  • 为 item 指定 key(避免不必要的重组)
  • 避免在 item 中创建复杂的 lambda
  • 使用 contentType 帮助复用
  • 大列表考虑 @Stable 标记数据类
kotlin
LazyColumn {
    items(
        items = users,
        key = { it.id },           // 稳定的 key
        contentType = { "user" }   // 内容类型,帮助复用
    ) { user ->
        UserItem(user)
    }
}

Q10: DataBinding 和 ViewBinding 的区别?

考察点:视图绑定

完整回答

维度ViewBindingDataBinding
功能类型安全的 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 不直接关心数据来自网络还是数据库。