🎯 前言
ANR是Android开发中非常经典且棘手的性能问题。很多同学习惯直接看Logcat日志排查,但普通logcat只能看到ANR发生通知,无法获取主线程阻塞堆栈;想要精准定位阻塞代码,必须拿到系统保存的ANR Trace文件。
本文结合真实项目案例,完整讲解「日志抓取 → 堆栈解析 → 根因定位 → 代码修复」全流程,同时梳理不同设备ANR日志抓取方案的坑点。
🔍 一、现象描述
设备打开首页Activity时偶现应用无响应ANR弹窗;
从Logcat可以观察到关键系统日志:
log
Window{xxx} is not responding. Waited 5003ms for FocusEvent(hasFocus=true)
系统提示:Activity等待焦点事件超时5秒,触发ANR。
⚠️ 重点提醒 :单纯Logcat只能证明发生ANR,看不到主线程卡死在哪一行,无法定位根因!
📁 二、如何正确抓取ANR完整堆栈
ANR触发时,ActivityManager会自动收集所有线程堆栈,写入 /data/anr/ 目录下的trace文件。
🔧 方式1:adb pull /data/anr/(行业开发板/userdebug固件推荐)
bash
adb pull /data/anr/ ./anr_logs
⚠️ 局限性(非常重要)
这条命令不适用于所有Android设备:
- RK开发板、行业平板、userdebug固件:adb shell具备权限,可以直接读取,成功率高;
- 零售手机正式User固件:受SELinux权限限制,会报
Permission denied,无法访问/data/anr。
📱 方式2:通用兼容方案(手机正式固件优先使用)
无法访问/data/anr时,使用Dropbox获取历史ANR记录:
bash
adb shell dumpsys dropbox --print data_anr
🐛 补充调试技巧(复现场景快速抓栈)
未触发ANR但怀疑主线程卡死时,主动向进程发送信号导出主线程堆栈,无需等待ANR:
bash
adb shell kill -3 应用PID
堆栈会直接输出在Logcat中。
🔬 三、ANR Trace堆栈解析 & 根因定位
拿到anr trace文件,搜索关键词 ----- MAIN THREAD -----,主线程堆栈片段:
java
"main" prio=5 tid=1 Sleeping
at java.lang.Thread.sleep(Native method)
at android.os.SystemClock.sleep(SystemClock.java:131)
at xxx.sdk.impl.SdkApi.readHardwareState(SdkApi.java:1099)
at xxx.sdk.impl.SdkApi.queryState(SdkApi.java:1947)
at com.xxx.ui.MainActivity.initSdk(MainActivity.kt:865)
at com.xxx.ui.MainActivity.onCreate()
📊 堆栈调用链路梳理
MainActivity.onCreate()
→ initSdk()
→ 硬件状态同步查询接口
→ readHardwareState()
→ SystemClock.sleep()
🎯 核心结论
- 阻塞位置 :
Activity#onCreate主线程同步调用第三方SDK查询硬件状态; - 阻塞元凶 :SDK内部同步方法执行
SystemClock.sleep(); - ANR触发原理
主线程负责消息循环、界面绘制、处理系统焦点/输入事件。主线程一旦执行sleep阻塞:
- sleep时长随机;
- 单次阻塞≥5秒,InputDispatcher无法收到主线程响应 → 判定应用无响应,触发ANR;
💡
SystemClock.sleep()和Thread.sleep()特性一致:阻塞当前执行线程,严禁在主线程调用。
❌ 容易踩坑的误区
很多人以为sleep很短就不会ANR,但场景是偶现 :
硬件读取耗时存在波动,sleep时长不固定,小于5秒正常,超过5秒直接ANR,表现为间歇性复现,极难稳定复现。
🛠️ 四、分层解决方案
🎨 方案1:应用层改造(优先,最小改动,无需修改SDK源码)
页面onCreate不要在主线程执行耗时初始化,把SDK初始化、硬件状态查询迁移至后台子线程。
Kotlin协程示例:
kotlin
// 错误写法(主线程直接执行)
// initSdk()
// 正确写法,切后台线程执行
lifecycleScope.launch(Dispatchers.IO) {
initSdk()
// 如果初始化完成后需要更新UI,切回主线程
withContext(Dispatchers.Main) {
refreshView()
}
}
📝 注意:子线程中禁止直接操作View;UI刷新必须调度到主线程。
🔧 方案2:SDK底层根治(如果拥有SDK源码权限)
同步查询接口内部移除sleep轮询逻辑:
- 同步接口禁止内部sleep阻塞调用方;
- 硬件状态监测改为异步回调/事件监听模式;
- 对外提供纯非阻塞查询接口。
📝 五、开发避坑总结
- Activity onCreate / onResume、Application onCreate 是ANR高发区,尽量剥离所有同步耗时逻辑;
- 不要依赖Logcat定位ANR,必须获取完整ANR trace主线程堆栈;
- 区分设备选择日志抓取命令:开发板使用
adb pull /data/anr,商用手机使用dumpsys dropbox; - 任何包含
sleep、同步IO、同步Binder调用、锁等待的方法,不要直接在主线程调用; - 对接第三方硬件SDK、原生库时,重点警惕同步查询接口内部隐藏阻塞逻辑。
💭 拓展思考
除sleep外,主线程常见ANR诱因:
- 主线程网络请求、大文件读写;
- synchronized锁竞争,主线程等待子线程释放锁;
- JNI native方法长时间阻塞;
- 跨进程同步Binder调用超时。