iOS应用架构设计:MVC、MVVM、VIPER对比|一个深夜刷题工程师的血泪总结
上周五晚上 11 点,我还在公司对着 Xcode 调一个诡异的内存泄漏问题——又是 View Controller 持有太多业务逻辑,导致 dealloc 根本不触发。那一刻我真的想把 MacBook 直接砸了,但一想到下周还要交 LeetCode 周赛打卡(跳槽人设不能崩),只能默默泡了杯速溶咖啡,继续肝。
我是小红书推荐算法组干了两年的 iOS 工程师(没错,算法岗也要写 App!)。虽然主业是搞用户增长模型,但因为团队人少活多,经常被拉去救火客户端项目。最近准备跳槽,除了狂刷《剑指 Offer》和《算法导论》这种硬核书籍,也得回头补一补基础架构知识——毕竟大厂面试官最爱问“你们 App 用啥架构”。
今天就来盘一盘 iOS 三大经典架构:MVC、MVVM、VIPER。这不是教科书式的理论堆砌,而是我在真实项目里踩过坑、熬过夜、被产品经理追着改需求后总结的最佳实践。
为啥要关心架构?因为烂代码会杀人
去年双11期间,我们团队紧急上线一个“种草心愿单”功能。当时为了赶 deadline,直接在 ViewController 里塞了网络请求、数据解析、埋点上报、甚至部分推荐逻辑……结果上线第三天就 crash 率飙升,Crashlytics 里全是 EXC_BAD_ACCESS。
测试同学疯狂 at 我:“你这个 VC 有 800 行,还持有 URLSession、Timer、还有 JSBridge 回调???”
我:……(内心 OS:这锅我不背,是产品说“先上线再说”)
痛定思痛,我们决定重构。但选哪种架构?MVC 太老,MVVM 听起来高大上,VIPER 又感觉过度设计。于是花了两周时间,在三个分支分别实现同一功能,横向对比效果。
MVC:苹果亲儿子,但容易变“Massive VC”
MVC 是 Apple 官方推荐的架构,Xcode 模板默认就是它。简单说就是:
- Model:数据层(比如 User、Product)
- View:UI 层(UILabel、UIButton)
- Controller:协调者(UIViewController)
听起来很美好,但现实是:90% 的逻辑都塞进了 ViewController。为什么?因为 View 不能直接和 Model 通信,所有交互必须经过 VC。久而久之,VC 就成了“上帝类”。
// 典型的 Massive VC 写法(别学!)
class ProductDetailViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
fetchProduct { [weak self] product in
self?.nameLabel.text = product.name
self?.priceLabel.text = "$\(product.price)"
Analytics.logEvent("view_product", params: ["id": product.id]) // 埋点?
if product.isRecommendable {
self?.showRecommendBanner() // 推荐逻辑?
}
}
}
func fetchProduct(completion: @escaping (Product) -> Void) {
// 网络请求 + JSON 解析(可能还用了 SwiftyJSON)
}
}
问题在哪?
- 测试困难:VC 依赖 UIKit,单元测试几乎不可能
- 复用性差:换个页面就得复制粘贴 500 行
- 内存泄漏高发区:闭包没 weak self、delegate 没置 nil……
📌 我的建议:MVC 可以用,但必须严格约束 VC 职责——只做 UI 绑定和生命周期管理,其他统统抽出去!
MVVM:解耦神器,但别被 RxSwift 带偏了
被领导逼着学 MVVM 那会儿,我差点以为自己要转行做前端——满屏的 Observable、bind(to:)、DisposeBag……好家伙,这不就是用 Swift 写 React 吗?
但冷静下来发现,核心思想其实很简单:
- View:纯 UI,被动接收数据
- ViewModel:处理业务逻辑,暴露可绑定的数据(比如
Published变量) - Model:不变
关键在于:View 和 ViewModel 通过数据绑定通信,而不是直接调用方法。
// SwiftUI + Combine 版 MVVM(更符合 Apple 生态)
class ProductDetailViewModel: ObservableObject {
@Published var productName = ""
@Published var productPrice = ""
@Published var shouldShowRecommend = false
private let productService: ProductService
init(productService: ProductService) {
self.productService = productService
}
func loadProduct(id: String) {
productService.fetch(id: id) { [weak self] result in
switch result {
case .success(let product):
self?.productName = product.name
self?.productPrice = "$\(product.price)"
self?.shouldShowRecommend = product.isRecommendable
Analytics.logEvent("view_product", params: ["id": product.id])
case .failure(let error):
print("Load failed: \(error)")
}
}
}
}
// View 层(SwiftUI)
struct ProductDetailView: View {
@ObservedObject var viewModel: ProductDetailViewModel
var body: some View {
VStack {
Text(viewModel.productName)
Text(viewModel.productPrice)
if viewModel.shouldShowRecommend {
RecommendBanner()
}
}
.onAppear {
viewModel.loadProduct(id: "123")
}
}
}
优点很明显:
- ViewModel 无 UIKit 依赖,单元测试轻松写
- 逻辑集中,复用方便(比如 H5 页面也能用同一个 VM)
- SwiftUI 天然契合 MVVM(Apple 自己推的组合拳)
但注意两个坑:
- 别为了“响应式”而响应式!简单场景用
@Published+assign就够了,别一上来就 RxSwift,增加维护成本。 - JSBridge 调用要小心:我们有个需求是从 WebView 调原生方法,一开始把 JS 回调写在 ViewModel 里,结果内存炸了——WebView 持有 VM 引用,VM 又持有 WebView,循环引用!最后拆成独立的
WebBridgeManager才解决。
📌 我的建议:MVVM + SwiftUI 是当前最优解,尤其适合新项目。老项目迁移可逐步替换,别追求一步到位。
VIPER:架构洁癖患者的终极选择?
VIPER 听起来像特工代号,实际是:
- View:只负责 UI
- Interactor:业务逻辑
- Presenter:View 和 Interactor 的粘合剂
- Entity:数据模型
- Router:页面跳转
每个模块严格单向依赖,测试覆盖率能冲到 90%+。但代价是:文件数量爆炸。一个简单页面要建 5 个文件!
// 仅展示 Presenter 和 Interactor 的交互
protocol ProductDetailPresenterOutput: AnyObject {
func displayProductName(_ name: String)
}
class ProductDetailInteractor {
weak var output: ProductDetailInteractorOutput?
private let service: ProductService
func fetchProduct(id: String) {
service.fetch(id: id) { [weak self] result in
if case .success(let product) = result {
self?.output?.display(product: product) // 回调给 Presenter
}
}
}
}
class ProductDetailPresenter {
weak var viewController: ProductDetailDisplayLogic?
private let interactor: ProductDetailInteractor
func viewDidLoad() {
interactor.fetchProduct(id: "123")
}
func display(product: Product) {
viewController?.displayProductName(product.name)
// ...其他 UI 更新
}
}
适合谁用?
- 金融、医疗等对稳定性要求极高的 App
- 团队有专职架构师,能 hold 住复杂度
- 你享受写 Protocol 和 delegate 的快感(不是)
不适合谁?
- 小团队(我们组 3 个 iOS,根本没人 review 这么多文件)
- 快速迭代项目(产品经理每天改需求,VIPER 改到哭)
- 我这种边刷题边干活的打工人(时间不够啊!)
横向对比:别光听概念,看真实数据
| 维度 | MVC | MVVM | VIPER |
|---|---|---|---|
| 上手难度 | ⭐ | ⭐⭐ | ⭐⭐⭐⭐ |
| 代码量 | 少(但集中在 VC) | 中 | 极多 |
| 测试友好度 | 差 | 好 | 极好 |
| SwiftUI 兼容 | 一般 | 完美 | 需改造 |
| 适合团队规模 | 1~2 人 | 2~10 人 | 10+ 人 |
| 我的推荐指数 | 🌕(慎用) | 🌕🌕🌕🌕🌕 | 🌕🌕 |
最后说点掏心窝子的话
作为既要搞算法又要救火客户端的苦命人,我的结论很务实:
- 新项目:直接 MVVM + SwiftUI,拥抱 Apple 生态,审核也更稳(App Store 最近严查滥用 UIKit)
- 老项目:别盲目重构!先把 VC 里的网络、埋点、业务逻辑抽成独立 Manager,再逐步引入 ViewModel
- 别炫技:我见过有人在简单设置页用 VIPER,结果新人入职一周还没搞懂怎么改个按钮文案……
对了,最近在啃《iOS 应用安全与逆向工程》这本书(跳槽面安全岗备选),发现架构混乱的 App 更容易被 hook——因为逻辑都暴露在 VC 里。所以啊,好的架构不仅是工程问题,更是安全防线。
写完这篇,LeetCode 还剩两题……程序员的命也是命,但 deadline 不等人。共勉!
(PS:如果你也在深夜刷题/改 Bug,评论区抱团取暖吧!)

评论 0