mcpField
S
symbolize-android-stacktrace

symbolize-android-stacktrace

github.com/chunkytofustudios开发544 次安装26 周前更新
代码仓库

关于 symbolize-android-stacktrace

用于解析 Flutter Android 应用的 Google Play Console 崩溃和 ANR 堆栈,结合对应的 Codemagic 构建产物还原符号。适合上传原生与 Flutter 符号文件的发布流程,并可选用 R8 映射还原 Java 帧。

主要功能

  1. 01还原原生与 Flutter 堆栈帧
  2. 02从 Codemagic 获取符号产物
  3. 03解析 R8 和 ProGuard 映射
  4. 04汇总未解析帧数量

适用场景

  1. 01排查 Google Play 崩溃堆栈
  2. 02将 Flutter ANR 帧定位到源码行
  3. 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.py
deobfuscate_r8.py
symbolize_flutter_anr.sh
tests/
classification.bats
help.bats
helpers.bash
metachars.bats
symbolize.bats
test_codemagic_fetch.py
test_deobfuscate_r8.py
tests/fixtures/
mapping.txt
not-a-trace.txt
tests/fixtures/flutter/
app.android-arm64.symbols
tests/stubs/ndk/toolchains/llvm/prebuilt/bats-host/bin/
llvm-addr2line
llvm-readelf

详细介绍

符号化 Android 堆栈跟踪(Flutter + Codemagic)

将混淆后的 Google Play Console 崩溃/ANR 堆栈跟踪转换为已符号化的堆栈跟踪(为每个原生和 Flutter 帧提供文件名 + 行号),以便分析 bug。

何时使用

以下三项必须全部满足——如果其中任一项不满足,此工具就不适用:

  1. 该应用是面向 Android 的 Flutter 应用。
  2. 堆栈跟踪从 Google Play Console(崩溃信息中心或 ANR 信息中心)导出——通常是包含 pc 0x… 帧的 .txt/.log 文件,也可能带有 # Application: / # Version: 标头。
  3. 发布构建由 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
                   ^^^^^^^^^^^^^^^

版本号优先按以下顺序获取:

  1. 堆栈跟踪中的 # Version: 标头行(Crashlytics 风格的导出内容)。
  2. 用户分享堆栈跟踪时提供的版本号。
  3. 如果仍然无法确定,请停止并询问用户。 用户正在从 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

评论

支持 Markdown