从双11崩溃到架构重构:一个考公程序员的 MVVM 实战血泪史

独立开发小站
2026-01-06 10:09
阅读 2612

上周五晚上十一点,我正窝在 MacBook Pro 前刷行测题,手机突然疯狂震动——公司 App 在双11大促期间崩了。作为主力开发之一,我一边心里默念“稳住别慌”,一边火速切回 Xcode,结果看到控制台满屏的 EXC_BAD_ACCESS 和用户投诉截图。那一刻,我真的想砸电脑。

说来惭愧,我在这家公司干了三年多,主要负责移动端业务模块。产品迭代快、需求杂、运营天天催,代码早就成了“意大利面条”。之前为了赶上线,ViewModel 里塞满了网络请求、本地缓存、甚至还有埋点逻辑。这次崩溃,不过是压垮骆驼的最后一根稻草。

更讽刺的是,我最近其实已经在偷偷准备考公——不是对编程没爱了,而是实在受够了这种“救火队员”式的工作节奏。但临走前,总得给团队留下点像样的东西吧?于是,我主动请缨,牵头重构核心模块,采用 MVVM 架构。一来是技术债不能留给下一任,二来……也是给自己简历添点干货,万一考不上还能跳槽呢(狗头保命)。


为什么是 MVVM?不就是换个名字吗?

刚开始提 MVVM,产品经理一脸懵:“这玩意儿能让我明天上线新活动吗?”测试同事翻白眼:“又改架构?上次改完回归测了三天!”连隔壁 Java 后端都调侃:“你是不是 Rust 写多了,以为所有问题都能靠 ownership 解决?”

说实话,我一开始也怀疑:MVVM 真的比 MVP 或 MVC 强?还是只是前端圈的新潮流?

但仔细一想,我们的问题很典型:

  • UI 逻辑和业务逻辑高度耦合:一个按钮点击,既要调接口、又要更新本地状态、还要上报行为数据。
  • 测试几乎为零:因为所有逻辑都在 ViewController 里,Mock 数据难如登天。
  • 跨平台适配痛苦:iOS 和 Android 代码结构不一致,同一个需求要写两套逻辑。

MVVM 的核心思想是 分离关注点(Separation of Concerns),把 UI 层(View)和业务逻辑层(ViewModel)解耦,通过数据绑定自动同步状态。听起来很虚?举个真实例子:

以前,用户登录成功后,我们要手动:

// ViewController 里的地狱代码
if loginResponse.success {
    nameLabel.text = response.name
    avatarImageView.image = loadImage(response.avatarUrl)
    UserDefaults.save(token: response.token)
    Analytics.track("login_success")
    navigateToHome()
} else {
    showErrorAlert()
}

而在 MVVM 中,ViewModel 只负责暴露 userName: String?avatarURL: URL? 等可观察属性,View 层通过绑定自动更新 UI。ViewModel 不知道 View 的存在,自然也就更容易单元测试。


工具选型:Swift + Combine 还是 RxSwift?

作为 Mac 党,我当然首选 Apple 官方方案。iOS 13 起引入的 Combine 框架,本质上就是响应式编程(Reactive Programming)的官方实现,和 MVVM 天然契合。

虽然 RxSwift 社区更成熟,但考虑到:

  • 公司最低支持 iOS 14
  • 团队新人多,学习曲线要平缓
  • 我自己也在用 Swift 写一些小工具练手

最终拍板:Swift + Combine + MVVM

不过现实很快打脸——Combine 的文档少得可怜,错误信息还特别抽象。比如这个经典报错:

Publishers.Share<Publishers.ReceiveOn<Publishers.Map<...>>> has no interface

我当时盯着屏幕愣了十分钟,最后发现是因为忘了 .eraseToAnyPublisher()。这种坑,踩一次就长记性。


实战:重构用户中心模块

我们先拿“用户中心”开刀——这是 App 里最复杂的页面之一,包含头像、昵称、会员状态、订单入口、设置项等,且数据来自多个 API。

目录结构设计

我参考了 SwiftUI 的分层思想,但保留 UIKit(毕竟老项目):

/UserProfile/
├── View/
│   ├── UserProfileViewController.swift
│   └── UserProfileView.xib
├── ViewModel/
│   ├── UserProfileViewModel.swift
│   └── UserProfileState.swift
├── Model/
│   ├── User.swift
│   └── Membership.swift
└── Service/
    ├── UserService.swift
    └── ImageService.swift

关键原则:ViewModel 不引用任何 UIKit 组件,只处理数据和状态。

ViewModel 的核心:状态驱动

我定义了一个 UserProfileState 结构体,用枚举表示加载状态:

struct UserProfileState {
    enum LoadingStatus {
        case idle
        case loading
        case success(User)
        case failure(Error)
    }
    
    var userInfo: LoadingStatus = .idle
    var avatarImage: UIImage?
}

