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

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

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

本文以 Android 和 Kotlin 项目为主要语境,很多方法也适用于 Flutter、微信小程序、Web 前端和服务端。文中的类名如 ViewModelRepositoryUseCaseWorkManagerCoroutineScope 用作 AndroidX 与 Kotlin 生态中的概念锚点;不同版本、不同团队的封装会有差异,不要把示例当作稳定内部 API。


1. 先识别项目到底"差"在哪里

阅读性差通常不是单点问题,而是几个问题互相缠在一起:

现象 表层感受 背后风险
一个 ActivityFragment 几千行 看不懂入口 UI、状态、网络、缓存、埋点混在一起
工具类无限膨胀 到处都能调 全局副作用多,依赖方向失控
回调层层嵌套 流程难追 取消、异常、生命周期泄漏不可控
复制粘贴相似逻辑 改一处漏三处 行为不一致,问题难定位
没有测试和监控 不敢动 只能靠人工点点点回归
性能问题靠感觉改 优化很热闹 可能没有改善关键体验

改造前先把目标说清楚。不要把"重构一下"当目标,它太软。更好的目标是:

  • 新需求能在目标模块内完成,不需要跨 5 个目录追调用。
  • 核心链路有最小自动化回归。
  • 冷启动、首屏、列表滚动、接口耗时有稳定指标。
  • 每次 PR 能通过格式化、静态检查、单元测试和性能预算。
  • AI 或新人改代码时,修改范围、验证命令和禁止动作足够清楚。

2. 先画流程,再谈类和目录

遗留项目最容易掉进一个坑:上来就按目录解释 activityadapterutilsmanager,但真正的复杂度在一次请求或一次用户操作的流动路径里。

可以先画一条"用户操作到结果渲染"的主链路:
#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 方法
协调者 谁编排多个步骤 UseCaseInteractor、业务服务
生命周期持有者 谁负责开始、停止、取消 ActivityFragmentViewModel
任务调度或追踪者 谁管理并发、重试、后台任务 CoroutineScopeJobWorkManager
执行和缓存边界 真实 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 调用 domaindomain 定义业务模型和接口,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)

命名不是洁癖,它能减少读代码时的猜测成本。遗留项目里尤其要避免 ManagerHelperUtilsCommon 变成万能容器。只要名字无法说明职责,代码很快会再次膨胀。

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

相关推荐
喵喵爱自由1 小时前
Deepseek Harness镜像离线部署
docker·ai·语言模型
菩提小狗2 小时前
每日极客日报 · 2026年09月12日
ai·开源·极客日报·it热点·技术资讯
mmsx2 小时前
Android 手簿 ADB 无线调试全攻略:USB 转 WiFi 一键连接
android·前端
VIP_CQCRE2 小时前
Ace Data Cloud MCP:把整个平台能力接入你的 AI 助手
ai·api·开发工具·mcp·acedatacloud
律宏阔3 小时前
Android WebRTC + H.265 适配记录
android
mmsx3 小时前
MapLibre 实战 08|GPS 点漂了几百米才被发现:GCJ-02 纠偏原理与"转两次"陷阱
android·前端
春夏与冬3 小时前
Android : apktool
android
YF02113 小时前
如何验证App预置为特权App功能正常?
android
长谷深风1113 小时前
Agent执行系统中的身份与版本设计
java·大数据·人工智能·ai·大模型·task·aiagent