什么是 ANR?它产生的根本原因有哪些?

解读

在国内面试中,ANR 是“必考题”。面试官不仅想确认候选人能背出“5 秒/10 秒/20 秒”这些数字,更想听到候选人能把“主线程阻塞”拆解成“消息队列模型 → 调度 → 系统判定”这一整条因果链,并能结合国内常见的“主线程 IO”“同步 Binder 满池”“后台被厂商杀后拉起异常”等真实场景给出定位思路。答得太浅会被追问“怎么定位”,答得太偏(只讲死锁或只讲 GC)又容易露怯。因此答案要分层:先给教科书定义,再给系统级根因,最后落到国内机型与业务代码的“高发病灶”。

知识点

  1. ANR 全称:Application Not Responding,由系统服务 ActivityManagerService(AMS)/InputDispatcher 判定,弹出“应用无响应”对话框或通知。
  2. 触发阈值(原生逻辑,国内 ROM 一般不改):
    • 前台 Service 无回调:20 s
    • 前台广播 onReceive 未返回:10 s(后台 60 s)
    • 输入事件 5 s 未处理
    • ContentProvider publish 在 10 s 内未完成
  3. 判定原理:系统向主线程 Looper 发送一个“判官”消息,若在规定时间内仍未执行到该消息,则采集主线程 trace、kernel 调度、Binder 状态等,写入 /data/anr/ 下的 trace 文件(国内 ROM 大多会额外拷贝到 /data/system/dropbox 并上传云侧,方便厂商后台聚合)。
  4. 根因分类:
    • 主线程阻塞:死循环、死锁、同步网络/IO、密集计算、大量 SQL 或 SharedPreferences apply() 触发 fsync。
    • 系统资源饥饿:CPU 被后台抢占(国内毒瘤 SDK 互相拉活)、I/O 通道排队(低端机 eMMC)、Binder 线程池耗尽(跨进程调用高频)、内存抖动触发全局 GC。
    • 消息调度链被打断:onCreate 里同步等待子线程结果、锁嵌套导致主线程等待子线程,而子线程又等待主线程释放锁。
    • 厂商定制副作用:后台拉起时触发权限弹窗阻塞主线程、折叠屏旋转同步调用系统服务超时、5G SA 切 NSA 时 Modem 返回慢导致 Telephony Binder 调用卡住。
  5. 定位工具:adb pull /data/anr、Android Studio CPU Profiler、Systrace、Perfetto、BlockCanary(国内开源库)、字节码插桩 + 函数耗时埋点、厂商云侧日志(小米/华为后台开放给开发者)。

答案

ANR 是 Android 系统检测到应用主线程在规定时间内无法响应用户输入或生命周期回调而弹出的“应用无响应”错误。其产生的根本原因可以归结为“主线程消息未在阈值时间内执行完”,具体分三类:

  1. 主线程自身耗时或阻塞

    • 网络、文件 IO、数据库大批量写入、SharedPreferences apply() 触发 fsync
    • 密集计算、JSON 解析、大图解码
    • 死锁:主线程与子线程互相持有锁;或锁范围内再调同步 Binder
  2. 系统资源瞬时饥饿

    • CPU 被后台毒瘤抢占,主线程得不到 16 ms 调度片
    • Binder 线程池满(默认 16 条),跨进程调用排队
    • 内存紧张触发全局 GC,Stop-The-World 停顿长
  3. 厂商场景与业务代码耦合

    • 国内 ROM 后台拉起时弹出权限悬浮窗,阻塞主线程 5 s 以上
    • 折叠屏旋转同步请求 DisplayManager 获取物理参数,低端机耗时 >5 s
    • 5G 双卡切卡时,Telephony 服务返回慢,主线程同步等待

定位时先看 /data/anr/trace_xx.txt 主线程栈,再结合 Systrace 确认 CPU 调度与 Binder 耗时,最后通过插桩或 BlockCanary 找到业务函数热点,逐层拆解即可。

拓展思考

  1. 国内厂商对 ANR 阈值“暗改”吗?——绝大多数 ROM 仍沿用原生阈值,但在后台管控场景会提前“冻结”应用,使主线程长时间得不到调度,看起来像是“伪 ANR”;这类 trace 里主线程栈往往停在 MessageQueue.next(),无业务锁,需要结合 kernel 调度信息判断。
  2. 线上如何秒级捕获?——生产环境可开启 libart 的 SignalCatcher 机制,在系统写 trace 前注册 SIGQUIT 回调,把主线程堆栈、内存、网络状态通过 mmap 缓存,用户点击“关闭应用”前即可上报,比传统 pull 文件快 30 倍。
  3. Compose 与协程会消灭 ANR 吗?——声明式 UI + 协程只是把“耗时”从主线程挪到 Default Dispatcher,若开发者在 LaunchedEffect 里写 while(true) 或 withContext(Dispatchers.IO) 里阻塞,主线程仍会因等待结果而 ANR;因此架构升级后,仍需结合 suspendCancellableCoroutine、withTimeout 等 API 做“可取消”设计,才能真正降低 ANR 率。