🧩 一、前言
在 Android 开发中,ANR(Application Not Responding,应用无响应) 是每个工程师都必须面对的问题。
无论是主线程耗时、锁死、系统资源竞争,还是 Broadcast 卡顿,都可能导致应用“假死”并弹出那句可怕的提示框:
“应用无响应。是否关闭?”
本文将系统讲解:
- ANR 的触发机制
- 常见类型与触发原因
- 如何监测和分析 ANR
- 优化与预防实战方案
- 面试高频问题总结
⚙️ 二、ANR 是什么?
ANR:Application Not Responding
系统在检测到 主线程(UI 线程)长时间被阻塞 时,会认为应用已失去响应,从而触发 ANR。
🕐 Android 系统的 ANR 判定时间:
| 场景 | 超时时间 |
|---|---|
| Activity 无响应 | 5 秒 |
| BroadcastReceiver 无响应 | 10 秒(前台)/ 60 秒(后台) |
| Service 无响应 | 20 秒 |
| Input 事件未处理 | 5 秒 |
🧠 三、ANR 触发原理
系统通过 ActivityManagerService (AMS) + Watchdog 来检测应用是否响应。
简化流程如下:
Input → Looper(MessageQueue)
↓
主线程阻塞 or 死循环
↓
系统5秒未收到响应 → AMS发出ANR报告
当输入事件(如点击、触摸)发送到主线程时,若 5 秒内未被处理,Watchdog 会认为应用无响应并生成 ANR 日志。
🧩 四、常见 ANR 类型与原因分析
| 类型 | 触发条件 | 常见原因 |
|---|---|---|
| Input Dispatch Timeout | 输入事件(触摸/点击)5s 未响应 | 主线程执行耗时操作、死循环 |
| Broadcast Timeout | 广播在前台10s、后台60s 内未处理完 | 广播中执行 I/O、网络请求 |
| Service Timeout | 服务在20s 内未启动或停止 | 服务内进行复杂计算 |
| ContentProvider Timeout | Provider 连接10s 内未返回 | 数据库访问阻塞 |
| Watchdog Timeout | 系统 Watchdog 检测到 SystemServer 卡死 | 系统级 ANR(极少见) |
🧮 五、常见场景举例
1️⃣ 主线程耗时操作
fun onClick(view: View) {
Thread.sleep(6000) // ❌ 阻塞主线程
Toast.makeText(this, "Clicked", Toast.LENGTH_SHORT).show()
}
主线程休眠 6 秒 → 触发 Input ANR。
✅ 正确做法:
lifecycleScope.launch(Dispatchers.IO) {
// 耗时操作
val result = fetchData()
withContext(Dispatchers.Main) {
// 更新 UI
textView.text = result
}
}
2️⃣ BroadcastReceiver 执行耗时逻辑
override fun onReceive(context: Context, intent: Intent) {
// ❌ 不要在广播中执行耗时任务
uploadToServer()
}
✅ 正确做法:
override fun onReceive(context: Context, intent: Intent) {
val workIntent = Intent(context, UploadService::class.java)
context.startService(workIntent)
}
3️⃣ 主线程 I/O 或数据库操作
val cursor = db.rawQuery("SELECT * FROM big_table", null) // ❌
✅ 使用异步线程或 Room 的 suspend 函数:
lifecycleScope.launch(Dispatchers.IO) {
val data = dao.queryAll()
withContext(Dispatchers.Main) {
updateUI(data)
}
}
🔍 六、如何监测与分析 ANR
1️⃣ 查看 ANR 日志文件
路径:
/data/anr/traces.txt
通过 adb 命令查看:
adb pull /data/anr/traces.txt
或实时查看:
adb shell cat /data/anr/traces.txt
2️⃣ 日志关键字段
典型 ANR 日志:
----- pid 1234 at 2025-10-20 10:20:15 -----
Cmd line: com.example.app
ANR in com.example.app (com.example.app/.MainActivity)
Reason: Input dispatching timed out (Waiting for a focused window...)
关键字段解释:
Reason:表示触发原因pid:进程 ID- 堆栈跟踪:分析主线程被卡在哪个方法
例如:
"main" prio=5 tid=1 Native
at java.lang.Thread.sleep(Native Method)
at com.example.MainActivity.onClick(MainActivity.kt:22)
可直接定位问题行。
3️⃣ 使用性能工具分析
| 工具 | 用途 |
|---|---|
| Android Studio Profiler | 查看主线程 CPU、内存、方法耗时 |
| Systrace / Perfetto | 系统级卡顿跟踪 |
| BlockCanary | 检测主线程卡顿堆栈 |
| ANR-WatchDog | 自定义监控 ANR 的开源库 |
🛠️ 七、ANR 实时监测方案
💡 使用方式
在 Application.onCreate() 初始化:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
ANRWatchDog.start(timeout = 5000) { stackInfo ->
// 这里可以上传到后台监控系统或写入文件
Log.e("ANRWatchDog", "Detected ANR:\n$stackInfo")
}
}
}
⚙️ 完整工具类实现
package com.hatio.chat.base.utils
import android.os.Handler
import android.os.Looper
import android.util.Log
import java.io.PrintWriter
import java.io.StringWriter
import java.util.concurrent.atomic.AtomicBoolean
/**
* ANR 实时检测工具
* 原理:在主线程和监测线程之间循环心跳检测,超过阈值未响应即认为主线程阻塞。
*/
object ANRWatchDog {
private const val TAG = "ANRWatchDog"
private var timeoutMillis = 5000L // 默认超时阈值
private var callback: ((String) -> Unit)? = null
private val mainHandler = Handler(Looper.getMainLooper())
private val tick = AtomicBoolean(false)
private var running = false
/**
* 启动 ANR 监控
* @param timeout 超时时间(毫秒)
* @param onAnrDetected 回调:当检测到 ANR 时返回主线程堆栈
*/
fun start(timeout: Long = 5000L, onAnrDetected: (String) -> Unit) {
if (running) return
running = true
timeoutMillis = timeout
callback = onAnrDetected
Thread {
while (running) {
tick.set(false)
mainHandler.post {
tick.set(true)
}
Thread.sleep(timeoutMillis)
if (!tick.get()) {
val stackTrace = getMainThreadStack()
Log.e(TAG, "⚠️ ANR detected! 主线程可能被阻塞超过 ${timeoutMillis}ms")
callback?.invoke(stackTrace)
}
}
}.apply {
name = "ANRWatchDogThread"
isDaemon = true
start()
}
Log.i(TAG, "✅ ANRWatchDog started with timeout = $timeoutMillis ms")
}
/** 停止监控 */
fun stop() {
running = false
Log.i(TAG, "🛑 ANRWatchDog stopped")
}
/** 获取主线程堆栈信息 */
private fun getMainThreadStack(): String {
val mainThread = Looper.getMainLooper().thread
val sw = StringWriter()
val pw = PrintWriter(sw)
mainThread.stackTrace.forEach { pw.println(it.toString()) }
pw.flush()
return sw.toString()
}
}
🎯 功能特性
- ✅ 实时监测主线程卡顿(检测 >5s 即视为可能 ANR)
- ✅ 记录主线程堆栈信息
- ✅ 可自定义超时时间
- ✅ 支持日志输出/回调上报
- ✅ 轻量级、零依赖、可随项目启动
🧩 工作原理
| 模块 | 说明 |
|---|---|
| 监测线程 | 每隔 N 秒检查一次主线程响应 |
| 主线程心跳 | 使用 Handler.post() 设置“活跃”标志 |
| 未响应检测 | 若超时未更新标志 → 判定主线程卡死 |
| 堆栈抓取 | 调用 Looper.getMainLooper().thread.stackTrace 获取当前堆栈 |
| 回调处理 | 可输出日志 / 上传至服务端 / 本地文件保存 |
🚀 八、优化与预防方案总结
| 问题类型 | 优化方案 |
|---|---|
| 主线程耗时 | 耗时操作放入协程或线程池 |
| I/O 阻塞 | 使用异步 I/O(OkHttp、Room suspend) |
| 死锁 | 避免嵌套锁、及时释放资源 |
| Handler 消息过多 | 及时清理未处理消息 |
| 动画卡顿 | 使用 Choreographer + FrameMetrics 分析帧率 |
| 数据加载 | 分页加载、懒加载策略 |
💬 九、面试高频问题
| 问题 | 答案简述 |
|---|---|
| ANR 是什么? | 应用主线程在特定时间未响应系统事件(如输入/广播) |
| 主线程能否执行耗时任务? | 不可以,会阻塞 Looper 消息循环导致 ANR |
| ANR 如何分析? | 查看 /data/anr/traces.txt 主线程堆栈 |
| 如何防止 ANR? | 避免主线程 I/O、死锁、长耗时计算 |
| Watchdog 是什么? | 系统线程,用于检测主线程是否卡死 |
| BroadcastReceiver 为什么容易 ANR? | onReceive 在主线程执行,超过 10s 会超时 |
| ANR 与 OOM 区别? | ANR 是“卡住”,OOM 是“内存爆掉”导致崩溃 |
📚 十、总结
| 方向 | 关键点 |
|---|---|
| 触发机制 | 主线程长时间阻塞导致 |
| 检测方式 | traces.txt / Profiler / 自定义 Watchdog |
| 优化策略 | 异步化、线程分离、性能监控 |
| 设计建议 | 主线程只负责 UI,业务逻辑交给协程或后台线程 |
一句话总结:
👉 主线程只做轻逻辑,重任务交给后台。ANR 是可以预测并防御的。
⚡ 十一、延伸阅读