一个阅读性很差的项目,真正难改的往往不是"代码丑",而是没人敢判断一次修改会影响哪些页面、哪些状态和哪些隐含流程。
一句话结论:遗留项目改造不要从大重写开始,而要从"可观测、可验证、可回退"的小步治理开始,把混乱代码逐渐收敛成有边界、有测试、有性能预算的系统。
本文以 Android 和 Kotlin 项目为主要语境,很多方法也适用于 Flutter、微信小程序、Web 前端和服务端。文中的类名如
ViewModel、Repository、UseCase、WorkManager、CoroutineScope用作 AndroidX 与 Kotlin 生态中的概念锚点;不同版本、不同团队的封装会有差异,不要把示例当作稳定内部 API。
1. 先识别项目到底"差"在哪里
阅读性差通常不是单点问题,而是几个问题互相缠在一起:
| 现象 | 表层感受 | 背后风险 |
|---|---|---|
一个 Activity 或 Fragment 几千行 |
看不懂入口 | UI、状态、网络、缓存、埋点混在一起 |
| 工具类无限膨胀 | 到处都能调 | 全局副作用多,依赖方向失控 |
| 回调层层嵌套 | 流程难追 | 取消、异常、生命周期泄漏不可控 |
| 复制粘贴相似逻辑 | 改一处漏三处 | 行为不一致,问题难定位 |
| 没有测试和监控 | 不敢动 | 只能靠人工点点点回归 |
| 性能问题靠感觉改 | 优化很热闹 | 可能没有改善关键体验 |
改造前先把目标说清楚。不要把"重构一下"当目标,它太软。更好的目标是:
- 新需求能在目标模块内完成,不需要跨 5 个目录追调用。
- 核心链路有最小自动化回归。
- 冷启动、首屏、列表滚动、接口耗时有稳定指标。
- 每次 PR 能通过格式化、静态检查、单元测试和性能预算。
- AI 或新人改代码时,修改范围、验证命令和禁止动作足够清楚。
2. 先画流程,再谈类和目录
遗留项目最容易掉进一个坑:上来就按目录解释 activity、adapter、utils、manager,但真正的复杂度在一次请求或一次用户操作的流动路径里。
可以先画一条"用户操作到结果渲染"的主链路:
#mermaid-svg-kxREmtF5RCDkkWOR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kxREmtF5RCDkkWOR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kxREmtF5RCDkkWOR .error-icon{fill:#552222;}#mermaid-svg-kxREmtF5RCDkkWOR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kxREmtF5RCDkkWOR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kxREmtF5RCDkkWOR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kxREmtF5RCDkkWOR .marker.cross{stroke:#333333;}#mermaid-svg-kxREmtF5RCDkkWOR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kxREmtF5RCDkkWOR p{margin:0;}#mermaid-svg-kxREmtF5RCDkkWOR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kxREmtF5RCDkkWOR .cluster-label text{fill:#333;}#mermaid-svg-kxREmtF5RCDkkWOR .cluster-label span{color:#333;}#mermaid-svg-kxREmtF5RCDkkWOR .cluster-label span p{background-color:transparent;}#mermaid-svg-kxREmtF5RCDkkWOR .label text,#mermaid-svg-kxREmtF5RCDkkWOR span{fill:#333;color:#333;}#mermaid-svg-kxREmtF5RCDkkWOR .node rect,#mermaid-svg-kxREmtF5RCDkkWOR .node circle,#mermaid-svg-kxREmtF5RCDkkWOR .node ellipse,#mermaid-svg-kxREmtF5RCDkkWOR .node polygon,#mermaid-svg-kxREmtF5RCDkkWOR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kxREmtF5RCDkkWOR .rough-node .label text,#mermaid-svg-kxREmtF5RCDkkWOR .node .label text,#mermaid-svg-kxREmtF5RCDkkWOR .image-shape .label,#mermaid-svg-kxREmtF5RCDkkWOR .icon-shape .label{text-anchor:middle;}#mermaid-svg-kxREmtF5RCDkkWOR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kxREmtF5RCDkkWOR .rough-node .label,#mermaid-svg-kxREmtF5RCDkkWOR .node .label,#mermaid-svg-kxREmtF5RCDkkWOR .image-shape .label,#mermaid-svg-kxREmtF5RCDkkWOR .icon-shape .label{text-align:center;}#mermaid-svg-kxREmtF5RCDkkWOR .node.clickable{cursor:pointer;}#mermaid-svg-kxREmtF5RCDkkWOR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kxREmtF5RCDkkWOR .arrowheadPath{fill:#333333;}#mermaid-svg-kxREmtF5RCDkkWOR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kxREmtF5RCDkkWOR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kxREmtF5RCDkkWOR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kxREmtF5RCDkkWOR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kxREmtF5RCDkkWOR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kxREmtF5RCDkkWOR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kxREmtF5RCDkkWOR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kxREmtF5RCDkkWOR .cluster text{fill:#333;}#mermaid-svg-kxREmtF5RCDkkWOR .cluster span{color:#333;}#mermaid-svg-kxREmtF5RCDkkWOR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kxREmtF5RCDkkWOR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kxREmtF5RCDkkWOR rect.text{fill:none;stroke-width:0;}#mermaid-svg-kxREmtF5RCDkkWOR .icon-shape,#mermaid-svg-kxREmtF5RCDkkWOR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kxREmtF5RCDkkWOR .icon-shape p,#mermaid-svg-kxREmtF5RCDkkWOR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kxREmtF5RCDkkWOR .icon-shape .label rect,#mermaid-svg-kxREmtF5RCDkkWOR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kxREmtF5RCDkkWOR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kxREmtF5RCDkkWOR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kxREmtF5RCDkkWOR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户操作
入口:Activity 或 ViewModel
协调者:UseCase
状态持有:ViewModel
任务调度:CoroutineScope
执行边界:Repository
缓存命中
本地结果
网络或数据库
UI 渲染
这张图不是要求所有项目都必须分成这些类,而是强迫我们回答五个问题:
| 源码锚点 | 要回答的问题 | 常见落点 |
|---|---|---|
| 入口 API | 谁触发这次行为 | 点击事件、路由、ViewModel 方法 |
| 协调者 | 谁编排多个步骤 | UseCase、Interactor、业务服务 |
| 生命周期持有者 | 谁负责开始、停止、取消 | Activity、Fragment、ViewModel |
| 任务调度或追踪者 | 谁管理并发、重试、后台任务 | CoroutineScope、Job、WorkManager |
| 执行和缓存边界 | 真实 I/O 在哪里发生 | Repository、DAO、HTTP Client、Cache |
如果一段代码同时回答了这五个问题,它就很可能是改造热点。比如一个 OrderDetailActivity 里既拼参数、又发请求、又写缓存、又维护 loading、又做埋点、还处理重试,这不是"写法不优雅",而是职责边界已经失效。
3. 第一步不是重构,是建立基线
基线的价值是让你知道"现在是什么样",也知道"改完是否更好"。没有基线,重构会变成一种凭手感的冒险。
建议先建立四类基线:
| 基线类型 | 记录什么 | 推荐方式 |
|---|---|---|
| 构建基线 | 当前能否稳定编译 | 固定 JDK、Gradle、构建命令、CI 环境 |
| 行为基线 | 核心链路现在如何表现 | 手工用例、截图、接口 mock、回归清单 |
| 质量基线 | 复杂度、重复率、依赖方向 | ktlint、detekt、lint、依赖分析 |
| 性能基线 | 启动、首屏、滚动、内存、包体积 | Android Studio Profiler、Macrobenchmark、日志埋点 |
不要一开始就追求"完美测试覆盖率"。遗留项目更现实的做法是先覆盖高价值入口:
- 登录、首页、下单、支付、退款、搜索等主链路。
- 最近经常出 bug 的模块。
- 即将改造、但没有人完全讲清楚的模块。
- 涉及缓存、并发、金额、权限、生命周期的逻辑。
这一步的产物不是漂亮架构图,而是一个朴素但有用的事实清单:哪些命令能跑、哪些页面必须稳、哪些指标现在很差、哪些模块先不能碰。
4. 用安全网接住第一次改动
遗留项目的第一轮改造,重点不是把代码变好看,而是让未来每一次变更更可控。
常见安全网可以这样铺:
| 护栏 | 作用 | 初期策略 |
|---|---|---|
| 格式化 | 减少风格争论 | 只对改动文件生效,逐步扩大 |
| 静态检查 | 暴露复杂度和危险 API | 先设 warning,再逐步变 error |
| 单元测试 | 保护纯逻辑 | 从工具类、映射、状态机开始 |
| 集成测试 | 保护关键链路 | 只覆盖核心页面和高风险流程 |
| 架构检查 | 防止依赖倒灌 | 禁止 UI 层直接访问数据源 |
| CI 门禁 | 把约束变硬 | 每次合并必须跑最小验证集 |
如果团队一上来把所有规则都开成失败,老项目通常会直接"红到不能工作"。更好的节奏是:先记录问题,再限制新增问题,最后分批清理存量问题。
例如 detekt 可以先生成 baseline,把历史问题冻结住;之后要求新代码不能增加复杂度、空捕获、长方法和循环依赖。这样团队不会被历史债务淹没,也不会继续往坑里填土。
5. 改造分六步走
一套稳妥的节奏通常是:
#mermaid-svg-dctULbVRMxqWbRcB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dctULbVRMxqWbRcB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dctULbVRMxqWbRcB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dctULbVRMxqWbRcB .error-icon{fill:#552222;}#mermaid-svg-dctULbVRMxqWbRcB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dctULbVRMxqWbRcB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dctULbVRMxqWbRcB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dctULbVRMxqWbRcB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dctULbVRMxqWbRcB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dctULbVRMxqWbRcB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dctULbVRMxqWbRcB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dctULbVRMxqWbRcB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dctULbVRMxqWbRcB .marker.cross{stroke:#333333;}#mermaid-svg-dctULbVRMxqWbRcB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dctULbVRMxqWbRcB p{margin:0;}#mermaid-svg-dctULbVRMxqWbRcB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dctULbVRMxqWbRcB .cluster-label text{fill:#333;}#mermaid-svg-dctULbVRMxqWbRcB .cluster-label span{color:#333;}#mermaid-svg-dctULbVRMxqWbRcB .cluster-label span p{background-color:transparent;}#mermaid-svg-dctULbVRMxqWbRcB .label text,#mermaid-svg-dctULbVRMxqWbRcB span{fill:#333;color:#333;}#mermaid-svg-dctULbVRMxqWbRcB .node rect,#mermaid-svg-dctULbVRMxqWbRcB .node circle,#mermaid-svg-dctULbVRMxqWbRcB .node ellipse,#mermaid-svg-dctULbVRMxqWbRcB .node polygon,#mermaid-svg-dctULbVRMxqWbRcB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dctULbVRMxqWbRcB .rough-node .label text,#mermaid-svg-dctULbVRMxqWbRcB .node .label text,#mermaid-svg-dctULbVRMxqWbRcB .image-shape .label,#mermaid-svg-dctULbVRMxqWbRcB .icon-shape .label{text-anchor:middle;}#mermaid-svg-dctULbVRMxqWbRcB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dctULbVRMxqWbRcB .rough-node .label,#mermaid-svg-dctULbVRMxqWbRcB .node .label,#mermaid-svg-dctULbVRMxqWbRcB .image-shape .label,#mermaid-svg-dctULbVRMxqWbRcB .icon-shape .label{text-align:center;}#mermaid-svg-dctULbVRMxqWbRcB .node.clickable{cursor:pointer;}#mermaid-svg-dctULbVRMxqWbRcB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dctULbVRMxqWbRcB .arrowheadPath{fill:#333333;}#mermaid-svg-dctULbVRMxqWbRcB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dctULbVRMxqWbRcB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dctULbVRMxqWbRcB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dctULbVRMxqWbRcB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dctULbVRMxqWbRcB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dctULbVRMxqWbRcB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dctULbVRMxqWbRcB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dctULbVRMxqWbRcB .cluster text{fill:#333;}#mermaid-svg-dctULbVRMxqWbRcB .cluster span{color:#333;}#mermaid-svg-dctULbVRMxqWbRcB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dctULbVRMxqWbRcB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dctULbVRMxqWbRcB rect.text{fill:none;stroke-width:0;}#mermaid-svg-dctULbVRMxqWbRcB .icon-shape,#mermaid-svg-dctULbVRMxqWbRcB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dctULbVRMxqWbRcB .icon-shape p,#mermaid-svg-dctULbVRMxqWbRcB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dctULbVRMxqWbRcB .icon-shape .label rect,#mermaid-svg-dctULbVRMxqWbRcB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dctULbVRMxqWbRcB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dctULbVRMxqWbRcB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dctULbVRMxqWbRcB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段一:建立基线
阶段二:增加护栏
阶段三:清理热点
阶段四:重画边界
阶段五:按数据优化
阶段六:长期预算
阶段一:建立基线
先保证项目能在固定环境里稳定构建,并把核心流程跑通。记录当前包体积、启动耗时、首屏耗时、关键接口耗时、崩溃率和主要页面的手工回归路径。
这一步不要急着改目录。你还不知道哪些代码真的危险。
阶段二:增加护栏
把格式化、静态检查、最小测试和 CI 跑起来。规则要现实,先防止继续变差。比如:
- 新增代码必须格式化。
- 新增函数圈复杂度不能超过团队阈值。
- UI 层不能直接创建 HTTP Client。
- 新增公共工具类必须说明调用方和边界。
- 每个改造 PR 必须附验证命令和影响范围。
阶段三:清理热点
热点不是最丑的代码,而是"高频变更 + 高风险 + 高收益"的代码。典型例子:
- 首页首屏加载。
- 订单详情状态流转。
- 支付结果轮询。
- 登录态刷新。
- 图片列表渲染。
- 多页面共用的请求封装。
先在热点周围补测试和日志,再抽离纯逻辑。不要边抽象边改行为,否则很难判断问题来自重构还是需求变化。
阶段四:重画边界
边界不是目录名,而是依赖方向。一个更清晰的 Android 模块可以这样组织:
text
feature-order/
├── ui/
│ ├── OrderDetailFragment.kt
│ └── OrderDetailViewModel.kt
├── domain/
│ ├── GetOrderDetailUseCase.kt
│ └── OrderDetailState.kt
└── data/
├── OrderRepository.kt
├── OrderRemoteDataSource.kt
└── OrderLocalDataSource.kt
推荐的依赖方向是:ui 调用 domain,domain 定义业务模型和接口,data 实现数据获取。实际项目可以更轻,不一定每个页面都要三层齐全;但是方向必须清楚。
阶段五:按数据优化性能
性能优化不要和质量重构混在一个 PR 里。重构追求行为不变,性能优化追求指标改善。两者同时做,回归成本会被放大。
优化前先回答:
- 慢在哪里:启动、首屏、接口、数据库、图片、渲染、内存还是包体积?
- 影响谁:全量用户、低端机、弱网、冷启动还是某个页面?
- 指标是多少:P50、P90、P95 各是多少?
- 目标是多少:要降到什么范围才算成功?
阶段六:长期预算
改造最怕"一次性胜利"。代码刚整理完时很好,三个月后又回到原样。
所以要把预算写进流程:
- 主包或安装包体积不能超过阈值。
- 核心页面首屏耗时不能超过阈值。
- 新增模块必须声明依赖方向。
- 新增大依赖需要解释收益、体积和替代方案。
- 修改核心链路必须更新回归用例。
6. 代码质量提升:先让变化变小
很多人理解代码质量,会直接想到命名、注释、设计模式。它们当然重要,但在遗留项目里,代码质量首先体现为"修改一件事时,需要理解和影响的范围有多大"。
6.1 把长方法拆成稳定步骤
先看一个常见问题:UI 入口里塞满流程。
kotlin
// 简化示例:坏味道演示,不代表生产写法
fun onPayClicked() {
showLoading()
val token = readToken()
val params = buildPayParams(token)
api.pay(params, object : Callback<PayResult> {
override fun onSuccess(result: PayResult) {
cache.save(result)
analytics.track("pay_success")
hideLoading()
renderSuccess(result)
}
override fun onError(error: Throwable) {
analytics.track("pay_failed")
hideLoading()
showError(error.message)
}
})
}
第一步不一定是引入复杂架构,而是把"计算"和"副作用"分开:
kotlin
// 简化伪代码:省略依赖注入、错误分类、重试策略和线程切换细节
class PayViewModel(
private val payOrder: PayOrderUseCase
) : ViewModel() {
private val _state = MutableStateFlow<PayUiState>(PayUiState.Idle)
val state: StateFlow<PayUiState> = _state
fun onPayClicked(orderId: String) {
viewModelScope.launch {
_state.value = PayUiState.Loading
_state.value = payOrder(orderId).fold(
onSuccess = { PayUiState.Success(it) },
onFailure = { PayUiState.Error(it.message ?: "支付失败") }
)
}
}
}
这个示例刻意省略了生产项目需要补齐的内容:错误码映射、幂等保护、防重复点击、埋点策略、超时、重试、支付 SDK 回调清理。它只用来说明一个机制:入口只表达用户意图,业务编排进入 UseCase,I/O 进入 Repository,状态由 ViewModel 持有。
6.2 命名要体现业务,不要只体现技术
差的命名:
kotlin
fun handleData(type: Int, flag: Boolean)
更好的命名:
kotlin
fun refreshOrderAfterPayment(orderId: String, forceRemote: Boolean)
命名不是洁癖,它能减少读代码时的猜测成本。遗留项目里尤其要避免 Manager、Helper、Utils、Common 变成万能容器。只要名字无法说明职责,代码很快会再次膨胀。
6.3 让依赖方向可被检查
文字约定很容易失效。能检查的规则才会长期有效。比如:
feature-*之间不能互相直接依赖。ui不能依赖具体网络实现。domain不引用 Android UI 类。data不反向调用ViewModel。- 公共模块不能依赖业务模块。
这类规则可以通过 Gradle 模块、静态分析、自定义 lint、架构测试或 CI 脚本落地。不要只写在 Wiki 里。
7. 生命周期和并发:遗留 Android 项目最容易出事的地方
可读性差的项目,经常伴随生命周期问题。比如页面销毁后回调还在更新 UI,或者多次进入页面触发重复请求。
Android 里可以用"谁持有任务,谁负责取消"来判断:
| 场景 | 推荐持有者 | 原因 |
|---|---|---|
| 与页面 UI 状态绑定的请求 | ViewModel |
配置变化时保留状态,页面销毁时可取消 |
| 与可见生命周期绑定的收集 | LifecycleOwner |
页面不可见时停止收集,避免无效渲染 |
| 必须完成的后台任务 | WorkManager |
跨进程、重启、约束条件更明确 |
| 单次 SDK 回调桥接 | 当前调用作用域 | 需要及时取消并清理 callback |
用 Kotlin 协程桥接回调时,关键不是"把回调包成 suspend",而是处理取消和清理:
kotlin
// 简化伪代码:省略权限检查、线程约束和错误码细分
suspend fun LocationClient.awaitOnce(): Location =
suspendCancellableCoroutine { continuation ->
val callback = object : LocationCallback {
override fun onLocation(location: Location) {
if (continuation.isActive) {
continuation.resume(location)
}
removeCallback(this)
}
override fun onError(error: Throwable) {
if (continuation.isActive) {
continuation.resumeWithException(error)
}
removeCallback(this)
}
}
addCallback(callback)
continuation.invokeOnCancellation {
removeCallback(callback)
}
}
这个模式背后的源码级问题是:入口由谁调用,回调对象由谁保存,取消何时发生,结果交给谁。只要这四个问题没回答清楚,代码看起来再"协程化"也可能泄漏。
8. 性能提升:先定位瓶颈,再动刀
性能优化的第一原则是:不要优化你没有测量过的问题。
客户端常见性能问题可以分层处理:
| 层面 | 典型问题 | 优化方向 |
|---|---|---|
| 启动 | Application 初始化太重 |
延迟初始化、按需加载、启动任务分级 |
| 包体积 | 图片、so、重复依赖过大 | 资源压缩、依赖治理、动态特性或分模块 |
| 网络 | 串行请求、重复请求、弱网差 | 并发编排、缓存、合并接口、超时重试 |
| 数据库 | 主线程查询、无索引、大事务 | 后台执行、索引、分页、批量写入 |
| 渲染 | 列表节点复杂、频繁刷新 | Diff、局部刷新、稳定 item key |
| 内存 | 大图、缓存无上限、引用泄漏 | 图片采样、缓存预算、泄漏检测 |
| 并发 | 任务无取消、重复执行 | 结构化并发、去重、状态机 |
8.1 启动优化:别把所有初始化塞进 Application
遗留项目常见写法是:
kotlin
class App : Application() {
override fun onCreate() {
super.onCreate()
initAnalytics()
initPush()
initMap()
initImageLoader()
initPayment()
initDebugTools()
}
}
优化不是简单把它们全丢到后台线程,而是按用户路径分级:
| 级别 | 例子 | 策略 |
|---|---|---|
| 必须同步 | 崩溃捕获、基础配置 | 保持极少,测量耗时 |
| 首屏前需要 | 登录态、首页必要配置 | 并发加载,失败可降级 |
| 进入业务才需要 | 地图、支付、IM、分享 | 按页面或功能懒加载 |
| 调试才需要 | Debug 面板、日志上传 | 只在开发包启用 |
8.2 网络优化:减少等待链路
如果首页要依次请求 A、B、C,但 B 和 C 并不依赖 A,就应该并发:
kotlin
// 简化伪代码:省略错误降级、取消策略和缓存策略
suspend fun loadHome(): HomeData = coroutineScope {
val profile = async { repository.loadProfile() }
val banners = async { repository.loadBanners() }
val feeds = async { repository.loadFeeds() }
HomeData(
profile = profile.await(),
banners = banners.await(),
feeds = feeds.await()
)
}
并发不是越多越好。它需要配合超时、失败隔离、缓存命中和后端承载能力。否则只是把"慢"变成"同时慢"。
8.3 列表优化:减少无意义刷新
列表卡顿常见原因不是 RecyclerView 本身,而是:
notifyDataSetChanged()过度使用。- item 布局层级太深。
- 图片尺寸远大于展示尺寸。
- 滚动中做同步计算。
- ViewHolder 绑定时触发网络或数据库操作。
优先使用 Diff、分页、局部刷新和图片尺寸约束。每个 item 的绑定逻辑应该尽量是"把已经准备好的状态渲染出来",不要在绑定时继续做复杂业务。
9. AI 可以帮忙,但必须被约束
AI 很适合处理遗留项目里的重复阅读、局部重构、测试补齐和规则巡检。但它也容易在边界不清时扩大修改范围,或者把"看起来合理"的代码写进关键链路。
因此要把 AI 当成受约束的协作者,而不是自由发挥的架构师。
#mermaid-svg-1oFlZCbhXbosfbsd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1oFlZCbhXbosfbsd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1oFlZCbhXbosfbsd .error-icon{fill:#552222;}#mermaid-svg-1oFlZCbhXbosfbsd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1oFlZCbhXbosfbsd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1oFlZCbhXbosfbsd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1oFlZCbhXbosfbsd .marker.cross{stroke:#333333;}#mermaid-svg-1oFlZCbhXbosfbsd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1oFlZCbhXbosfbsd p{margin:0;}#mermaid-svg-1oFlZCbhXbosfbsd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1oFlZCbhXbosfbsd .cluster-label text{fill:#333;}#mermaid-svg-1oFlZCbhXbosfbsd .cluster-label span{color:#333;}#mermaid-svg-1oFlZCbhXbosfbsd .cluster-label span p{background-color:transparent;}#mermaid-svg-1oFlZCbhXbosfbsd .label text,#mermaid-svg-1oFlZCbhXbosfbsd span{fill:#333;color:#333;}#mermaid-svg-1oFlZCbhXbosfbsd .node rect,#mermaid-svg-1oFlZCbhXbosfbsd .node circle,#mermaid-svg-1oFlZCbhXbosfbsd .node ellipse,#mermaid-svg-1oFlZCbhXbosfbsd .node polygon,#mermaid-svg-1oFlZCbhXbosfbsd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1oFlZCbhXbosfbsd .rough-node .label text,#mermaid-svg-1oFlZCbhXbosfbsd .node .label text,#mermaid-svg-1oFlZCbhXbosfbsd .image-shape .label,#mermaid-svg-1oFlZCbhXbosfbsd .icon-shape .label{text-anchor:middle;}#mermaid-svg-1oFlZCbhXbosfbsd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1oFlZCbhXbosfbsd .rough-node .label,#mermaid-svg-1oFlZCbhXbosfbsd .node .label,#mermaid-svg-1oFlZCbhXbosfbsd .image-shape .label,#mermaid-svg-1oFlZCbhXbosfbsd .icon-shape .label{text-align:center;}#mermaid-svg-1oFlZCbhXbosfbsd .node.clickable{cursor:pointer;}#mermaid-svg-1oFlZCbhXbosfbsd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1oFlZCbhXbosfbsd .arrowheadPath{fill:#333333;}#mermaid-svg-1oFlZCbhXbosfbsd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1oFlZCbhXbosfbsd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1oFlZCbhXbosfbsd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1oFlZCbhXbosfbsd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1oFlZCbhXbosfbsd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1oFlZCbhXbosfbsd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1oFlZCbhXbosfbsd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1oFlZCbhXbosfbsd .cluster text{fill:#333;}#mermaid-svg-1oFlZCbhXbosfbsd .cluster span{color:#333;}#mermaid-svg-1oFlZCbhXbosfbsd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1oFlZCbhXbosfbsd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1oFlZCbhXbosfbsd rect.text{fill:none;stroke-width:0;}#mermaid-svg-1oFlZCbhXbosfbsd .icon-shape,#mermaid-svg-1oFlZCbhXbosfbsd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1oFlZCbhXbosfbsd .icon-shape p,#mermaid-svg-1oFlZCbhXbosfbsd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1oFlZCbhXbosfbsd .icon-shape .label rect,#mermaid-svg-1oFlZCbhXbosfbsd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1oFlZCbhXbosfbsd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1oFlZCbhXbosfbsd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1oFlZCbhXbosfbsd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
任务说明
允许修改范围
仓库规则文件
自动化检查
门禁通过
人工评审
退回修正
这里的核心原则是:提示词是软约束,测试、静态检查和 CI 才是硬门禁。
9.1 仓库规则文件怎么写
可以在仓库根目录放一个 AI 协作说明文件。不同工具支持的文件名和继承规则不同,常见做法包括 AGENTS.md、CONTRIBUTING.md、目录级规则文件或工具自己的配置文件。不要依赖某个名字本身产生魔法,真正可靠的是自动化检查。
一个可直接改造的模板:
markdown
# AI Collaboration Rules
## Project Goal
This repository prioritizes readable, testable, incremental changes. Do not rewrite a module unless the task explicitly asks for it.
## Allowed Scope
- Keep edits inside the requested feature or module.
- Preserve public behavior unless the task says otherwise.
- Do not change build configuration, dependency versions, release signing, analytics, payment, or auth flows without explicit approval.
## Architecture Rules
- UI code may call ViewModel or presentation contracts only.
- Domain code must not depend on Android UI classes.
- Data implementations must hide network, database, and cache details behind repository interfaces.
- Shared modules must not depend on feature modules.
## Quality Rules
- Prefer small functions with business names.
- Add or update tests for changed pure logic.
- Separate behavior changes from mechanical refactoring.
- Do not introduce global mutable state or catch broad exceptions silently.
## Performance Rules
- Do not add synchronous I/O on the main thread.
- Do not add large dependencies without explaining size and alternatives.
- Do not move heavy work into Application startup.
- For list rendering, avoid full refresh when a scoped update is possible.
## Required Verification
- Run the smallest relevant build or test command.
- State what was tested and what was not tested.
- Include risk notes for lifecycle, cache, concurrency, and compatibility.
这份文件只是一层协作契约。要想真的约束 AI,还需要把规则落到命令里:
- 格式化失败不能合并。
- 静态检查失败不能合并。
- 核心测试失败不能合并。
- 包体积超过预算不能合并。
- 关键页面性能回退超过阈值不能合并。
9.2 给 AI 的任务提示词模板
遗留项目里,不要只说"帮我优化一下代码"。可以这样写:
text
请只改造 feature-order 模块中的订单详情加载逻辑。
目标:
- 保持现有 UI 行为不变。
- 把网络请求、缓存读取和状态映射从 Fragment 中移出。
- 使用 ViewModel 持有页面状态。
- 为订单状态映射补单元测试。
限制:
- 不修改支付、登录、埋点和路由逻辑。
- 不升级依赖版本。
- 不做包名迁移。
- 不改变接口字段含义。
验证:
- 运行订单模块单元测试。
- 说明是否影响启动、缓存、并发和生命周期。
- 如果发现需要更大范围改动,先停下来说明原因。
这种提示词有四个好处:目标明确、范围明确、禁止动作明确、验证明确。AI 的输出会更像一次可评审的工程变更,而不是一次不可控的代码翻新。
10. 常见失败姿势
| 失败姿势 | 为什么危险 | 更好的做法 |
|---|---|---|
| 一次性重写整个模块 | 行为差异太多,回归不可控 | 先补基线和测试,再按链路迁移 |
| 只做目录搬家 | 结构看似清楚,依赖仍混乱 | 先定义依赖方向和检查规则 |
| 重构和需求混在一起 | 出问题难定位 | 行为不变的重构单独提交 |
| 性能优化凭感觉 | 可能优化错地方 | 先采样和打点,再定目标 |
| AI 自由发挥 | 修改范围和风险不可控 | 任务契约 + 自动化门禁 + 人工评审 |
| 只清理存量问题 | 新问题继续产生 | 先限制新增,再分批偿还旧债 |
11. 最小 Definition of Done
一次遗留项目改造 PR,至少应该满足:
- 说明改造的入口、影响范围和不改的范围。
- 行为变化和结构变化分开提交,或者在 PR 描述里清楚标记。
- 改动文件通过格式化和静态检查。
- 改过的纯逻辑有单元测试或明确的人工验证说明。
- 涉及生命周期、并发、缓存、权限、金额、支付等风险点时,有专门说明。
- 涉及性能优化时,给出优化前后指标,至少说明采样方法。
- AI 参与生成的代码经过人工阅读,不把提示词当质量证明。
12. 30 / 60 / 90 天改造路线
| 时间 | 目标 | 产物 |
|---|---|---|
| 前 30 天 | 看清现状,停止恶化 | 构建基线、核心回归清单、静态检查、AI 规则文件 |
| 前 60 天 | 改造高频热点 | 2 到 3 条核心链路的测试、状态收敛、职责拆分 |
| 前 90 天 | 把质量变成机制 | 模块边界、性能预算、CI 门禁、代码评审 checklist |
这个节奏的重点是持续交付。项目还在跑业务,用户还在使用,改造必须能伴随日常需求前进。
13. 最后的取舍
遗留项目改造不是把旧代码批判一遍,也不是把新架构套进去。更成熟的做法是先尊重它为什么长成现在这样,然后用数据、测试、边界和自动化规则慢慢把它变得可维护。
真正有价值的改造,最后会带来三个变化:
- 开发者敢改,因为知道怎么验证。
- 新人能读,因为入口、状态和边界清楚。
- 系统能变快,因为性能问题有指标、有预算、有责任归属。
AI 能加速这个过程,但不能替团队承担工程判断。让 AI 做可验证的小步工作,让 CI 守住底线,让人来决定边界和取舍,这才是遗留项目长期变好的方式。