第2章 ANR产生原因:主线程阻塞、耗时操作、死锁、Binder调用超时

ANR产生原因总览 ANR原因 主线程阻塞 UI事件排队超时 耗时操作 IO/网络/大计算 死锁 线程锁竞争 Binder调用超时 Server端响应慢 核心排查思路 • 主线程阻塞 → 看Looper日志,检查dispatchMessage耗时 • 耗时操作 → 关注IO、网络、反射、Bitmap解码 • 死锁 → 使用Thread Dump,分析锁的等待链 • Binder超时 → 抓取binder trace,看远端服务是否卡死

这一章,我们来聊聊ANR到底是怎么产生的。很多人一提到ANR就想到“主线程卡了”,但其实原因远不止这一条。我在项目里见过各种奇葩的ANR,有的是死锁,有的是Binder调用超时,甚至还有因为ContentProvider初始化太慢导致的——嗯,后面我会细讲。

你想想看,系统判定ANR的标准是什么?说白了就是一个超时机制:如果主线程在规定时间内没有处理完一条消息,或者某个广播、服务启动没按时完成,系统就会弹框。但这个“超时”的锅,很多时候并不在主线程自己身上。

1. 主线程阻塞

这是最直接的ANR原因。主线程(也叫UI线程)负责处理所有用户交互、布局绘制、事件分发。一旦它卡住,系统就炸了。常见的阻塞场景有:

  • 密集计算:比如在主线程里做大型JSON解析、Bitmap缩放、字符串拼接。我印象特别深,有一版App在启动时用主线程解析一个300KB的JSON,直接ANR。后来我改成了异步,ANR就没了。
  • 磁盘IO:SharedPreferences的commit()(同步写入)、直接读写文件、SQLite操作。记住,apply()只是异步写磁盘,但commit()会阻塞主线程等结果。
  • 网络请求:没开子线程就直接用HttpURLConnection或OkHttp同步调用——这个应该没人犯了吧?但我在老项目里还见过。
  • 锁的等待:主线程去拿一个被子线程长时间持有的锁,也是常见原因。

重点提醒:系统给主线程的“宽容时间”是5秒(对于输入事件)或10秒(广播/服务)。实际上,现在很多厂商改成了更短的时间,华为、小米有些机型只有3秒就会弹出ANR。所以别卡主线程超过2秒,安全第一。

2. 耗时操作

这里特意把“耗时操作”单独拿出来说,是因为很多开发者觉得“只要不在主线程里做就行”——这话对,但不全对。为什么?因为即使你放在子线程里,如果子线程里有一个同步锁、Binder调用或者任务队列堆积,依然可能间接导致ANR。

我记得有一次排查一个奇怪的ANR,主线程并没跑任何代码,但就是卡了。后来发现是AsyncTask串行线程池里积压了上百个任务,导致某个onPostExecute迟迟得不到执行——而它更新UI的时机又被主线程的Handler阻塞了。这种事情很隐蔽。

常见的耗时操作分类:

操作类型典型场景建议方案
IO密集型读大文件、网络下载、数据库批量写入异步线程 + 线程池,限制并发数
CPU密集型图像处理(Bitmap缩放/滤镜)、大数组排序子线程 + 适当降低优先级
锁竞争多个线程抢同一个ReentrantLock或synchronized减少锁范围、用ReadWriteLock或ConcurrentHashMap
Binder 远程调用频繁调用getSystemService、跨进程通信缓存结果、异步回调、控制频率

3. 死锁

好,我们来聊聊死锁。死锁的经典条件是四个:互斥、请求与保持、不可剥夺、循环等待。但在Android里最常见的死锁,就是主线程和子线程相互等待对方释放锁。

// 假设主线程持有锁A,想拿锁B
// 子线程持有锁B,想拿锁A
// 结果:两个线程互相等着,主线程卡死 → ANR

我以前在做一个视频播放模块时碰到了这个坑。主线程里有个Handler回调,会取锁A;同时一个线程池里的任务先拿了锁B,然后调用了主线程的post方法——任务里又需要等锁A释放。好家伙,死锁了。当时线上ANR率暴增,查了好久才抓到。

我的排查经验:抓取ANR文件里的VM TRACES AT THE TIME OF ANR部分,直接看所有线程的堆栈。如果发现有2个以上线程互相卡在LockSupport.park()或wait()上,基本就是死锁。查看"Binder:XXXX_XX"线程的锁状态,也能辅助定位。

4. Binder调用超时

你有没有遇到过一种ANR,主线程堆栈里干干净净,没有任何耗时操作?那很可能就是Binder超时了。系统服务(比如PackageManager、WindowManager、ActivityManager)都是跨进程的,通过Binder通信。客户端发请求后,如果在20秒内没收到回复(不同服务时间不同),就会抛TransactionTooLargeException或直接ANR。

我记得一个特别典型的案例:某个App在启动时调用了PackageManager.getInstalledApplications(),这个调用需要遍历所有已安装的应用列表。如果手机里装了500多个应用,这个Binder调用可能耗时超过10秒。而主线程在等它返回——ANR就出现了。解决方法很简单:把这种调用放到子线程,或者用PackageManager的缓存版本。

警告:Binder缓冲区最大只有1MB,千万别一次性传输大图或大量数据。否则直接TransactionTooLargeException,即使不ANR也会导致程序崩溃。

避坑指南:我自己的几招

  • 开线程池时一定要设名字:new ThreadFactoryBuilder().setNameFormat("image-pool-%d")。这样ANR文件里直接看到“image-pool-2”线程卡在哪,省去猜的功夫。
  • 慎用getView()里的锁:ListView或RecyclerView会在主线程里调用getView(),如果你在里面加锁,很容易和别的线程死锁。
  • 对Binder调用设超时:如果是自己写的AIDL接口,尽量在客户端设置transact()超时,避免无限等待。
  • 用StrictMode抓主线程IO:在Debug版本开启StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build()),能提前发现潜在问题。

总结下来,ANR的原因就那么几类,但实际情况往往是多个因素叠加。比如主线程里发起了一个Binder同步调用,而那个Binder服务端又因为死锁卡住了——这就变成了链式反应。我的建议是:每次遇到ANR,先看主线程堆栈,再看其他线程的锁状态,最后看Binder trace。按这个顺序,大部分疑难杂症都能解开。