ANR 概述:什么是 ANR,为什么会出现 ANR

大家好,我是老张,一个在 Android 底层摸爬滚打十来年的工程师。今天咱们聊聊所有 Android 开发者都躲不开的一个坎 —— ANR。

你可能在测试时见过那个烦人的对话框,也可能在线上收到过成堆的 ANR 报警。说实话,我自己刚入行那会儿,第一次遇到 ANR 完全摸不着头脑,只知道应用卡死了,然后点了“确定”就过去了。后来线上出了事故,才被迫真正去搞懂它。

什么是 ANR?

ANR 的全称是 Application Not Responding,翻译过来就是“应用无响应”。系统觉得你的 App 已经“死”了,就会弹出对话框问用户是要继续等,还是直接关掉。

你想想看,系统怎么知道你的 App 还活着?很简单:它给你安排了一个“监控定时器”。如果你在规定时间内没完成该做的事,系统就判定你无响应,触发 ANR。

这里有个关键点:ANR 的判定只看主线程。为什么呢?因为只有主线程才负责处理用户输入和生命周期回调。如果主线程被别的活儿占住了,用户点屏幕、按 Home 键这些操作就会排队等着,越等越久,最终超时。

个人经验:我刚开始做 Android 时,以为只有点击事件超时才会 ANR。后来有一次线上发现,一个 Service 的 onCreate 里做了大量数据库迁移,直接搞死了一大批用户。嗯,Service 同样会触发 ANR,只不过不同类型组件的超时时间不一样。

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,用户直接卸载。

注意:不要以为加密只有“慢”这一个问题。还要考虑锁竞争:很多加密库底层会持锁,如果主线程在等一个被后台线程占用的锁,照样 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) 块里面又做了耗时操作,那就白费功夫了。

我的建议:如果你维护旧项目,暂时没法改成协程,至少用 AsyncTask 或 Handler 把加密操作挪到子线程。不要因为嫌麻烦就留在主线程上,这个坑我替你踩过了。

最后我画了一张 ANR 从触发到系统响应的完整流程图,你可以对照着理解整个机制:

ANR 触发与处理流程 用户输入 / 系统回调 主线程处理任务 是否超过阈值(5s/10s/20s)? 正常继续 触发 ANR 系统发送 SIGQUIT 生成 /data/anr/traces.txt 弹出 ANR 对话框 用户点击 / 超时 → 杀死进程 关键时间点 输入事件:5秒 前台 Broadcast:10秒 Service:20秒 后台 Broadcast:60秒

图上的流程很清晰:主线程一旦被阻塞超过阈值,系统就会发出 SIGQUIT 信号,强制所有线程 dump 堆栈到 trace 文件,然后弹出烦人的 ANR 对话框。如果用户选择“等待”或者对话框超时,系统就会直接杀掉你的进程。

所以,认识 ANR 只是第一步。后续我们会深入分析主线程加密为何是 ANR 的头号杀器,以及如何通过 trace 文件精准定位问题。保持关注,这一课咱们稳扎稳打。