告别偶现ANR:用ADB脚本给App装上“行车记录仪”
你遇到过这样的场景吗?用户反馈App偶尔卡死,但自己拿USB调试时一切流畅;logcat里日志满天飞,等想起来抓线程堆栈时,现场早已被覆盖。这种“幽灵卡顿”让无数开发者在深夜挠头。其实,只要写一个简单的Shell脚本,配合ADB命令,就能像行车记录仪一样24小时不间断录制线程状态——卡顿一出现,关键证据自动存档,再也不用赌概率了。
一、核心思路:轮询采样 + 异常触发
行车记录仪平时循环录制,遇到碰撞才保存碰撞前后片段。我们的脚本同理:
-
周期性采样:每隔几秒执行一次
dumpsys thread,获取目标进程所有线程的状态、堆栈、CPU占用。 -
异常检测:如果发现主线程状态变为
BLOCKED或WAITING(典型卡顿信号),立即把当前完整线程快照写入文件,并额外触发kill -3输出Java堆栈。 -
自动清理:只保留异常日志,正常采样记录直接丢弃,避免存储爆炸。
这样一来,无论ANR多难复现,脚本都会在故障发生瞬间“按下保存键”,留下铁证。
二、脚本实现(30行搞定)
#!/bin/bash# 配置项PKG="com.example.yourapp" # 替换成你的包名INTERVAL=2 # 采样间隔(秒)MAX_KEEP=200 # 最多保留多少个异常日志OUT_DIR="./anr_snapshots" # 存储目录mkdir -p $OUT_DIR# 清理超过7天的旧日志find $OUT_DIR -type f -mtime +7 -delete 2>/dev/nullget_pid() {adb shell "ps | grep $PKG | awk '{print \$2}'" 2>/dev/null}PID=$(get_pid)if [ -z "$PID" ]; thenecho "错误:找不到进程 $PKG ,请确保App正在运行"exit 1fiecho "开始监控 PID=$PID,按 Ctrl+C 终止"while true; do# 检查进程是否还活着(可能被系统杀死或用户手动关闭)if ! adb shell "ps | grep -q $PKG"; thenecho "进程已退出,等待5秒后重试..."sleep 5PID=$(get_pid)[ -n "$PID" ] && echo "新进程PID=$PID" || continuefiTS=$(date +"%Y%m%d_%H%M%S")TMP_FILE="$OUT_DIR/temp_$TS.txt"# 获取线程快照adb shell dumpsys thread $PID > $TMP_FILE 2>&1# 检测异常状态(BLOCKED / WAITING)if grep -qE "State.*(BLOCKED|WAITING)" $TMP_FILE; thenFINAL_FILE="$OUT_DIR/anr_$TS.txt"mv $TMP_FILE $FINAL_FILEecho "[!] 捕获到线程阻塞,已保存: $FINAL_FILE"# 额外抓取Java堆栈(对应kill -3)adb shell kill -3 $PID# 注意:kill -3的输出在logcat中,需另外保存,这里只是触发else# 无异常,丢弃临时文件rm -f $TMP_FILEfi# 控制异常日志总数,超过则删除最旧的LOG_COUNT=$(ls -1 $OUT_DIR/anr_*.txt 2>/dev/null | wc -l)if [ $LOG_COUNT -gt $MAX_KEEP ]; thenls -1t $OUT_DIR/anr_*.txt | tail -n +$((MAX_KEEP+1)) | xargs rm -ffisleep $INTERVALdone
代码要点解读:
-
get_pid通过ps | grep实时获取PID,解决了App因崩溃或用户清理而重启的问题。 -
临时文件先写
temp_*.txt,只有匹配到BLOCKED/WAITING才重命名为anr_*.txt,否则删除。这相当于“只保存事故录像”。 -
kill -3会让虚拟机把当前所有线程的Java堆栈输出到logcat(通常带有“thread dump”标签),配合日志一起分析,能直接定位到业务代码行号。
三、避坑指南:常见失败原因及解决
|
|
|
|
|---|---|---|
dumpsys: thread service not found |
thread 服务 |
adb shell dumpsys activity threads 或直接 dumpsys activity | grep -A 100 "PID: $PID" |
Permission denial: requires android.permission.DUMP |
dumpsys 权限 |
adb shell am force-stop $PKG,再快速启动App并运行脚本,此时权限检查会宽松一些;或者使用 adb shell "run-as $PKG dumpsys thread $PID"(仅限debug包) |
adb: more than one device/emulator |
|
adb -s <serial> shell ...,或在脚本开头 export ANDROID_SERIAL=xxx |
|
|
|
adb devices 检查,若掉线则 adb reconnect |
四、性能调优:别让监控成为新卡顿源
-
采样间隔:2秒是黄金平衡点。太短(<1秒)会增加ADB通信开销,导致App多出5%~10%的CPU占用;太长(>5秒)可能漏掉一闪而过的卡顿。实测2秒能捕获绝大多数超过3秒的ANR。
-
存储优化:只保留异常文件,正常轮空删除。一个异常日志大约100~300KB,即使每天抓100次,7天也不过占用几十MB,完全可以接受。
-
减少ADB协议开销:将
adb shell dumpsys thread $PID改为adb exec-out dumpsys thread $PID,可省去多余的终端转义处理,速度提升约30%。 -
不落盘方案(高级):直接把线程快照通过
adb logcat输出到日志缓冲区,然后用另一个脚本实时过滤。这样可以彻底避免写入手机闪存,但实现稍复杂。
五、实战效果:一个真实案例
某社交App用户反馈“发图片时偶尔卡死10秒”。开发组用本脚本在后台跑了三天,终于捕获到一份 anr_20260515_143022.txt。打开看到:
text
"main" prio=5 tid=1 WAITING at java.lang.Object.wait(Native Method) at android.media.MediaCodec.dequeueOutputBuffer(...) ...
结合 kill -3 输出的堆栈,发现是图片压缩时等待硬件编码器超时。问题定位后,改用软编码 + 异步回调,卡顿消失。
总结:再隐蔽的ANR,只要线程状态会变化,就逃不过轮询采样的“天网”。把这30行脚本挂在测试机上一两天,你大概率会收获意想不到的现场证据。与其焦虑复现,不如让脚本替你值班。
夜雨聆风