大型项目总会积累一些“永远为 true”或“永远为 false”的布尔标志——BuildConfig.IS_PRODUCTION、FeatureFlags.isLegacyMode()、各种运行时开关。标志的值固定下来之后,被它守护的代码就变成了死代码。
几行 if(false) 不影响编译,但几百处散布在十几个模块里就不一样了:IDE 索引变慢、代码搜索噪音增大、新人被废弃逻辑误导、APK 体积白白膨胀。
手动改几百处不现实,IDE 的 Inspections 只能处理最简单的 if(true) 场景。写了一个 Python 工具来做这件事——dead-code-pruner。
它做什么
给它一份配置文件,告诉它哪些表达式恒为 true 或 false:
replacements: - pattern: "BuildConfig.IS_PRODUCTION" value: true - pattern: "FeatureFlags.isLegacyMode" value: false工具会执行一个 6 步流水线。每一步的产出可能给下一步创造新的简化机会,所以整条流水线循环执行直到收敛——某一轮没有任何文件变更时停止。
为什么需要迭代收敛
考虑这段代码:
if (BuildConfig.IS_PRODUCTION) { showProduction();} else { if (isLegacy()) { handleLegacy(); }}Phase 1 的 step1 把 BuildConfig.IS_PRODUCTION 替换为 true,step4 展开 if(true) 块、删除 else 块。一轮结束。
但 handleLegacy() 被删除后,isLegacy() 方法可能变成无调用者。Phase 3 的 step6 扫描发现它是死方法,删除定义。而 isLegacy() 内部可能还调用了其他方法……级联效应。
只跑一遍流水线,这些级联产生的死代码会被遗漏。
6 步详解
step1:常量替换
读取配置文件,把匹配的表达式替换为布尔字面量。所有替换操作共享一个 tokenizer,把源码拆成“代码段”和“非代码段”(注释、字符串字面量),只对代码段做替换。不做这层保护的话,// BuildConfig.IS_PRODUCTION 这种注释也会被改掉。
step2:简单布尔简化
处理一元和二元布尔运算。工具内部注册了完整的模式表:
| 模式 | 结果 |
|---|---|
!true | false |
!false | true |
true == true | true |
false == false | true |
true == false | false |
false == true | false |
true != false | true |
false != true | true |
true != true | false |
false != false | false |
另外还处理冗余括号:(true) → true、(false) → false,但只在安全的上下文中——不能剥离 if(true) 中的括号(语法要求),也不能剥离函数调用 foo(true) 中的括号。工具会检查括号前一个 token 来判断安全性:if/while/for → 不剥离,return/throw/运算符/行首 → 安全剥离。
step3:复合布尔简化
处理短路运算和三元表达式,正确处理运算符优先级。覆盖的模式:
| 模式 | 结果 | 说明 |
|---|---|---|
false && EXPR | false | 短路,EXPR 被整体删除 |
EXPR && false | false | 反向短路 |
true && EXPR | EXPR | 恒等消除 |
EXPR && true | EXPR | 同上,连同前导注释行一起移除 |
true || EXPR | true | 短路 |
EXPR || true | true | 反向短路 |
false || EXPR | EXPR | 恒等消除 |
EXPR || false | EXPR | 同上 |
true ? X : Y | X | 三元消除 |
false ? X : Y | Y | 三元消除 |
false + "" | "false" | 字符串拼接折叠 |
true + "" | "true" | 同上 |
这里有个容易出错的优先级问题:true || A && B 应该简化为 true,而不是 true || A 然后留下 B。因为 && 优先级高于 ||,A && B 是 || 的一个操作数,整体应该被消除。工具在处理 || 时会把 && 表达式当作一个整体跳过。
嵌套三元表达式 condition ? (innerCond ? A : B) : C 需要追踪括号深度和三元嵌套深度来找到正确的 : 位置,不能简单地找第一个冒号。
每个模式匹配还要避开 == / != 上下文——x == true 中的 true 是比较操作数,不能当作短路的左侧。
step4:if 块消除
覆盖的场景:
| 输入 | 输出 |
|---|---|
if (true) { A } | A(展开块内容) |
if (true) { A } else { B } | A |
if (true) { A } else if (X) { B } else { C } | A(删除整个 else-if 链) |
if (false) { A } | 删除 |
if (false) { A } else { B } | B |
if (false) { A } else if (X) { B } else { C } | if (X) { B } else { C } |
if (false) return X; | 删除(单行无花括号) |
删除死代码时不能跨 switch/case 标签。Java 的 case 之间没有显式闭合,删多了会把相邻 case 的代码吃掉。
step5:恒返回方法内联
扫描所有方法定义,找到只有 return true; 或 return false; 的方法,在调用处替换为常量值:
// 内联前private boolean isLocal() { return false; }if (isLocal()) { ... }
// 内联后if (false) { ... } // 下一轮被 step4 清理有个微妙的 bug:shouldBlock(); 作为独立语句时,内联后变成 false;——不是合法的 Java 语句。解决方案是内联后扫描文件,移除所有独立的 true;/false; 行。
step6:死方法清理
找到空 void 方法和恒返回布尔方法,移除定义和调用处。
这一步需要构建内存引用索引。项目可能有上万个源文件,对每个方法做全局 grep 查引用耗时不可接受。做法是一次遍历所有文件建立方法名→引用位置的倒排索引,后续查询走内存,复杂度从 O(N×M) 降到 O(N+M)。实测从“跑不完”变为 40 秒完成。
安全边界:
- 参数数量精确匹配——
render()和render(dialog)是不同的方法 - 非 private 方法只内联调用,不删定义——protected/package-private 方法可能被子类
@Override,静态分析无法确定安全性 - 跳过接口方法、抽象方法、
@Override方法——构建类继承层次,排除框架约束的方法 - 链式调用保护——
method().subscribe()这种链不会因为删除 void 调用而断裂
使用
git clone https://github.com/OldJii/dead-code-pruner.git
# 在项目根目录放一份配置cp dead-code-pruner/pruner.example.yaml pruner.yaml# 编辑 pruner.yaml
# 预览python3 dead-code-pruner/prune.py . --dry-run
# 执行python3 dead-code-pruner/prune.py .
# 编译验证./gradlew compileDebugJavaWithJavac可以只跑某一阶段(--phase 1/2/3),也可以单独执行某个 step(python3 step3_compound_boolean.py .)。零外部依赖,Python 3.8+。YAML 配置需要 PyYAML,不想装的话用 JSON 格式。
实战
用这个工具对一个中大型 Android 项目做了一次完整清理。项目中有几百处运行时布尔判断。配置文件写一行 pattern: "BuildConfig.IS_PRODUCTION", value: true,跑完三阶段流水线,布尔简化 → 方法内联 → 死方法清理全部自动完成。
之后配合 Android 的 shrinkResources 和 AS 的 Remove Unused Resources 做资源清理。需要注意 getIdentifier() 动态资源引用——shrinkResources 检测不到这种引用关系,匹配模式要加入 keep.xml 白名单。
最终 APK 从 125 MB 降到 113 MB,减少约 12 MB(接近 10%)。DEX 减了 6 MB(源码级清理 + R8 优化的正向循环),res 减了 5 MB。
持续维护
清理不是一次性的。每次 merge 主分支都可能引入新的条件判断代码。工具设计成可重复执行——merge 后跑一次 python3 prune.py . 就能自动清理增量,不需要人再理解业务逻辑。
局限
- 针对 Java/Kotlin 源文件,基于模式匹配而非语义分析
- 无法处理中间变量赋值(
boolean x = BuildConfig.FOO; if (x) ...) - step6 死方法检测偏保守:只移除真正空体或单常量返回的方法
保守是有意为之——空壳方法留着不会出 bug,删错了会。
设计选择
为什么不用 AI / IDE? 评估过 Cursor 逐文件改和 AS Inspections。AI 单次 context window 覆盖不了上万文件的完整引用链,每次结果不确定。IDE 只能处理最简单的 if(true),嵌套三元、短路运算、级联死代码一概不管。脚本行为是确定性的——同一输入同一输出,跑错了修脚本重跑。
为什么分 6 个独立文件? 每个 step 可以独立运行和调试。排查问题时可以只跑 step3 看复合布尔简化的结果,不用跑整条流水线。
为什么用 subprocess 而不是 import? 隔离性。每个 step 有自己的文件扫描逻辑和状态,进程隔离避免共享状态的 bug。实测整条流水线 40 秒级别,进程启动开销可忽略。