移动应用架构怎么搞才不翻车?我在滴滴用 MVVM 重构司机端的血泪史

TPS计算员
2026-01-13 09:28
阅读 428

凌晨两点,杭州西溪园区的灯还亮着几盏。我刚 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),配合 keyremember,只有真正变化的组件才会 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 的朋友几点建议

  1. 别死磕理论:先跑通一个 demo,比如用 MVVM 写个 TodoList,比看十篇博客有用
  2. 从局部开始:不用全量重构,挑一个复杂页面试点(我们是从“司机工作台”开始的)
  3. 工具链跟上:Hilt 做依赖注入,Coroutines 处理异步,Compose 写 UI,效率飞起
  4. 监控必不可少:埋点统计 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

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