第四节:映射文件 (mapping.txt) —— 混淆后的“罗塞塔石碑”

说真的,刚开始做混淆的时候,我一度以为 mapping.txt 就是个废物文件。直到线上崩溃日志里全是 a.a.b() 这种天书,才意识到这玩意儿就是救命稻草。嗯,今天我们就把它彻底讲透。

1. 映射文件生成在哪儿?

ProGuard 默认在每次 Release 构建后,把 mapping.txt 丢到这个位置:

app/build/outputs/mapping/release/mapping.txt

但我个人习惯把它改名备份,加上版本号和构建时间。比如:

mapping-v2.1.0-20250412.txt

为什么会这样做?因为线上 Bug 往往是旧版本用户报的,而你本地 mapping 已经被新版本覆盖了。我曾经就因为没备份,对着 b.c.a() 干瞪眼了一下午。

⚠️ 注意:mapping.txt 只在混淆开关开启后生成。如果你用 -dontobfuscate,那就没有映射文件。Debug 构建默认不加混淆,自然也没有。

2. 映射文件长什么样?

拿一个真实项目的 mapping.txt 举例(简化后):

com.example.app.MainActivity -> com.example.app.a:
    android.os.Bundle? onCreate(android.os.Bundle) -> a
    void initViews() -> b
    int mUserId -> c
    5:7:int getUserId() -> d

格式其实很简单:

行类型 说明 示例
类映射 原类名 -> 混淆后类名 com.example.app.MainActivity -> com.example.app.a:
方法映射 返回值 方法签名 -> 混淆后方法名 void initViews() -> b
字段映射 类型 字段名 -> 混淆后字段名 int mUserId -> c
行号映射(可选) 原起始行:原结束行:原行号:混淆后的方法名 5:7:int getUserId() -> d
💡 提示:行号映射不是默认开启的,需要在 proguard-rules.pro 里加 -keepattributes SourceFile,LineNumberTable。我建议所有 Release 构建都加上,否则堆栈里只有 Unknown Source。

3. 怎么用 mapping.txt 还原堆栈?

拿到崩溃堆栈后,你需要用到 ProGuard 自带的 retrace 工具。路径在 Android SDK 里:

$ANDROID_HOME/tools/proguard/bin/retrace.sh  (Mac/Linux)
$ANDROID_HOME\tools\proguard\bin\retrace.bat (Windows)

用法很简单:

retrace.sh -verbose mapping.txt crash_stack.txt

它会把混淆后的堆栈逐行反解成原始类名和方法名。举个我当时遇到的例子:

混淆后的堆栈:

java.lang.NullPointerException
    at com.example.app.a.a(Unknown Source:5)
    at com.example.app.b.b(Unknown Source:12)
    at com.example.app.MainActivity.onCreate(Unknown Source:3)

用 retrace 还原后:

java.lang.NullPointerException
    at com.example.app.MainActivity.initViews(SourceFile:5)
    at com.example.app.Utils.checkNetwork(SourceFile:12)
    at com.example.app.MainActivity.onCreate(SourceFile:3)

你看,a.a 变成了 MainActivity.initViews,b.b 变成了 Utils.checkNetwork——这才是你能定位的代码位置。

📌 关键点:retrace 工具不会修改原始堆栈文件,它会输出到标准输出。记得重定向到新文件保存。

4. 手动还原堆栈(高级技巧)

有时候 retrace 工具不好用(比如行号对不上),你可以手动对照 mapping.txt 来还原。步骤:

  1. 在堆栈里找到混淆后的类名(例如 com.example.app.a),在 mapping.txt 中搜索 -> com.example.app.a:
  2. 看箭头左边就是原类名——com.example.app.MainActivity
  3. 再看方法名 a,在类映射块内找到 () -> a 的行,左边就是原方法名 initViews

不过说实话,手动翻几百行的 mapping.txt 真的很痛苦。我后来写了个小脚本自动做这步,但多数情况下 retrace 够用了。

⚠️ 踩坑提醒:如果 mapping.txt 和 APK 不是同一版本,还原结果会完全错误。我曾经把 v2.0 的 mapping 去解 v2.1 的堆栈,得到一堆乱码。一定要确认版本一致。

5. 映射文件的结构图解

下面这张图展示了从混淆后的崩溃堆栈到可读堆栈的完整流程:

映射文件使用流程 混淆后的崩溃堆栈 a.a.b() 之类 mapping.txt 类/方法/字段映射 retrace 工具 自动反解 可读堆栈 MainActivity.initViews() 手动反解 对照mapping逐行翻译 重要提醒 • 保留每个版本的 mapping.txt,建议与 APK 一起归档 • retrace 本质是字符串替换,别指望它能自动纠错 • 如果堆栈中有行号,确保 mapping 也包含行号映射

6. 我踩过的坑(避坑指南)

  • 混淆后字段丢失:如果 mapping.txt 里找不到某个字段,说明它被 ProGuard 优化掉了。检查 -keepclassmembers 规则。
  • 多个 mapping 文件混用:同一个 bug 在不同版本中混淆名可能不同。我一般用 git tag 来管理 mapping。
  • Retrace 大文件卡死:如果堆栈有几十万行,retrace 可能很慢。可以用 grep 先过滤出关键行再反解。
  • 行号映射不全:有时只有 Unknown Source。检查是否加了 -keepattributes SourceFile,LineNumberTable。
💡 我的习惯:每次发版后,我会把 mapping.txt 压缩打包,文件名带上版本号和构建日期。同时上传到公司的崩溃日志平台,线上堆栈直接自动反解,省去手动步骤。

好了,映射文件这块儿就这么多。记住一句话:mapping.txt 是你的“反混淆字典”,丢了就真的抓瞎了。