第3章 ANR原理与堆栈分析
ANR,全称 Application Not Responding,是Android开发里最让人头疼的问题之一。说白了,就是你的App在干正事的时候卡住太久,系统看不下去了——「嘿,用户还在等你呢,你倒是动啊?」然后直接弹个对话框:「×应用无响应」。嗯,这不仅是用户体验的灾难,还极大概率被Google Play下架警告。我今天就带你彻底搞懂ANR的触发条件、日志抓取、traces.txt解读,以及几个我踩过的坑。
核心认知:ANR不是bug,是系统对主线程阻塞的超时惩罚。主线程必须在给定时间内完成输入事件分发、广播接收、服务启动或ContentProvider查询,否则系统就会给你发「红牌」。
3.1 ANR触发条件——这四种情况要牢记
Android系统对主线程有四个明显的超时监控器,每个都有自己的阈值。我刚开始做性能优化时,经常搞混它们。其实记清楚就可以了:
| 类型 | 触发场景 | 超时时间 | 常见原因 |
|---|---|---|---|
| 输入事件ANR | 触摸、按键等事件分发 | 5秒 | 主线程做耗时操作(网络I/O、大Bitmap解码) |
| 广播ANR | BroadcastReceiver.onReceive() | 前台广播10秒,后台广播60秒 | 广播中执行数据库操作、直接启动Activity |
| 服务ANR | Service的生命周期方法 | 20秒 | onCreate/onStartCommand里做大量计算 |
| ContentProvider ANR | ContentProvider.query/insert等 | 10秒 | Provider跨进程操作超时 |
我个人习惯在项目初期就开启StrictMode,这样能提前发现主线程上的磁盘读写和网络请求。
3.2 日志获取——学会「捞」ANR信息
当ANR发生后,系统会生成两类关键产物:
- logcat日志:实时查看ANR的上下文,比如“ANR in com.example.app (pid 12345)”及CPU使用率。
- traces.txt:dump所有线程的堆栈,保存在
/data/anr/traces.txt或/data/anr/traces_xxx.txt。
如何快速获取?我常用的命令组合:
# 实时过滤ANR
adb logcat | grep -i "ANR"
# 如果设备有root,直接拉取traces.txt
adb shell su -c "cat /data/anr/traces.txt" > traces.txt
# 如果没有root,先抓取bugreport
adb bugreport bugreport.zip
# 解压后在FS/data/anr/目录下找traces文件
我曾经遇到过某些厂商的定制ROM会把traces文件放在/data/system/anr/下,这点需要注意。另外,Android 10以上的设备可能没有root也能用adb shell dumpsys dropbox --print data_app_anr来获取部分ANR信息。
3.3 traces.txt解读——抓住罪魁祸首
打开traces.txt,第一眼会看到一大片堆栈。别怕,我教你快速定位:
- 找主线程:搜索“main”或“thread: main”,通常排在文件最前面。
- 看状态:主线程状态如果是
RUNNABLE但卡在某个方法里,说明它在做CPU密集操作;如果是BLOCKED,说明它在等锁。 - 看堆栈顶部:最上面几行是当前正在执行的方法,比如
android.os.MessageQueue.nativePollOnce是正常等待,但如果是android.view.Choreographer.doFrame后面跟了你自己的业务代码,那就得警惕了。
举个例子,一个典型的输入ANR traces片段:
----- pid 12345 at 2025-03-10 10:15:30 -----
Cmd line: com.example.app
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x12c00010
| sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x7f4f0d0000
at com.example.app.MainActivity$1.onClick(MainActivity.java:35)
- waiting to lock <0x0c1a2b48> (a java.lang.Object) held by thread 3
at android.view.View.performClick(View.java:7440)
...
看到了吗?主线程在onClick里等待一个锁,而这个锁被线程3持有。线程3可能在做什么耗时操作。这就是典型的「锁竞争导致的ANR」。我遇到过好几次,都是子线程里用了synchronized锁住了公共对象,结果主线程点击时也去争这把锁,直接卡死。
注意:traces.txt里的时间戳是ANR发生时的dump时间,不是卡顿开始的时间。你需要结合logcat里“ANR in ...”前后的日志来推断真正卡了多久。
3.4 常见ANR案例——血泪史
案例1:SharedPreferences写入卡主线程
我接手过一个社交App,每次用户发完消息后回列表都会ANR。查traces发现主线程卡在SharedPreferencesImpl.writeToFile上。原来是项目里用SP存了巨大的JSON字符串,每次apply()都会在主线程做同步写入!
解决方式很简单:
- 大JSON改用MMKV或Room数据库。
- 如果非用SP不可,记得在子线程调用
apply(),但apply()内部依然有可能在主线程执行写文件(Android 8.0以下)。
案例2:广播里弹Dialog
有个新闻App在后台收到Push后,广播接收器里直接弹出一个Dialog。前台广播超时10秒,但Dialog展示前需要获取WindowToken,如果当前Activity被回收了,系统会一直等——然后就ANR了。我说实话,这种错误太常见了。
正确做法:在广播里发一个通知(Notification),或者用PendingIntent来启动Activity。
案例3:ContentProvider跨进程查询超时
记得有一次我调试一个三方SDK提供的Provider,它内部查询网络数据,结果网络慢,Provider超时10秒直接导致调用方ANR。你想想看,Provider的CRUD操作如果走网络,那就等于给自己埋雷。
解决办法:Provider的实现应该只做本地操作,耗时网络请求扔给后台Service。
这张图把ANR从触发到分析的整个链路串起来了。你从左上角开始看,系统根据四种超时类型触发ANR,然后通过日志工具获取traces,最后解读堆栈时重点看主线程状态、锁等待和耗时代码。嗯,这样你的排查思路就清晰了。
避坑总结:
- 不要在主线程做任何I/O、网络、Bitmap解码或复杂计算。
- BroadcastReceiver里只做轻量操作,耗时任务丢给IntentService或WorkManager。
- 使用锁的时候注意不要让主线程去等子线程的锁。
- 如果遇到「假ANR」(比如低端机型偶尔卡一下),建议结合CPU负载、线程状态和系统日志综合判断。