常见ANR原因分类:到底是谁在卡主线程?
做过ANR排查的人都有个共同感受:明明感觉没做什么,系统就给你弹了个“无响应”弹窗。其实说白了,ANR的本质就是主线程被阻塞了超过5秒(Activity)或10秒(BroadcastReceiver)。但主线程为什么会被堵?我根据自己的实战经验,把最常见的原因分了五大类。
嗯,我梳理了一个分类图,先看整体结构,再一个一个拆开讲。
一、主线程耗时操作——最常见,也最容易被忽视
说白了,主线程就一条路,你把这条路堵了,车(消息)就过不去。我见过太多新人在主线程里做这些事:
- 网络请求——比如直接用HttpURLConnection在UI线程请求数据,网络又慢,5秒瞬间就过去。
- IO读写——读写文件、数据库查询,尤其是没加索引的SQLite查询,一查就是几百毫秒。
- 密集型计算——比如在主线程解析大JSON、做图片裁剪、循环遍历上万条数据。
Bitmap.compress()输出到文件。结果用户选了一张4K照片,压缩了将近8秒,直接报ANR。后来才意识到,IO和计算必须丢到子线程去。
怎么监控?我建议在Looper.getMainLooper()里加一个自定义Printer,打印每条消息的执行耗时。超过200ms就上报为潜在风险。
二、锁竞争——你等我也等,大家一起等
多线程环境下,锁是必需品。但如果在主线程里持有一个锁,而子线程也拿着另一个锁互等,就会死锁。即使没死锁,单是锁等待也会让主线程卡住。
// 典型死锁场景
// 线程A(主线程)
synchronized (lock1) {
// 同时想要lock2
synchronized (lock2) { ... }
}
// 线程B
synchronized (lock2) {
// 同时想要lock1
synchronized (lock1) { ... }
}
我个人的习惯是:锁的粒度要小,并且绝不让主线程去等一个长时间被占用的锁。如果真的需要同步,用ReentrantLock配合tryLock(timeout),超时就抛出警告。
Object锁来同步日志写入。结果主线程偶尔也要写日志,就出现锁竞争。解决办法很简单——给日志写队列单独加一把锁,或者用Log.isLoggable提前判断。
三、Binder调用阻塞——你以为只是发了个消息?
Binder是Android进程间通信的基石。但主线程同步调用Binder方法时,如果服务端响应慢,主线程就会一直等。最常见的场景:
- 系统服务调用:比如
LocationManager.getLastKnownLocation()、PackageManager.getInstalledApplications() - ContentProvider查询:跨进程的
ContentResolver.query(),如果Provider在另一个慢进程里,主线程就挂住了。
我记得有一次排查一个ANR,traces文件显示主线程卡在BinderProxy.transact()上,回调的却是WifiManager.getScanResults()。后来才知道,那个系统服务底层正在处理大量WiFi扫描结果,Binder线程池都满了,主线程排队等了好几分钟。
解决方案?对于自己写的Binder调用,可以设置oneway标志,让调用变成异步。但系统API没办法改,只能尽量不在主线程调,或者加超时重试机制。
四、高内存压力——一个“温柔”的ANR推手
高内存压力本身不会直接导致ANR,但它会引发一系列连锁反应。当App的Java堆接近上限时,系统会频繁触发GC。而在ART下,虽然大部分GC是并发的,但仍有STW(Stop-The-World)的暂停阶段。
我来列一下常见的内存压力场景:
| 场景 | 影响 |
|---|---|
| 频繁分配大量小对象(比如循环中new对象) | 触发Young GC,每次几十毫秒 |
| Bitmap对象未及时回收 | 占用Native内存,触发Global GC,可能会STW数百毫秒 |
| Fragment/Activity泄漏 | 堆内存持续增长,系统不断尝试GC,越来越慢 |
我习惯在性能监控中加入Runtime.getRuntime().totalMemory()和freeMemory()的采样,当堆使用率超过85%时自动报警。另外,记得用LeakCanary排查泄漏,这是基本功了。
五、GC停顿——被遗忘的“时间黑洞”
ART下的GC虽然比Dalvik提升很多,但依然有暂停。尤其是并发标记阶段的最后一步——重标记(Remark),需要暂停所有线程去修正引用关系。这个暂停时间取决于存活对象数量和堆的大小。
实测数据: 在一次项目优化前,我拍过log。一个列表页快速滑动时,GcReason=AllocStall导致的STW时间达到120ms。虽然单次没到5秒,但连续滑动过程中,GC次数增加,用户就会明显感到掉帧,进而引发ANR(因为事件滞留太多)。
怎么应对?
- 减少对象分配频率——用对象池、复用数据类型(如
SparseArray替代HashMap<Integer, Object>)。 - 避免在onDraw里new对象——这会导致屏幕每帧都触发GC。
- 使用
android:largeHeap="true"?我不推荐。那只是延长了GC触发时间,并没有减少GC代价,反而可能让卡顿更集中。
小结:一张表看清所有原因
我把这五类原因的关键点和排查手段汇总成表,方便你日常复习:
| 原因分类 | 典型场景 | 主线程直接阻塞原因 | 我的排查手段 |
|---|---|---|---|
| 主线程耗时操作 | 网络、IO、计算 | CPU执行耗时 = 主线程忙等 | TraceView / SystemTrace看函数耗时 |
| 锁竞争 | 死锁、锁等待超时 | 线程处于BLOCKED状态 | 看traces.txt是否有lock信息 |
| Binder调用阻塞 | 系统服务、ContentProvider | 线程处于WAITING on Binder | 抓取Binder线程池状态 |
| 高内存压力 | 大Bitmap、内存泄漏 | 间接引起频繁GC | Heap dump + MAT分析 |
| GC停顿 | 大量对象分配 | STW暂停所有线程 | 看logcat中GC日志的时间戳 |
嗯,基本上当你碰到一个ANR,先把traces.txt拉出来,看主线程堆栈停在哪个方法上。再对照上面五类,八九不离十。下一类经验,我再跟你聊聊怎么从源码层面读懂ANR的生成机制——不过那是后面章节的事了。