一、ANR 基础概念与核心原理(必考题)
1. 什么是 ANR?为什么会发生 ANR?
答案要点:
- 定义:ANR(Application Not Responding)即应用无响应,是 Android 系统检测到主线程(UI 线程)长时间阻塞时触发的机制,用户会看到 “等待 / 关闭应用” 对话框18。
- 根本原因:主线程被耗时操作(如 IO、网络请求、复杂计算)阻塞,或因锁竞争、CPU 资源不足导致无法及时处理输入事件或系统回调911。
- 系统检测机制:
- 输入事件:5 秒未处理(如点击、滑动)513。
- 广播接收器:前台广播 10 秒、后台广播 60 秒未完成
onReceive
47。 - 服务:前台服务 20 秒、后台服务 200 秒未完成
onStartCommand
等生命周期方法413。 - 内容提供者:10 秒未完成
query
/insert
等操作513。
2. 列举 ANR 的四种场景及超时时间(高频考点)
答案要点:
场景 | 超时时间 | 触发条件 |
---|---|---|
输入事件超时 | 5 秒 | 用户交互(如点击、滑动)未在 5 秒内处理完成513。 |
前台广播超时 | 10 秒 | BroadcastReceiver.onReceive 执行超过 10 秒(如Context.sendOrderedBroadcast )47。 |
后台广播超时 | 60 秒 | 后台广播(如Context.startBroadcast )未在 60 秒内完成413。 |
前台服务超时 | 20 秒 | Service.onStartCommand /onBind 未在 20 秒内返回413。 |
后台服务超时 | 200 秒 | 后台服务执行超过 200 秒(Android 8.0+)413。 |
内容提供者超时 | 10 秒 | ContentProvider 的query /insert 等方法未在 10 秒内完成513。 |
注意:不同 Android 版本可能略有差异,需强调常见标准值47。
二、ANR 日志分析与定位(技术难点)
1. 如何通过日志定位 ANR 原因?
答案要点:
- 日志获取:
- 使用
adb pull /data/anr/traces.txt
导出 ANR 日志67。 - 通过
adb logcat -b events -s anr
实时捕获 ANR 信息6。
- 使用
- 关键字段解析:
ANR in com.example.app
:定位发生 ANR 的应用包名和组件(如 Activity)67。Reason: Input dispatching timed out
:明确 ANR 类型(输入事件、广播等)67。- 主线程堆栈:
"main" prio=5 tid=1 Blocked at com.example.app.MainActivity.loadData(MainActivity.kt:45) // 阻塞代码行 - waiting to lock <0x123456> (a java.lang.Object) owned by thread=10 // 锁竞争
- 分析
state=S
(阻塞状态)、waiting to lock
(锁持有者)及具体代码行号79。
- 分析
- 其他线程状态:
"Thread-10" prio=5 tid=10 Holding lock at com.example.app.DataManager.lockData(DataManager.kt:78) // 持有锁的线程
- 检查是否有子线程长时间持有锁或占用 CPU79。
面试技巧:结合日志示例说明分析步骤,强调从Reason
→主线程堆栈→其他线程状态的逻辑链67。
2. 如何区分 ANR 是应用自身问题还是系统资源不足?
答案要点:
- 应用自身问题:
- 主线程堆栈显示耗时操作(如
Thread.sleep
、数据库查询)911。 - 锁竞争导致主线程等待(如
synchronized
块未及时释放锁)79。
- 主线程堆栈显示耗时操作(如
- 系统资源不足:
- 日志中
CPU usage
显示高负载(如user + kernel > 80%
)713。 - 内存不足导致频繁 GC 或进程被回收913。
- 日志中
- 工具辅助:
- 使用
adb shell top -m 10 -s cpu
查看 CPU 占用,定位高负载进程79。 - 通过 Android Studio Profiler 分析主线程耗时函数111。
- 使用
三、ANR 规避与优化(实战重点)
1. 如何避免主线程阻塞?
答案要点:
- 耗时操作异步化:
- 使用
Coroutine
/Handler
/WorkManager
将网络请求、文件读写等移至后台线程110。 - 示例:
// 使用协程处理耗时任务 viewModelScope.launch { val data = withContext(Dispatchers.IO) { fetchDataFromNetwork() } withContext(Dispatchers.Main) { updateUI(data) } }
- 使用
- 优化布局与渲染:
- 减少布局嵌套,使用
ViewStub
延迟加载非必要视图19。 - 避免在
onDraw
中创建对象,防止内存抖动29。
- 减少布局嵌套,使用
- 合理使用锁:
- 缩小
synchronized
块范围,避免在锁内执行耗时操作19。 - 使用
ReentrantLock
替代synchronized
,提高锁竞争效率19。
- 缩小
2. BroadcastReceiver 导致 ANR 的原因及解决方案
答案要点:
- 原因:
onReceive
在主线程执行,若包含耗时操作(如网络请求、数据库写入),超过 10 秒 / 60 秒触发 ANR25。- 有序广播未及时调用
abortBroadcast()
,导致后续 Receiver 阻塞79。
- 解决方案:
- 耗时操作转后台:通过
IntentService
/WorkManager
处理异步任务210。class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { context.startService(Intent(context, MyIntentService::class.java)) } }
- 限制
onReceive
逻辑:仅解析 Intent 或启动组件,避免复杂计算59。
- 耗时操作转后台:通过
3. Service 导致 ANR 的典型场景及优化
答案要点:
- 典型场景:
- 直接在
Service.onStartCommand
中执行文件下载、数据库批量操作等耗时任务29。 - 前台服务未及时调用
startForeground()
,导致超时阈值按后台服务处理413。
- 直接在
- 优化方案:
- 使用
JobIntentService
(继承自IntentService
)自动处理异步任务并销毁服务110。 - 示例:
class MyJobService : JobIntentService() { override fun onHandleWork(intent: Intent) { // 后台线程执行耗时操作 doHeavyWork() } }
- 前台服务需在 5 秒内调用
startForeground()
,避免超时413。
- 使用
四、ANR 面试高频问题与陷阱
1. 为什么 ANR 通常发生在主线程?子线程阻塞会触发 ANR 吗?
答案:
- 主线程职责:处理 UI 更新、输入事件、系统回调(如 Activity 生命周期、广播接收),任何阻塞都会导致界面无响应89。
- 子线程阻塞:不会直接触发 ANR,但可能通过以下方式间接导致:
- 子线程持有锁,主线程等待锁释放(锁竞争)79。
- 子线程占用大量 CPU 资源,导致主线程无法抢占时间片913。
2. 如何模拟 ANR?列举至少两种方法
答案:
- 输入事件超时:
// 在Activity.onCreate中阻塞主线程 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Thread.sleep(6000) // 超过5秒触发ANR }
- 广播接收器超时:
class MyBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Thread.sleep(11000) // 前台广播超过10秒触发ANR } }
- 服务超时:
class MyService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Thread.sleep(21000) // 前台服务超过20秒触发ANR return super.onStartCommand(intent, flags, startId) } }
3. 如何监控线上 ANR?
答案要点:
- 工具推荐:
- 华为 AGC 性能管理:自动采集 ANR 日志,提供堆栈分析和系统资源状态13。
- BlockCanary:开源库,监控主线程卡顿并输出堆栈信息111。
- StrictMode:开发阶段检测主线程耗时操作(如磁盘 I/O、网络请求)111。
- 实现自定义监控:
- 监听系统
SIGQUIT
信号,解析/data/anr/traces.txt
文件111。 - 通过
Looper.getMainLooper().setMessageLogging
监控消息队列延迟11。
- 监听系统
扩展ANR日志使用
1. ANR 基础信息
** ANR in com.example.app (com.example.app/.MainActivity)
PID: 12345 // 进程ID
Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing its input events)
ANR Details: All threads were suspended except the debugger worker thread! Wait queue length: 1 Wait queue head age: 5001ms // 超时时间,超过5秒触发Input ANR
- 关键字段:
Reason
:明确 ANR 类型(Input/Service/Broadcast/Provider)。Wait queue head age
:阻塞持续时间,对应不同场景的超时阈值(见下文)。
2. 主线程(UI 线程)堆栈
"main" prio=5 tid=1 Blocked | group="main" sCount=1 dsCount=0 obj=0x74a00000 self=0xabc123 | sysTid=12345 nice=0 cgrp=default sched=0/0 handle=0xdef456 | state=S schedstat=( 123456789 12345678 1234 ) utm=4 stm=6 at com.example.app.MainActivity.loadData(MainActivity.kt:45) // 阻塞发生的代码行 - waiting to lock <0x123456> (a java.lang.Object) owned by thread=10 // 等待的锁对象 at android.app.ActivityThread.handleMessage(ActivityThread.java:1994) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6938)
- 分析重点:
state=S
:表示主线程处于阻塞(Sleep)状态。waiting to lock
:若存在锁竞争,显示被哪个线程(如thread=10
)持有锁。- 代码行号:直接定位到阻塞发生的具体方法(如
MainActivity.loadData
)。
3. 其他线程状态
"Thread-10" prio=5 tid=10 Holding lock | group="main" sCount=1 dsCount=0 obj=0x123456 self=0x ghi789 | sysTid=12346 nice=0 cgrp=default sched=0/0 handle=0x jkl012 | state=S schedstat=( 76543210 1234567 890 ) utm=2 stm=5 at com.example.app.DataManager.lockData(DataManager.kt:78) // 持有锁的线程代码 - locked <0x123456> (a java.lang.Object)
- 若主线程因等待锁而阻塞,需检查其他线程是否长时间持有锁(如耗时操作未释放锁)。
不同场景 ANR 的日志分析实战
1. Input ANR:主线程阻塞在耗时操作
- 日志特征:
Reason
包含Input dispatching timed out
。- 主线程堆栈显示在执行耗时操作(如 IO、复杂计算、未异步处理的网络请求)。
- 示例分析:
// 主线程在执行文件读取(耗时操作未异步化) at com.example.app.MainActivity.loadLargeFile(MainActivity.kt:105) at com.example.app.MainActivity.onCreate(MainActivity.kt:40)
- 优化方向:将耗时操作移至子线程(如
Coroutine
/AsyncTask
/WorkManager
)。
2. BroadcastReceiver ANR:onReceive 耗时过长
- 日志特征:
Reason
包含Timeout during broadcast handling
。- 主线程堆栈显示在
BroadcastReceiver.onReceive
中执行耗时操作(如数据库写入、网络请求)。
- 特殊场景:
- 有序广播(Ordered Broadcast):若在
onReceive
中未及时调用abortBroadcast()
或处理结果,可能导致后续 Receiver 阻塞。 - 前台广播超时阈值 10 秒,后台 60 秒,需通过
android:process
或IntentService
异步处理。
- 有序广播(Ordered Broadcast):若在
- 示例日志:
"main" prio=5 tid=1 Blocked at com.example.app.MyBroadcastReceiver.onReceive(MyBroadcastReceiver.kt:30) // 耗时的网络请求
3. Service ANR:后台任务未异步化
- 日志特征:
Reason
包含Timeout executing service
。- 主线程堆栈显示在
Service.onStartCommand
中执行耗时逻辑(如未使用IntentService
或协程)。
- 典型错误:
// 直接在Service主线程处理文件下载 at com.example.app.DownloadService.onStartCommand(DownloadService.kt:55)
- 优化方案:使用
JobIntentService
或WorkManager
处理异步任务。
4. 锁竞争导致的 ANR
- 日志特征:
- 主线程状态为
waiting to lock
,指向某个被其他线程持有的锁(如synchronized
对象)。 - 持有锁的线程可能在执行耗时操作(如死锁、长耗时同步块)。
- 主线程状态为
- 示例分析:
// 主线程等待线程10释放锁 "main" waiting to lock <0x123456> (owned by thread=10) "Thread-10" holding lock <0x123456> at com.example.app.DataManager.lockData(...)
- 解决方案:缩小同步块范围,避免在锁内执行耗时操作。
ANR 日志分析的核心步骤(面试高频考点)
- 定位 ANR 类型:通过
Reason
字段确定是 Input/Service/Broadcast 等类型。 - 提取主线程堆栈:找到阻塞发生的具体方法(关注代码行号和锁信息)。
- 检查超时阈值:对比日志中的
Wait queue head age
是否超过对应场景的阈值(如 5 秒、10 秒)。 - 分析其他线程:查看是否有子线程持有锁、长时间占用 CPU 或阻塞主线程。
- 结合代码逻辑:确认阻塞是否由耗时操作(IO / 网络 / 复杂 UI)、未异步化任务或锁竞争导致。
实战工具与技巧
- adb 命令辅助:
adb shell dumpsys activity activities | grep mResumedActivity
:查看当前卡顿的 Activity。adb shell top -m 10 -s cpu
:定位 CPU 占用高的进程,辅助判断是否因 CPU 繁忙导致主线程阻塞。
- Android Studio Profiler:
- 通过 CPU Profiler 查看主线程在 ANR 前后的函数调用耗时,定位耗时方法。
- 避免 ANR 的最佳实践:
- 主线程仅处理 UI 更新,耗时操作通过
Coroutine
/Handler
/WorkManager
异步化。 - 限制
BroadcastReceiver.onReceive
执行时间(10 秒内结束,复杂逻辑启动 Service)。 - 避免在
onCreate
/onResume
等生命周期中执行耗时初始化操作。
- 主线程仅处理 UI 更新,耗时操作通过