知识点 5:MVVM / MVP / MVC 对比
#MVVM#MVP#MVC#MVI
← 学习资料优先级:中
优先级:🟢 中(高频开场题,但投入产出比很高) 这道题往往是架构讨论的起点。复习重点不是背定义,而是理解几种模式各自解决了什么问题、又带来了什么代价。
核心原理讲解
💡 按照面试口语化表述,为你梳理这三种模式的演进关系和核心区别:
从 MVC 到 MVP 的演进
- MVC(Model-View-Controller):
- 职责分工:XML 充当 View,Model 负责数据,Activity 充当 Controller。
- 核心痛点:在 Android 中,View(XML)太弱了,Activity 既要处理用户点击事件(Controller 职责),又要处理 UI 渲染和动画(View 职责)。导致 Activity 变得极其臃肿(动辄上千行代码)。而且 View 和 Model 之间存在双向耦合,无法进行单元测试。
- MVP(Model-View-Presenter):
- 职责分工:完全隔离了 View 和 Model。View(Activity/Fragment)和 Presenter 之间定义了一层厚厚的“契约接口”(Contract)。View 发生操作调 Presenter 接口,Presenter 处理完调 View 接口。
- 核心痛点:Presenter 需要持有 View 的强引用,在异步回调返回前如果页面关闭了,极易导致 Activity 泄漏。另外,每个简单的交互都要定义繁琐的接口方法,导致代码量暴增,契约类异常臃肿。
待生成图片:图片 05-1:MVC、MVP 和 MVVM 架构中 View-Model-Controller 依赖与数据流向对比
从 MVP 到 MVVM / MVI 的飞跃
-
MVVM(Model-View-ViewModel):
- 核心改变:用数据双向绑定或单向观察(LiveData/Flow) 替代了 MVP 的接口回调。ViewModel 内部不持有任何 View 的引用,只向外暴露响应式数据。View 主动绑定或观察数据变化。由于 ViewModel 中没有 UI 依赖,可以直接运行在本地 JVM 上跑单元测试。
-
MVI 强化(单向数据流与单一可信数据源):
- 在传统的 MVVM 中,UI 状态可能分散在多个不同的数据流中(比如 loading、error 独立变化),在复杂场景下容易出现状态“打架”的竞态问题。
- MVI(Model-View-Intent)倡导单向数据流。将界面所有的显示状态聚合为一个统一、不可变的
UiState(通过 Kotlinsealed class表达)。UI 只能通过发送 Intent 改变 State,并接收完整的新 State 进行刷新,确保了 UI 状态的绝对一致。
待生成图片:图片 05-2:MVI 模式单向数据流 (Unidirectional Data Flow) 闭环与不可变状态 copy 机制
应用场景
复杂列表或多状态页面中,可以在 MVVM 基础上引入 MVI 风格的单向数据流。将 Loading、Error、Success 等互斥状态收敛到一个不可变 UiState 中,由 ViewModel 统一产出,UI 只负责根据状态渲染,能降低状态分散导致的不一致风险。
高频追问 + 答题要点
追问 1:MVI 既然能解决状态不一致,那为什么没有在所有的 Android 项目中彻底取代 MVVM?
- 要点框架:
- GC 压力:MVI 强调 State 不可变,每次微小状态变化(如勾选一个 CheckBox)都需要重新
copy整个 State 对象。在大而复杂的页面高频操作时,可能会造成频繁的对象创建和 GC 抖动。 - 开发开销:MVI 需要为每个动作定义相应的 Intent、Action 和全量 State,对简单的表单页面或纯展示页面来说,有些过度设计,不如 MVVM 的 LiveData 简洁。
- 重绘颗粒度:如果子 View 没有做好属性差异比对,全量 State 刷新可能会导致一些没有变化的 View 被动重复绘制。
- GC 压力:MVI 强调 State 不可变,每次微小状态变化(如勾选一个 CheckBox)都需要重新
追问 2:MVP 为什么容易出现内存泄漏?怎么解决?
- 要点框架:
- 泄漏根因:Presenter 执行了异步网络请求,内部回调隐式持有了 Presenter 实例,而 Presenter 持有着 View(Activity)的强引用。如果 Activity 销毁时网络请求没返回,Activity 就会泄漏。
待生成图片:图片 05-3:MVP 模式中 Presenter 持有 View 导致 Activity 内存泄漏链与 LifecycleObserver 修复方案
* **解决手段**:
1. 使用弱引用:Presenter 中持有 `WeakReference<View>`,每次使用前判空。
2. 绑定生命周期:让 Presenter 实现 LifecycleObserver,在 Activity 的 `onDestroy` 时置空 View 引用,并取消未完成的异步任务。
追问 3:如何对强化后的 ViewModel 进行单元测试?
- 要点框架:
- 因为 ViewModel 不持有任何 View 引用,且不依赖 Android SDK,所以可以直接跑在 JVM 单元测试上。
- 测试步骤:
- 使用
kotlinx-coroutines-test的runTest模拟协程环境。 - 向 ViewModel 发送特定的用户动作(Intent/调用方法)。
- 收集并断言
StateFlow中输出的UiState列表是否符合预期(比如是否先输出 Loading,再输出 Success)。
- 使用
当前知识缺口提示
-
[!IMPORTANT] MVI 与 Compose 的天然默契:虽然你的项目使用的是 XML 布局,但要理解,MVI 架构的“唯一不可变 UiState”与 Compose 的声明式 UI 是天然的绝配。因为声明式 UI 的本质就是 。如果以后向 Compose 迁移,你的 ViewModel 和数据流层完全不用动,只需要重写 View 层就行。