遗留项目改造实战指南:从可读性、代码质量到性能和 AI 约束

一个阅读性很差的项目,真正难改的往往不是"代码丑",而是没人敢判断一次修改会影响哪些页面、哪些状态和哪些隐含流程。

一句话结论:遗留项目改造不要从大重写开始,而要从"可观测、可验证、可回退"的小步治理开始,把混乱代码逐渐收敛成有边界、有测试、有性能预算的系统。

本文以 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 守住底线,让人来决定边界和取舍,这才是遗留项目长期变好的方式。

相关推荐
火柴就是我22 分钟前
Flutter 项目本地 Maven 引入 AAR 找不到?一次 Gradle 路径问题排查
android
火柴就是我35 分钟前
Android 打包报错 25.0.3
android·前端
码上有光1 小时前
Linux进程通信——共享内存、消息队列和信号量
android·linux·运维·共享内存·通信
落魄实习生2 小时前
Agent Scope Java 2.x 系列【10】Middleware
java·开发语言·ai
通信瓦工2 小时前
利用浊度和电导率测量确定乙二醇基流体的质量
网络·数据库·ai
林伽一3 小时前
100 万输出词元与窄开放,前沿模型发布范式正在改写|2026年10月02日
人工智能·科技·安全·ai
bigdata-余建新3 小时前
week5
ai
孙启超4 小时前
【FDE开发指南】第 1 课:认识 FDE —— 从一次生产事故说起
人工智能·ai·职场技能
燐妤5 小时前
LangGraph-复习总览
python·ai·面试·agent·学习方法·langgraph
VIP_CQCRE5 小时前
让 Claude 实时联网搜索:Ace Data Cloud Serp MCP 接入指南
ai·claude·搜索·mcp·acedatacloud