关于 symbolize-android-stacktrace
用于解析 Flutter Android 应用的 Google Play Console 崩溃和 ANR 堆栈,结合对应的 Codemagic 构建产物还原符号。适合上传原生与 Flutter 符号文件的发布流程,并可选用 R8 映射还原 Java 帧。
主要功能
- 01还原原生与 Flutter 堆栈帧
- 02从 Codemagic 获取符号产物
- 03解析 R8 和 ProGuard 映射
- 04汇总未解析帧数量
适用场景
- 01排查 Google Play 崩溃堆栈
- 02将 Flutter ANR 帧定位到源码行
- 03使用 mapping.txt 还原 Java 帧
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chunkytofustudios/ flutter-skills --skill symbolize-android-stacktrace安装后在 Claude Code 中输入/symbolize-android-stacktrace确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
scripts/codemagic_fetch_artifacts.pydeobfuscate_r8.pysymbolize_flutter_anr.shtests/classification.batshelp.batshelpers.bashmetachars.batssymbolize.batstest_codemagic_fetch.pytest_deobfuscate_r8.pytests/fixtures/mapping.txtnot-a-trace.txttests/fixtures/flutter/app.android-arm64.symbolstests/stubs/ndk/toolchains/llvm/prebuilt/bats-host/bin/llvm-addr2linellvm-readelf详细介绍
符号化 Android 堆栈跟踪(Flutter + Codemagic)
将混淆后的 Google Play Console 崩溃/ANR 堆栈跟踪转换为已符号化的堆栈跟踪(为每个原生和 Flutter 帧提供文件名 + 行号),以便分析 bug。
何时使用
以下三项必须全部满足——如果其中任一项不满足,此工具就不适用:
- 该应用是面向 Android 的 Flutter 应用。
- 堆栈跟踪从 Google Play Console(崩溃信息中心或 ANR 信息中心)导出——通常是包含
pc 0x…帧的.txt/.log文件,也可能带有# Application:/# Version:标头。 - 发布构建由 Codemagic CI/CD 生成,并且工作流将
android_native_debug_symbols.zip和<AppName>_<N>_artifacts.zip(Flutter 的 split-debug-info.symbols文件)都上传为构建制品。没有这些文件,就无法将混淆后的 PC 地址与符号对应起来。如果存在,也会一并获取mapping.txt(R8/ProGuard);它能让符号化工具将Ka.n.l+8这类 Java 帧还原为io.flutter.embedding.engine.FlutterJNI.onSurfaceDestroyed,这正是可读的 Java 部分与一堆混淆名称之间的区别。
如果不确定构建是否来自 Codemagic,请检查仓库根目录下的 codemagic.yaml,或询问用户。
设置(调用方的机器)
| 要求 | 满足方式 |
|---|---|
| Codemagic API 密钥 | 在 Codemagic → User settings → Integrations → Personal API token 生成。通过以下任一方式提供:(a) CODEMAGIC_API_KEY 环境变量,或 (b) 在仓库根目录创建 .codemagic-api-key 文件,以纯文本形式写入密钥——脚本会从 CWD 向上查找,并在 .git 边界处停止。对于希望将共享密钥存入私有仓库的团队,这种文件方式很方便;使用前请接受相应风险。 |
带有 llvm-addr2line 的 Android NDK | 默认查找路径:~/Library/Android/sdk/ndk/27.3.13750724。使用 ANDROID_NDK_HOME=/path/to/ndk 覆盖。任何较新的 NDK 都包含 llvm-addr2line 和 llvm-readelf。 |
unzip、python3(3.10+)、bash | 标准工具。Python 脚本仅使用标准库。 |
如果环境变量和文件都不存在,请停止并询问用户,不要猜测——每次调用 Codemagic 都会返回 401。
无需进行其他设置。尤其是,不需要 gh——请参阅步骤 3。
附带脚本
scripts/codemagic_fetch_artifacts.py— 三种模式的查找与下载工具。JSON 输出到 stdout,进度信息输出到 stderr。如果构建生成了mapping.txt,也会一并获取。scripts/symbolize_flutter_anr.sh— 不限顺序的堆栈跟踪与符号文件打包工具。会检测各输入文件的类型(堆栈跟踪、原生 zip、Flutter zip、mapping.txt)。scripts/deobfuscate_r8.py— 批量 R8/ProGuard 映射解析器。Bash 符号化脚本会为每个堆栈跟踪调用一次;通常无需直接调用。
三个脚本都提供 --help(或头部文档字符串)。尝试自行调整参数前,请先阅读说明。
工作流
1. 将堆栈跟踪保存到文件
如果用户粘贴的是文本而非附件,请将其写入 /tmp/play-stacktrace.log。Bash 脚本需要真实文件路径(它会对文件执行两次 grep——分别用于 ABI 检测和帧提取)。
2. 从堆栈跟踪中识别应用和版本
堆栈跟踪通常会在 /data/app/.../<applicationId>-…/split_config.<abi>.apk 中包含软件包名称:
/data/app/~~xxxx==/com.example.foo-yyyy==/split_config.arm64_v8a.apk
^^^^^^^^^^^^^^^
版本号优先按以下顺序获取:
- 堆栈跟踪中的
# Version:标头行(Crashlytics 风格的导出内容)。 - 用户分享堆栈跟踪时提供的版本号。
- 如果仍然无法确定,请停止并询问用户。 用户正在从 Google Play Console 下载堆栈跟踪,可以直接在其发送的堆栈跟踪旁边的崩溃详情页中查看
versionName。不要根据最近一次 Codemagic 构建猜测——只有版本与发生崩溃的构建完全一致时,Symbol Build ID 才能解析;版本不匹配会导致每个应用帧都静默显示[UNRESOLVED: no symbol file for BuildId …]。
3. 从 Codemagic 获取符号
分三步发现(跳过不需要的步骤):
# (a) List visible apps. Read the JSON to pick the right `appName`,
# then use that exact string for --app in step (b).
python3 scripts/codemagic_fetch_artifacts.py
# (b) List finished Android builds for the app, to confirm the version exists.
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy"
# (c) Download both symbol zips for that version. Most-recent build wins on ties.
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy" --build 1.2.3
--app 接受 Codemagic 显示名称(与 appName 不区分大小写的精确匹配)——这是规范的选择器,可在任何机器上使用。如果你更想传入 Android applicationId(例如 com.chunkytofustudios.pixel_buddy),请安装并验证 gh CLI(gh auth login);随后脚本会从仓库的 android/app/build.gradle{,.kts} 中解析 applicationId。不要仅为此技能安装 gh——请先列出应用并按名称匹配。
下载内容会缓存在 ~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/ 下——对同一构建重复调用时会立即完成。
步骤 (c) 的标准输出为 JSON——解析 .cacheDir 和 .files[].path,以供下一步使用。例如:
{
"cacheDir": "/Users/you/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>",
"files": [
{ "name": "android_native_debug_symbols.zip", "path": "…/android_native_debug_symbols.zip", "size": 123456789, "cached": true },
{ "name": "PixelBuddy_42_artifacts.zip", "path": "…/PixelBuddy_42_artifacts.zip", "size": 12345678, "cached": false },
{ "name": "mapping.txt", "path": "…/mapping.txt", "size": 6543210, "cached": false }
]
}
mapping.txt 是可选的,且仅在构建启用了 R8/ProGuard 时才会存在。未启用混淆的应用不会有此文件——没关系,在第 4 步跳过 mapping.txt 参数即可。
如果未找到应用或版本,脚本会以非零状态退出,并列出可用项——将这些信息展示给用户,而不是反复尝试。
4. 符号化
bash scripts/symbolize_flutter_anr.sh \
/tmp/play-stacktrace.log \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/android_native_debug_symbols.zip \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/<AppName>_<N>_artifacts.zip \
~/.cache/codemagic-fetch-artifacts/codemagic/<appId>/<buildId>/mapping.txt # optional
参数顺序无关紧要——脚本会检查每个输入,并将其归类为跟踪、原生 zip、Flutter 符号 zip 或 R8 mapping.txt。默认会在输入文件旁生成 <trace>.symbolized.txt 作为输出。使用 -o 覆盖输出路径。使用 --json 在标准输出中获取机器可读的摘要。
提供 mapping.txt 时,用户应用中的 Java 栈帧(路径如 <userPackage>/base.apk、<userPackage>/oat/.../base.odex 或 /memfd:jit-cache)会带有 [JAVA: …] 注释,其中包含反混淆后的 <class>.<method>。类不在映射文件中的栈帧(通常是 R8 未处理的 Flutter SDK 代码,或过期映射中的代码)会标记为 [JAVA: not in mapping.txt]。如果跳过 mapping.txt,这些栈帧会标记为 [JAVA: pass mapping.txt to deobfuscate],以明确说明下一步该怎么做。
5. 阅读符号化输出并调试
输出会将原始行与已解析的栈帧交错排列:
#07 pc 0x00000000004bfb78 /data/app/.../split_config.arm64_v8a.apk (flutter::SurfaceDestroyed(_JNIEnv*, _jobject*, long)+…) (BuildId: d73e2148…)
-> /Users/.../libflutter.so
flutter::SurfaceDestroyed(_JNIEnv*, _jobject*, long)
shell/platform/android/platform_view_android_jni_impl.cc:1234
脚本无法解析的栈帧会标记为 [UNRESOLVED]。末尾的摘要会报告 Frames 和 Unresolved 的数量。
信任模型
此技能会接收来自 Agent 进程外部的文件;处理这些文件的原则是“默认不信任”:
- Codemagic 构建产物来自用户自己经身份验证的 CI 构建(使用其个人 API 令牌解析),而非任意第三方。
codemagic_fetch_artifacts.py会通过 TLS 将请求限制在小型允许列表中(API 使用api.codemagic.io,Codemagic 签名 URL CDN 使用storage.googleapis.com/codemagic-build-artifacts/),在每次重定向时重新验证,并在跨主机跳转时移除x-auth-token标头,确保 Codemagic 令牌不会发送到 CDN。 mapping.txt由deobfuscate_r8.py逐行解析。不会对其内容执行eval/exec,不会导入内容,也不会用其内容构建文件系统路径。文件大小和单行长度上限可防止文件过大导致解析器内存不足。symbolize_flutter_anr.sh处理的跟踪和mapping.txt字段(如二进制文件路径、带括号的方法名)只会用于模式匹配(case),或作为字面字段值传递给awk -v——绝不会将其插入 shell 命令、eval或文件系统路径中。
注意事项
- 构建 ID 是匹配依据。 仅当符号文件中的
BuildId:(来自llvm-readelf --notes)与跟踪日志行中的BuildId:相等时,该文件才会与堆栈帧匹配。版本错误 → 无匹配项 → 每个堆栈帧都标记为[UNRESOLVED]。大多数堆栈帧无法解析时,脚本发出的第一条警告是“构建制品错误(常见)”。 - 版本字符串必须与 Codemagic 构建的
version字段完全一致。 Codemagic 存储的值例如"2.3.1";可以带v前缀,但不能附加构建编号(2.3.1+42)。如果不确定,请运行codemagic_fetch_artifacts.py --app …(不带--build)查看可用版本。 - ABI 通过对跟踪日志进行 grep 检测,查找
arm64、armeabi、x86_64、x86。99% 的情况下是arm64-v8a。如果跟踪日志确实没有 ABI 提示,脚本会默认使用arm64-v8a。 - Flutter 符号压缩包的名称为
<AppName>_<N>_artifacts.zip,其中<N>是 Codemagic 构建序列号(不是版本)。获取脚本会通过正则表达式.+_\d+_artifacts\.zip找到它。如果 Codemagic 工作流重命名了制品,请更新scripts/codemagic_fetch_artifacts.py中的FLUTTER_ARTIFACTS_RE。 - 系统库堆栈帧会按设计保持未解析状态。
libc.so、libart.so、com.google.android.gms、com.google.android.webview、/data/misc/apexdata/com.android.art/.../boot.oat(Android Runtime 的预编译启动映像)、system_server等中的堆栈帧并非来自你的构建——这些 PC 匹配的是 Android 系统二进制文件,Codemagic 不会提供这些文件的制品。脚本会将其标记为[SYSTEM: …]。不要在这些帧上花时间;请关注指向你自己软件包中split_config.<abi>.apk的堆栈帧。 - R8 混淆的 Java 堆栈帧需要
mapping.txt。<userPackage>/base.apk堆栈帧中类似Ka.n.l+8的名称属于你自己的代码,只是经过了缩混。Codemagic 会将mapping.txt与符号压缩包一同上传;将其作为第四个参数传入时,符号化器会自动应用该文件。若不提供该文件,Java 堆栈帧会显示[JAVA: pass mapping.txt to deobfuscate]提示,而不是被悄悄归入[SYSTEM]。<userPackage>/oat/.../base.odex和/memfd:jit-cache中的堆栈帧(AOT 和 JIT 编译的用户 Java 代码)也会以同样方式处理;而boot.oat和framework.jar则会被正确归类为系统帧。 - Flutter 引擎堆栈帧(
libflutter.so)发布时已剥离调试信息。android_native_debug_symbols.zip中的libflutter.so带有 Build ID,但不含 DWARF 调试信息,因此即使 Build ID 匹配,也无法解析出源代码坐标——脚本会将其标记为[UNRESOLVED: BuildId … matched but PC has no debug info (Flutter engine binaries ship stripped)]。对于引擎堆栈帧,Play Console 跟踪日志自身的(flutter::Foo()+offset)文本已经是我们能获得的最佳信息;该技能的主要价值在于通过app.android-<abi>.symbols文件准确解析libapp.so(Dart AOT)堆栈帧。 - 非 Codemagic 构建会在第 3 步失败。 如果发生崩溃的版本没有运行 Codemagic 工作流(例如,它是通过本地
flutter build appbundle上传的),API 就没有任何内容——请停止操作并告知用户。 symbolize_flutter_anr.sh的--json标志会将{"frames", "resolved", "unresolved", "unresolved_nomatch", "unresolved_stripped", "java_deobfuscated", "java_not_deobfuscated", "system", "abi", "output"}输出到 stdout,所有状态日志则输出到 stderr——在将符号化器串接到其他脚本时使用此标志。unresolved是unresolved_nomatch(归档中没有 BuildId → 构建错误)与unresolved_stripped(BuildId 匹配,但二进制文件缺少调试信息 → 通常是libflutter.so)之和。java_deobfuscated+java_not_deobfuscated统计用户应用的 Java 堆栈帧;后者会根据是否传入该文件,在语义上细分为“未提供mapping.txt”和“类不在mapping.txt中”。
快速参考
# End-to-end, when you already know the display name + version:
export CODEMAGIC_API_KEY=... # or drop a `.codemagic-api-key` file at the repo root
python3 scripts/codemagic_fetch_artifacts.py --app "Pixel Buddy" --build 1.2.3 \
| tee /tmp/cm.json
CACHE=$(jq -r .cacheDir /tmp/cm.json)
bash scripts/symbolize_flutter_anr.sh \
/tmp/play-stacktrace.log \
"$CACHE/android_native_debug_symbols.zip" \
"$CACHE"/*_artifacts.zip \
"$CACHE/mapping.txt" # omit if the build didn't run R8/ProGuard
# → /tmp/play-stacktrace.symbolized.txt