如何通过 trace.txt 文件分析 ANR 的主线程阻塞点?

解读

国内面试场景里,面试官抛出“trace.txt”通常不是让你背定义,而是考察“线上 ANR 无日志”时的定位能力。
关键点:

  1. 知道 trace.txt 的生成时机(SIGQUIT 3 秒后由 Signal Catcher 线程写 /data/anr/)
  2. 能在 30 秒内从 2~3 万行文本里锁定“main”线程的“tid=1”栈,并区分“真正死等”与“被 Binder 卡住”
  3. 能把栈片段翻译成业务代码,给出“下一步怎么验证”的落地动作,而不是只说“看主线程”

知识点

  1. trace.txt 结构:
    ----- pid 12345 at 2025-06-07 14:23:45 -----
    Cmd line: com.xxx.app
    ...
    "main" prio=5 tid=1 Blocked
    | group="main" sCount=1 ucsCount=0 flags=1 obj=0x7188 self=0xb400007d4f78f800
    | sysTid=12345 nice=-10 cgrp=default sched=0/0 handle=0x7d5d5cb4f8
    | state=S schedstat=( 285948372 198765423 601 ) utm=21 stm=7 core=3 HZ=100
    | stack=0x7fc35e9000-0x7fc35eb000 stackSize=8192KB
    | held mutexes=
    at sun.misc.Unsafe.park(Native method)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
    at com.xxx.cache.ImageLoader$1.run(ImageLoader.java:188)

    • locked <0x0745a3c6> (a java.lang.Object)
  2. 线程状态缩写:
    S=sleep、R=running、D=IO 等待、B=blocked on monitor、W=waiting、N=native

  3. 常见阻塞模式:

    • Blocked/Waiting:锁或 CountDownLatch
    • Native:JNI 或 IO
    • Binder:对端进程慢(如 AMS、ContentProvider)
    • 死锁:两个线程互相 held 同一地址的 monitor
  4. 量化指标:
    schedstat 三值(运行时间、等待时间、切换次数)可粗略判断“CPU 饥饿”还是“IO 慢”

  5. 国内补充:
    华为/小米/OPPO 定制 ROM 会把 trace 写到 /data/system/dropbox 并加密,需 adb shell setprop persist.sys.anr.debug 1 后复现才能拿到明文

答案

现场回答采用“三步走”话术,既体现思路,又给出可落地的命令:

第一步:确认文件有效性
adb shell ls -l /data/anr | grep trace
拿到最新 trace.txt 后,先看头部时间戳与 ANR 弹窗时间是否匹配,防止拿到旧文件

第二步:30 秒定位主线程

  1. 搜索 "main" prio=5 tid=1 定位首行
  2. 看紧跟的 state= 字段:
    • 如果是 BlockedWaiting,继续往下看栈顶,找 locked <addr>waiting on <addr>
    • 如果是 Native,看是否卡在 epoll_waitreadioctl,大概率是网络或磁盘 IO
    • 如果栈顶是 BinderProxy.transactNative,再搜 Binder thread 段,看对端 tid 与进程名,确认是系统服务慢还是跨应用 ContentProvider 慢
  3. 若发现 held mutexes= 与另一线程互相持有同一地址,即可判定死锁,把两个线程的栈一起截图给开发

第三步:映射到业务代码
把栈顶类名+行号与 mapping.txt 反混淆(R8 全量映射在 app/build/outputs/mapping/release/mapping.txt),得到真实源码位置,本地打断点或加日志复现;若栈顶是第三方 SDK,则升级或异步化

收尾一句:
“如果 trace 里主线程状态是 R 但 schedstat 等待时间极高,说明不是自身阻塞,而是 CPU 被其他进程抢占,需结合 adb shell top -t -o pid,tid,CPU,cmd 看系统负载,再决定是做进程优先级调整还是降频场景规避。”

拓展思考

  1. 线下压测时主动触发 SIGQUIT:
    adb shell kill -3 pidof com.xxx.app`` 可立即生成 trace,无需等 ANR,适合卡顿提前发现

  2. 自动化聚合:
    国内大厂普遍把 trace.txt 上传到日志平台,用 Python 脚本批量解析“main”线程 state 与栈关键字,聚合成“锁-等待-IO-Binder”四类,每周出 ANR Top10 报表,比人工翻文件高效

  3. 与 Perfetto 互补:
    trace.txt 只有瞬时快照,若问题偶现,可在用户授权后录制 10 秒 Perfetto 轨迹,用 atrace -c -a com.xxx.app sched freq idle 抓到 CPU 频率与调度信息,再对照 trace.txt 的 schedstat,判断是否因降频导致 16 ms 掉帧最终累加为 ANR

  4. 折叠屏/多窗口场景:
    大屏切换触发 Activity 重启,若主线程在 onCreate 做 IO,极易 ANR。此时 trace.txt 主线程栈顶往往是 Instrumentation.callActivityOnCreate,但真正的阻塞在 FileInputStream.read,需把 IO 提前到 ViewModel+Coroutine 并在 onCreate 只观察 LiveData

  5. 合规与隐私:
    国内上架应用市场要求“个人信息收集最小化”,上传 trace 前必须裁剪掉栈内可能含有的账号、手机号等字符串,可用 proguard-rules.pro-assumenosideeffects 把敏感类方法置空,防止合规扫描被打回