ANR 概述:什么是 ANR,为什么会出现 ANR
大家好,我是老张,一个在 Android 底层摸爬滚打十来年的工程师。今天咱们聊聊所有 Android 开发者都躲不开的一个坎 —— ANR。
你可能在测试时见过那个烦人的对话框,也可能在线上收到过成堆的 ANR 报警。说实话,我自己刚入行那会儿,第一次遇到 ANR 完全摸不着头脑,只知道应用卡死了,然后点了“确定”就过去了。后来线上出了事故,才被迫真正去搞懂它。
什么是 ANR?
ANR 的全称是 Application Not Responding,翻译过来就是“应用无响应”。系统觉得你的 App 已经“死”了,就会弹出对话框问用户是要继续等,还是直接关掉。
你想想看,系统怎么知道你的 App 还活着?很简单:它给你安排了一个“监控定时器”。如果你在规定时间内没完成该做的事,系统就判定你无响应,触发 ANR。
这里有个关键点:ANR 的判定只看主线程。为什么呢?因为只有主线程才负责处理用户输入和生命周期回调。如果主线程被别的活儿占住了,用户点屏幕、按 Home 键这些操作就会排队等着,越等越久,最终超时。
ANR 的触发条件
我把常见的 ANR 触发场景捋了一下,做成了一张表,你对照着看更清楚:
| 组件类型 | 超时时间 | 关键回调 |
|---|---|---|
| 输入事件(按键、触摸) | 5 秒 | dispatchTouchEvent 等 |
| BroadcastReceiver | 10 秒(前台) / 60 秒(后台) | onReceive |
| Service | 20 秒 | onCreate、onStartCommand、onBind 等 |
| ContentProvider | 10 秒 | query、insert、update、delete |
注意,输入事件的 5 秒是最容易触碰的。只要你的主线程有任何耗时操作超过 5 秒,用户一点屏幕,ANR 就来了。说白了,5 秒就是你的主线程“保命线”。
为什么会出现 ANR?
说白了,就是主线程被长时间阻塞了。导致阻塞的原因千奇百怪,我挑几个最常见的说说:
- 网络请求:直接在 UI 线程用 HttpURLConnection 同步请求,网络稍微一慢就超过 5 秒。
- 读写文件:特别是大文件、频繁 I/O,比如加载本地数据库、图片解码。
- 死锁:两个线程互相等待锁释放,主线程正好在其中一环。
- 加密解密:这是咱们这门课的重点。RSA 非对称加密、AES 加密大量数据,都是 CPU 密集型操作,在主线程上跑简直致命。
- 复杂布局或测量:嵌套深的 RelativeLayout、动画时频繁 requestLayout 等。
我记得有一次,线上用户反馈 App 一点“登录”就卡死。查了 trace 文件,发现主线程卡在一个 RSA 解密调用上,解密的是本地存储的加密 。代码是前人写的,直接把解密扔在了 onClick 回调里。这个案例让我深刻意识到:加密操作在主线程上是不能做的。
为什么会发生主线程加密阻塞?
加密算法天然就是 CPU 密集的。拿 RSA 来说,2048 位的密钥解密一次大概需要几十到几百毫秒,看起来不多对吧?但如果你的 是个大对象,或者需要循环解密多个字段,累积起来轻松超过 5 秒。更糟的是,如果你用了 Bouncy Castle 这种纯 Java 实现,没有通过硬件加速,速度会更慢。
AES 对称加密虽然快一些,但在主线程上处理几十兆的文件,同样会卡住界面。我见过一个极端案例:某个金融 App 在 Activity.onCreate 里解密了 20 多张加密的图片,结果启动就 ANR,用户直接卸载。
避坑指南(我踩过的坑)
我曾经在一个项目里,因为直接在 Activity 的 onCreate 里做了 RSA 解密,结果发布出去后线上 ANR 率暴涨。当时产品经理催得急,我图省事就写成了这样:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 解密加密的配置信息 —— 千万别学我
String decrypted = RSAUtil.decrypt(getEncryptedConfig());
// ... 后续初始化
}
结果?启动就 ANR,用户连主界面都看不到。后来我们改成在 Application.onCreate 里用子线程异步加载,才解决问题。嗯,这里要提醒你:任何可能超过 5 毫秒的操作,都不要放在主线程的启动路径上。
如何预防?
其实预防思路就一句话:把耗时操作扔到后台线程去。具体做法有很多:
- 使用
Thread或HandlerThread(老项目可能还在用) - 使用
AsyncTask(虽然已被标记过时,但依然能跑) - 使用
Coroutine(Kotlin 官方推荐,最现代的方式) - 使用
RxJava的调度器 - 使用
WorkManager处理后台任务
下面用协程举个简单的例子,解密操作放在 IO 线程:
lifecycleScope.launch(Dispatchers.IO) {
val decrypted = RSAUtil.decrypt(encryptedData)
withContext(Dispatchers.Main) {
// 回到主线程更新 UI
textView.text = decrypted
}
}
看到没?加密在 IO 线程跑,结果回调到主线程更新 UI,这样主线程永远不会被阻塞。很多新手容易犯的错误是:在 withContext(Dispatchers.Main) 块里面又做了耗时操作,那就白费功夫了。
最后我画了一张 ANR 从触发到系统响应的完整流程图,你可以对照着理解整个机制:
图上的流程很清晰:主线程一旦被阻塞超过阈值,系统就会发出 SIGQUIT 信号,强制所有线程 dump 堆栈到 trace 文件,然后弹出烦人的 ANR 对话框。如果用户选择“等待”或者对话框超时,系统就会直接杀掉你的进程。
所以,认识 ANR 只是第一步。后续我们会深入分析主线程加密为何是 ANR 的头号杀器,以及如何通过 trace 文件精准定位问题。保持关注,这一课咱们稳扎稳打。