移动应用架构怎么搞才不翻车?我在滴滴用 MVVM 重构司机端的血泪史
凌晨两点,杭州西溪园区的灯还亮着几盏。我刚 fix 了一个线上紧急 bug,顺手泡了杯速溶咖啡——别笑,真不是星巴克,是楼下便利店十块钱三包的那种。作为在滴滴干了四年后端的老兵,最近却被推去搞移动架构优化,说是为了“打通前后端体验”。行吧,反正 Rust 也没研究完,先来填这个坑。
事情得从去年双 11说起。那会儿我们司机端 App 的崩溃率突然飙升,用户反馈“接单卡成 PPT”,运营同学天天在群里@我:“你们后端接口是不是又挂了?” 我一脸懵:接口 QPS 正常,响应时间毫秒级,锅真不在我们这儿。后来拉上前端兄弟一起排查,才发现问题出在UI 线程被业务逻辑拖垮了——整个页面状态全靠一个巨型 Activity 控制,数据一多就卡死。
产品经理还不忘补刀:“能不能快点优化?下个月要推新功能,司机要是用不了,KPI 就没了。” 好家伙,这压力,比写年终述职报告还大。
为什么是 MVVM?
说实话,一开始我对移动端架构没那么上心。毕竟我主职是后端,写 Java、Go、偶尔撸点 Rust,觉得前端嘛,不就是 div + css + js 嘛(别打我,我知道错了)。但这次事故让我意识到:移动端不是展示层,而是用户体验的第一道防线。
我们技术负责人拍板:必须重构,用 MVVM。理由很实在:
- 解耦 UI 和业务逻辑:别再让 View 直接调接口、处理状态
- 便于测试:ViewModel 可以单元测试,不用每次都开模拟器
- 支持多平台:iOS 和 Android 能共享部分逻辑(虽然最后只共享了协议)
- 提升性能:避免主线程做重活,配合 LiveData 或 StateFlow 实现高效刷新
我当时还嘀咕:“MVVM 不是前端玩的吗?我们 Android 用这个合适?” 结果被反问一句:“你见过哪个现代 App 还用纯 MVC 的?” 好吧,时代变了。
踩坑实录:从“我以为”到“我裂开”
坑一:LiveData 到底 live 在哪?
刚开始我们直接照搬网上教程,ViewModel 里放一堆 MutableLiveData,Activity 里 observe 它。看起来很美,直到测试同学跑来说:“切后台再回来,数据没了!”
查了半天才发现:LiveData 默认只对活跃的 Observer 生效。Activity 进入 onStop 状态后,就不会收到更新。而我们有些业务需要在后台持续监听订单状态(比如司机接单后等待乘客上车),结果数据断了,界面卡在“正在接单”……
解决方案?改用 StateFlow(Kotlin 协程生态)或者自定义 LiveData 的 observeForever。但我们团队 Kotlin 水平参差不齐,最后折中:关键路径用 StateFlow,非关键用 Lifecycle-aware 的 observe。
// 错误示范:后台收不到更新
viewModel.orderStatus.observe(this) { status ->
updateUI(status)
}
// 正确姿势(Kotlin + 协程)
lifecycleScope.launch {
viewModel.orderStatusFlow.collect { status ->
updateUI(status)
}
}
坑二:ViewModel 生命周期谁管?
有一次我自信满满上线新版本,结果第二天报警:内存泄漏!LeakCanary 抓到 ViewModel 持有了 Context。
原因是我图省事,在 ViewModel 里直接注入了 Activity 的 context 去弹 Toast。ViewModel 的生命周期比 Activity 长,它可能在配置变更(比如横竖屏切换)时被复用,但持有的旧 context 已经销毁,导致泄漏。
教训:ViewModel 绝对不能持有 View 或 Context 引用!所有 UI 相关操作必须通过回调或事件暴露出去。我们后来封装了一个 UiEvent 类:
sealed class UiEvent {
data class ShowToast(val msg: String) : UiEvent()
object NavigateToProfile : UiEvent()
}
// 在 Activity 中监听
viewModel.uiEvents.observe(this) { event ->
when (event) {
is ShowToast -> Toast.makeText(this, event.msg, Toast.LENGTH_SHORT).show()
NavigateToProfile -> startActivity(Intent(this, ProfileActivity::class.java))
}
}
坑三:数据源太多,状态管理爆炸
司机端有多个数据源:订单状态、定位信息、账户余额、系统通知……早期代码里,每个数据都单独 observe,导致 UI 刷新频繁且不一致。比如订单状态更新了,但余额还没拉到,界面上显示“收入 0 元”,司机直接炸毛。
我们引入了 Single Source of Truth(单一数据源) 模式。ViewModel 内部维护一个 UiState 数据类,聚合所有必要字段:
data class DriverUiState(
val order: Order?,
val balance: Double,
val location: Location?,
val isLoading: Boolean,
val error: String?
)
class DriverViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DriverUiState())
val uiState: StateFlow<DriverUiState> = _uiState.asStateFlow()
fun loadDashboard() {
viewModelScope.launch {
// 并发拉取多个数据源
val orderDeferred = async { orderRepo.getCurrentOrder() }
val balanceDeferred = async { accountRepo.getBalance() }
val locationDeferred = async { locationRepo.getCurrentLocation() }
// 合并到统一状态
_uiState.update {
it.copy(
order = orderDeferred.await(),
balance = balanceDeferred.await(),
location = locationDeferred.await(),
isLoading = false
)
}
}
}
}
这样,UI 只需监听一个 StateFlow,每次都是完整、一致的状态快照,再也不用担心“半成品 UI”。
性能优化:不只是“不卡”那么简单
很多人以为 MVVM 只是代码结构好看,其实它对性能提升至关重要。
减少无效刷新
以前用传统方式,每次接口返回就直接 setTextView,哪怕内容没变。现在用 StateFlow + Compose(我们部分页面已迁移到 Jetpack Compose),配合 key 和 remember,只有真正变化的组件才会 recompose。
实测结果(某核心页面,1000 次操作):
| 方案 | 平均帧耗时 (ms) | 掉帧率 | 内存占用 (MB) |
|---|---|---|---|
| 老版 MVC | 28.5 | 12.3% | 86 |
| MVVM + LiveData | 16.2 | 5.7% | 72 |
| MVVM + StateFlow + Compose | 9.8 | 1.2% | 65 |
数据不会骗人。运营同学看到崩溃率下降 40%,直接请我们团队吃了顿火锅(虽然是公司报销的)。
后台任务不拖累主线程
司机经常在行车中使用 App,网络不稳定。以前一断网就卡住整个界面,现在所有网络请求都在 ViewModel 的协程作用域中执行,配合 flowOn(Dispatchers.IO),完全不阻塞 UI。
fun fetchOrders(): Flow<List<Order>> = flow {
emit(repository.fetchFromRemote()) // IO 线程
}.flowOn(Dispatchers.IO)
.catch { e ->
// 错误也走 Flow,不 crash
emit(emptyList())
}
和产品、运营怎么“和平共处”?
说到这,不得不提和产品团队的相爱相杀。他们总想加新功能,比如“司机可以看乘客历史评价”、“实时显示附近空车数”。每次需求评审,我都得问一句:“这个数据要实时刷新吗?频率多少?”
因为每一个实时数据源都意味着额外的网络请求、内存占用和电量消耗。司机手机本来就在高温、震动环境下工作,再搞个后台常驻服务,分分钟变暖手宝。
后来我们定了个规矩:所有新功能必须提供“降级方案”。比如非关键数据默认缓存 5 分钟,用户手动下拉才刷新;定位精度在后台自动降低。这些策略写在产品 PRD 里,运营也认可——毕竟司机体验好了,接单率才高,他们的 KPI 才稳。
为什么我觉得 MVVM 是“产品思维”的体现?
很多人把 MVVM 当纯技术方案,但我越用越觉得它像一种产品设计哲学:
- 关注用户状态:不是“我有什么数据”,而是“用户此刻需要什么状态”
- 可预测性:输入(Action)→ 处理(ViewModel)→ 输出(State),逻辑清晰,bug 少
- 可组合:一个 ViewModel 可以被多个页面复用(比如订单详情和接单页共用订单逻辑)
这和我们在滴滴做后端微服务的理念一模一样:高内聚、低耦合、可观测。甚至可以说,好的前端架构,本质是后端思维的延伸。
给想学 MVVM 的朋友几点建议
- 别死磕理论:先跑通一个 demo,比如用 MVVM 写个 TodoList,比看十篇博客有用
- 从局部开始:不用全量重构,挑一个复杂页面试点(我们是从“司机工作台”开始的)
- 工具链跟上:Hilt 做依赖注入,Coroutines 处理异步,Compose 写 UI,效率飞起
- 监控必不可少:埋点统计 ViewModel 初始化时间、State 更新频率,线上问题早发现
最后:架构没有银弹,但有“少踩坑”的路
写这篇文章的时候,已经是凌晨三点。窗外杭州的雨下个不停,但我的终端里,CI/CD 流水线绿得发亮——今天上线的 MVVM 新版本,零回滚。
回想这半年,从被逼着学 Jetpack,到主动给团队写内部教程;从前端小白,到现在能和 iOS 同学问“你们 SwiftUI 的 StateObject 怎么处理副作用”——成长是真的痛,但也是真的爽。
如果你也在被混乱的移动端代码折磨,不妨试试 MVVM。它不一定是最酷的,但绝对是最“稳”的选择之一。毕竟,在滴滴这样的出行平台,稳定压倒一切——司机不能因为 App 卡顿而错过订单,乘客不能因为界面错乱而取消行程。
对了,最近我在用 Rust 写一个移动端日志收集库,性能比 Java 快不少。等开源了,欢迎来 star(手动狗头)。
附:常用资源推荐
- 官方文档:Android Architecture Components
- 实战教程:Google 的 Sunflower 项目(MVVM + Compose 范例)
- 性能检测:StrictMode、Profiler、LeakCanary
- 团队规范:我们内部写了《MVVM 编码 Checklist》,包含 ViewModel 命名、State 设计、错误处理等 20 条规则(私聊可分享)
写代码不易,且写且珍惜。下次见!

评论 0