初识 ANR:为什么你的 App 会「卡死」?

干了这么多年 Android 开发,我个人觉得 ANR 是最让人头疼的问题之一。不为什么,因为它直接卡用户的手指。用户正刷着朋友圈呢,突然点不动了,心里肯定一肚子火。嗯,我们今天就来好好聊聊 ANR 到底是什么。

1.1 什么是 ANR?

ANR 全称是 Application Not Responding,也就是「应用无响应」。说白了,就是系统觉得你的 App 太忙了,没空搭理用户的点击操作。

  • 官方定义: 当应用的主线程(UI 线程)被阻塞超过一定时间,系统就会弹出一个 ANR 对话框。
  • 用户视角: 屏幕卡死,点击没反应,几秒后弹出「XXX 应用无响应」的窗口。
  • 本质原因: 主线程上执行了耗时操作,或者发生了死锁,导致无法处理新的 UI 事件。

核心点: ANR 不是 Crash,系统不会直接 Kill 应用,而是会先给用户一个选择——「等待」还是「关闭」。

我记得有一次,一个用户反馈说打开某个页面就黑屏,然后弹出 ANR。我一看 traces 文件,好家伙,主线程正在等待一把锁,这把锁被一个子线程死死攥着不放。那个子线程还在做网络请求,网络一慢,整个 App 就挂了。这就是典型的 ANR 场景。

1.2 ANR 的本质

你想想看,Android 系统为什么能实时响应用户的触摸?因为有一个叫 Looper 的东西在死循环里不停地从 MessageQueue 里取消息。如果主线程取出一条消息后,花了超过 5 秒才处理完,那 Looper 就被卡住了,新的触摸事件也就没办法派发了。

所以 ANR 的本质可以归纳为这样一句话:Android 的消息处理机制被阻塞了。

ANR 知识体系总览 ANR 基础 定义与本质 五个核心类型 触发场景与时间 日志与 traces Input 超时 Broadcast 超时 Service 超时 Provider 超时 其他场景 ANR 关键超时时间参数 类型 前台超时 后台超时 InputDispatching 5s 5s BroadcastReceiver 10s 60s Service 20s 200s ContentProvider 10s 10s

上面这张图基本上把本章的知识体系串起来了。下面我们一个点一个点地拆开说。

1.3 ANR 的五大类型

说白了,系统并不是什么事儿都管。它只监控以下四种核心场景,再加上一个「其他」。我习惯把它们记作「IBSP」加上一个兜底。下面这个表格一目了然:

类型 超时时间(前台) 超时时间(后台) 监控机制
InputDispatching Timeout 5 秒 5 秒 Input 事件 5 秒未处理完毕
BroadcastReceiver Timeout 10 秒 60 秒 onReceive 未在规定时间返回
Service Timeout 20 秒 200 秒 onCreate / onStartCommand 超时
ContentProvider Timeout 10 秒 10 秒 Provider 发布或 query 超时

一个小技巧: 后台的 Service 超时时间长达 200 秒,前台只有 20 秒。这意味着你的 App 如果切到后台,Service 可以慢悠悠地工作,但一旦回到前台,立刻就得麻溜起来。我曾经在一个后台下载任务上踩过坑,前台 Service 里做了一个 30 秒的耗时操作,用户回来后直接弹 ANR。

1.4 触发场景详解

理解了类型,我们再看看具体什么情况下会触发。嗯,这里要注意,很多开发者以为只有主线程做耗时操作才会出问题,其实不止。

1.4.1 输入分发超时

这是最经典的 ANR 场景。主线程在处理一个触摸事件或者按键事件时,超过 5 秒没有调用下一个事件分发。说白了,系统等不及了。

  • 常见原因: 主线程中做了大量数据库操作、网络请求(虽然网络库一般会自动切线程)、或者主线程被锁死。
  • 避坑指南: 我曾经在一个监控 App 里把一个庞大 JSON 解析写在了 Activity 的 onCreate 里,用户一打开页面就卡 6 秒以上,ANR 率飙升。后来果断扔到子线程 + 协程里,问题就解决了。

1.4.2 广播超时

这个场景比较隐蔽。你可能会想,广播接收器的 onReceive 方法不是在主线程运行吗?对,如果在 onReceive 里做耗时操作,直接触发 ANR。

而且广播有前后台之分。前台广播(通过 Context.sendBroadcast 发送)必须在 10 秒内返回,后台广播则是 60 秒。

警告: 千万不要在 onReceive 里直接启动一个线程去做耗时任务,因为 onReceive 返回后,系统认为广播处理已经结束了。如果这时候线程还在运行,一旦发生问题,系统不会认为这是广播的锅——它会在其他场景下触发 ANR,而且日志往往不明显。我曾经就因为这个背过锅,查了三天才发现广播里偷偷开了一个线程在写文件。

1.4.3 服务超时

Service 的超时时间比较长,前台 20 秒。但正因为时间长,开发者反而容易放松警惕。我见过有人把几十 MB 的文件解析放在了 Service 的 onCreate 里,导致第一次启动 Service 时 ANR。

我的个人习惯是,在 Service 里凡是耗时的操作,一律用 IntentService 或者 WorkManager,顺手把超时时间也算进去。

1.4.4 ContentProvider 超时

这个相对少见,但一旦碰上就很棘手。Provider 的超时多发生在两个 App 通过 Provider 共享数据时,比如系统通信录 App。如果你在 query 里做了复杂查询,或者 Provider 首次创建时做了太多初始化工作,就很有可能超时。

1.5 如何获取 ANR 日志?

好了,理论说完了,咱们来点实际的。遇到 ANR 后,第一件事就是找 traces 文件。这玩意儿是 ANR 分析的核心证据。

// 传统方式
adb pull /data/anr/traces.txt

// 或者查看最新的 ANR 日志
adb bugreport

// 在 Android 10 及以上,系统会把 ANR 日志存放在
/data/anr/   目录下,
文件名通常是 anr_xxxxx_xxxxx。

关键日志段: 打开 traces 文件后,直接搜索 "ANR in" 或者 "DALVIK THREADS"。你会看到类似这样的信息:

----- pid 12345 at 2023-01-01 12:00:00 -----
Cmd line: com.example.yourapp
...
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 obj=0x... self=0x...
  | sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0x...
  | state=S schedstat=( 0 0 0 ) utm=0 stm=0 core=0 HZ=100
  | stack=0x... stackSize=8MB
  | held by tid=5 (Thread-5)
  at com.example.yourapp.MainActivity.onClick(MainActivity.java:25)
  - waiting to lock <0x...> (a java.lang.Object)
  at com.example.yourapp.OtherThread.doWork(OtherThread.java:10)
  - locked <0x...> (a java.lang.Object)
  at ...

trace 文件里,重点关注这几个字段:

  • "main" 线程的状态: 如果是 Blocked,说明它在等锁;如果是 Runnable,说明它在忙 CPU 计算。
  • held by: 哪个线程持有了主线程需要的锁?这是死锁分析的关键。
  • stack 调用: 找到主线程卡死的具体代码行。

说实话,很多人刚看 traces 文件会觉得眼花缭乱。我的建议是,别急,先找到 "main" 这一行,然后往上往下各看 20 行,基本就能定位到问题函数了。一次不行就多看几次,习惯了就好。

1.6 本章总结

这一章我们主要理清了 ANR 是什么、有哪些类型,以及日志怎么看。说白了,ANR 就是主线程被堵住了。我个人的经验是:先搞清楚类型,再去看 traces 文件,定位代码位置,最后修复。