</>FUNNYGUYZ

知识点 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(通过 Kotlin sealed 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 被动重复绘制。

追问 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 单元测试上。
    • 测试步骤
      1. 使用 kotlinx-coroutines-testrunTest 模拟协程环境。
      2. 向 ViewModel 发送特定的用户动作(Intent/调用方法)。
      3. 收集并断言 StateFlow 中输出的 UiState 列表是否符合预期(比如是否先输出 Loading,再输出 Success)。

当前知识缺口提示

  • [!IMPORTANT] MVI 与 Compose 的天然默契:虽然你的项目使用的是 XML 布局,但要理解,MVI 架构的“唯一不可变 UiState”与 Compose 的声明式 UI 是天然的绝配。因为声明式 UI 的本质就是 UI=f(State)UI = f(State)。如果以后向 Compose 迁移,你的 ViewModel 和数据流层完全不用动,只需要重写 View 层就行。