ViewModel 内部使用 @Published 属性包装器,让状态变化自动通知 View:

class UserProfileViewModel: ObservableObject {
    @Published private(set) var state = UserProfileState()
    
    private let userService: UserServiceProtocol
    
    init(userService: UserServiceProtocol = UserService()) {
        self.userService = userService
    }
    
    func loadUserProfile() {
        state.userInfo = .loading
        
        userService.fetchUser()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    if case .failure(let error) = completion {
                        self?.state.userInfo = .failure(error)
                    }
                },
                receiveValue: { [weak self] user in
                    self?.state.userInfo = .success(user)
                    // 同时触发头像加载
                    self?.loadAvatar(for: user.avatarURL)
                }
            )
            .store(in: &cancellables)
    }
}

注意这里用了 [weak self] 避免循环引用,cancellables 是一个 Set<AnyCancellable>,用于自动取消订阅。

View 层:只关心“怎么展示”,不关心“数据哪来”

在 ViewController 里,我们通过 assignsink 订阅状态变化:

class UserProfileViewController: UIViewController {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var avatarImageView: UIImageView!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
    
    private let viewModel = UserProfileViewModel()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
        viewModel.loadUserProfile()
    }
    
    private func bindViewModel() {
        viewModel.$state
            .map(\.userInfo)
            .sink { [weak self] status in
                switch status {
                case .loading:
                    self?.loadingIndicator.startAnimating()
                case .success(let user):
                    self?.nameLabel.text = user.name
                    self?.loadingIndicator.stopAnimating()
                case .failure(let error):
                    self?.showError(error)
                    self?.loadingIndicator.stopAnimating()
                default: break
                }
            }
            .store(in: &cancellables)
            
        // 头像绑定
        viewModel.$state
            .compactMap { $0.avatarImage }
            .assign(to: \.image, on: avatarImageView)
            .store(in: &cancellables)
    }
}

看,ViewController 几乎没有业务逻辑!它只是一个“胶水层”,负责把 ViewModel 的状态“粘”到 UI 上。


跨平台适配与性能优化

Android 同事看了我们的架构,直呼内行。他们用 Kotlin + LiveData 也搞了一套类似的,两边共享 ModelService 层的设计思路。运营同学终于不用再问“iOS 和 Android 为什么表现不一致”了。

性能方面,我们做了几件事:

  1. 图片懒加载 + 缓存:用 ImageService 封装 Kingfisher,ViewModel 只传 URL。
  2. 避免主线程阻塞:所有网络和解析操作在后台队列,通过 receive(on: .main) 切回主线程更新 UI。
  3. 内存泄漏防护:ViewModel 持有 cancellables,ViewController 销毁时自动取消订阅。

线上监控数据显示,用户中心页面的平均加载时间从 1.8s 降到 0.6s,Crash 率下降 72%。运维大哥难得夸了句:“这次上线挺稳。”


开发心得:MVVM 不是银弹,但值得投入

经过两个月的实战,我对 MVVM 有了更深的理解:

优势 挑战
逻辑清晰,新人上手快 初期样板代码多
单元测试覆盖率提升至 65% 响应式思维需要适应
UI 与逻辑解耦,便于 AB Test 调试链路变长
支持 SwiftUI 平滑迁移 Combine 生态不如 Rx 成熟

最大的收获不是技术本身,而是团队协作方式的改变。现在产品经理提需求,会先问:“这个状态怎么表示?”测试同学可以直接 mock ViewModel 返回假数据。就连运营做活动配置,也能通过调整状态枚举快速验证效果。

当然,也有翻车时刻。有一次我忘了在 receive(on:) 里指定队列,导致 JSON 解析卡住主线程,用户滑动列表直接掉帧。被 QA 抓到后,我默默在 Confluence 上加了条规范:“所有异步操作必须显式指定调度队列”。


给同行的建议:别为了模式而模式

MVVM 的本质是 解耦,不是堆砌框架。如果你的页面只有两个按钮,硬套 MVVM 反而是过度设计。

我的经验是:

  • 从复杂页面开始试点,比如个人中心、订单详情
  • 先定义好 State 结构,再写 ViewModel
  • 善用工具:Xcode 的 Memory Graph Debugger 查内存泄漏,Instruments 测性能
  • 别怕重构:技术债越拖越贵,趁还能改,赶紧动

写这篇文章的时候,我已经提交了国考报名表。如果上岸,这段 MVVM 实战经历大概率会变成“前程序员的回忆”;如果没上岸,至少简历上能写“主导核心模块架构升级,性能提升 300%”。

不管结果如何,这次重构让我明白:好的架构不是写出来的,是用时间和 bug 喂出来的。就像我最近学的 Rust —— 所有权模型看着反人类,但真能帮你避开很多坑。编程如此,人生亦如此。

共勉。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