4. 常见ANR场景分析归纳总结
聊到 ANR,你想想看,很多同学第一反应就是「主线程卡了」。其实没那么简单。
我这几年前前后后治理过上百个ANR案例,总结下来,常见场景就那几大类。今天咱们一个一个掰开了揉碎了说。
核心观点: ANR 本质是系统在规定时间内得不到响应,但原因往往藏在你看不到的地方。
4.1 主线程耗时操作
这是最常见的一类,说白了就是主线程干了不该它干的活。
我个人习惯把「主线程触发 ANR」的原因总结成三句话:
- 做了 IO:读文件、写 SharedPreferences、操作数据库
- 算了不该算的:大量图片解码、JSON 解析、正则匹配
- 等了不该等的:Thread.sleep()、LockSupport.park()、wait()
避坑案例: 我曾经在一个 IM 项目里发现,每次收到消息就调 SharedPreferences.edit().putString() 再 apply()。看似没问题,但 apply 会在主线程做锁等待——如果文件很大,写入队列满,主线程一样被卡住。后来全改成异步写入 + 内存缓存,ANR 下降 70%。
还有一个坑:Activity 的 onCreate 里做网络请求。很多同学图方便直接在主线程搞 Retrofit 同步调用。
// 错误示范 —— 主线程做网络同步请求
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 糟糕,这行代码会阻塞主线程!
Response<User> response = apiService.getUser().execute();
}
嗯,只要网络慢点,或者后端挂了,ANR 几乎必现。应该用异步或协程。
4.2 锁竞争与死锁
这属于比较隐蔽的 ANR 场景。主线程没干重活,但它等锁——而且锁被别的线程一直拿着不放。
你想想看,常见的锁争夺点有哪些?
- 数据库锁:SQLite 是单写多读,写线程慢,主线程读就等。
- SharedPreferences 锁:多进程模式下,锁竞争更加严重。
- Log 锁:Log.d 内部也有锁,大量打日志会死锁。
- 自定义锁:HandlerThread 和主线程之间共享锁,顺序反了就死锁。
经典死锁案例: 主线程持有锁 A 等待锁 B,此时工作线程持有锁 B 等待锁 A。两边互不相让,死锁。但系统只看主线程有没有响应,所以只有主线程被判定 ANR。我曾经在排查时遇到一个 case:自定义 View 的 onDraw 里用了 synchronized (cacheLock),而另一个后台线程也在用同一个锁做耗时缓存清理。主线程等了 15 秒就 ANR 了。
排查锁问题,建议先看 traces.txt 里主线程的「waiting to lock」部分。找到锁对象的地址,再看哪个线程持有它。
4.3 Binder 调用超时
Binder 调用分为两类:
- 应用调用系统服务(比如 getSystemService、PackageManager.getInstalledPackages)
- 系统服务回调应用(比如 Input 事件、广播发送)
主线程调 Binder 时,系统会等待远程返回结果。如果远程服务处理慢(比如系统服务压力大、Binder 线程池满),主线程就一直等,直到超时。
我记得有个项目上线后,用户一打开「关于本机」就 ANR。查了半天,发现是 PackageManager.getInstalledPackages(0) 在低端机上要跑 3 秒。系统给的时间只有 5 秒,但如果系统本身也卡,叠加后就超了。
处理思路: 能异步就异步。如果是子线程调 Binder,就算慢也不会 ANR。但有些系统 API 只能在主线程调(比如 ActivityManager.getRecentTasks),无解的话就尽量少调,或者加缓存。
4.4 广播 / Service / ContentProvider 超时
这三种场景其实原理一样:系统给了一个时间窗口,你必须在窗口内完成,否则 ANR。
| 组件 | 超时阈值 | 典型触发场景 | ANR 关键字段 |
|---|---|---|---|
| BroadcastReceiver | 前台广播 10s / 后台广播 60s | onReceive 里做网络请求、写数据库、循环逻辑 | Broadcast of Intent { act=... } |
| Service | 前台 20s / 后台 200s | onCreate / onStartCommand 里有大量初始化 | ServiceRecord{...} timeout |
| ContentProvider | 通常 10s | query 里做复杂 SQL、跨进程大数据传输 | ContentProvider timeout |
看一下真实 traces 里的表现:
"main" prio=5 tid=1 Blocked
at com.example.MyBroadcastReceiver$1.run(MyBroadcastReceiver.java:35)
- waiting to lock <0x0a2b4c00> (a java.util.HashMap)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
重点提示: 广播里的锁等待同样会触发 ANR。很多同学以为只要 onReceive 里代码量不大就没事,但如果它被某个锁卡住了,一样超时。所以广播里最好只做「发通知、启动 Service/JobScheduler」这类轻量操作,真正的处理交给子线程去做。
4.5 综合排查套路
说了这么多,你可能会问:拿到一个 ANR,我怎么快速定位是哪一类?
我个人的排查流程:
- 看 traces.txt 里的主线程堆栈 —— 是正在执行代码(CPU 忙),还是在等锁(Blocked),还是在调 Binder(BinderProxy.transactNative)?
- 看 logcat 中的 ANR 日志 —— 关键词:DALVIK THREADS、ANR in、Reason 字段(比如 Input dispatching timed out / Broadcast of Intent)。
- 结合 CPU 使用率 —— 如果主线程堆栈在一个循环里,CPU 占用 100%,多半是主线程自己在跑耗时计算;如果主线程等锁且 CPU 低,那就是锁竞争。
- 看系统服务状态 —— 如果 traces 里 Binder 线程池全满,那可能是系统服务压力大。
一个小技巧: 很多 ANR 其实不是单次触发,而是「慢累积」。比如每次 onResume 都做 200ms 的布局,反复进入退出 Activity,最后一次卡在某个点上。这种需要用 Systrace 或 perfetto 看更细的时间线。
嗯,总结一下。常见 ANR 场景就这几类,但实际项目里往往混合出现。你可以把本章的 SVG 图存下来,遇到 ANR 时对号入座,大概率不会偏。