ANR(Application Not Responding)是 Android 中的一种常见问题。当应用程序未能在规定时间内响应用户输入、完成后台任务或处理系统消息时,系统会弹出一个对话框,提示“应用无响应”。这通常会严重影响用户体验。
在 Android 系统中,ANR 的触发主要有以下几种情境:
- 应用的主线程长时间阻塞,导致 UI 无法响应用户操作。
- 应用在处理特定类型的任务(如广播或服务)时未能在系统设定的时间内完成。
本文将结合 ANR 的源码逻辑和常见场景,深入解析 ANR 的三种主要类型及其解决方案。
一、ANR 的三种主要类型
1. Input Dispatching Timeout(输入事件分发超时)
问题描述
系统对用户输入事件(如点击、触摸等)的分发存在时限。默认情况下,主线程需要在 5 秒 内处理输入事件,否则会触发 ANR。
触发条件
- 主线程被耗时任务(如 I/O、计算任务)阻塞。
- 主线程因线程同步(如锁竞争)而无法处理输入事件。
源码分析
输入事件由 InputDispatcher
模块负责分发,其主要逻辑如下:
void dispatchInputEvent(InputEvent event) {
if (isMainThreadBlocked()) {
scheduleInputTimeout(); // 触发超时处理
} else {
deliverEventToApp(event); // 正常分发
}
}
在 ActivityManagerService
中,如果发现某个应用未响应,则记录并上报:
if (!appResponding) {
mHandler.postDelayed(() -> {
logAnrEvent();
showAnrDialog();
}, ANR_TIMEOUT); // 5 秒默认超时时间
}
解决方案
- 避免主线程阻塞确保耗时任务在工作线程中完成,主线程仅用于处理 UI 相关的逻辑。
// 示例:在主线程中避免长时间操作
new Thread(() -> {
performHeavyOperation(); // 耗时任务
runOnUiThread(() -> updateUI());
}).start();
- 排查线程同步问题检查是否存在锁竞争,避免长时间占用主线程:
synchronized (lock) {
performCriticalTask(); // 确保锁范围尽可能小
}
- 优化布局和绘制性能使用
ViewStub
或延迟加载减少布局复杂度。
2. BroadcastQueue Timeout(广播处理超时)
问题描述
广播接收器必须在指定时间内完成其任务:
- 前台广播:10 秒。
- 后台广播:60 秒。
如果超时,系统会认为应用发生了 ANR。
触发条件
- 在 onReceive() 方法中执行了耗时操作。
- 使用了复杂的广播逻辑或多次调用网络请求。
源码分析
广播超时主要由 BroadcastQueue
管理:
void processNextBroadcast() {
if (currentBroadcast.isRunningLong()) {
handleBroadcastTimeout(); // 广播超时处理
}
}
若某个广播未在指定时间内完成,系统会触发 AppNotRespondingDialog
:
if (isTimeout) {
triggerAnrDialogForBroadcast();
}
解决方案
- 避免在广播中执行耗时操作使用
IntentService
或后台线程处理耗时任务:
@Override
public void onReceive(Context context, Intent intent) {
new Thread(() -> performLongTask()).start();
}
- 减少广播的频繁触发合理设计广播逻辑,避免不必要的广播。
- 使用 WorkManager 替代广播对于复杂任务,可以考虑使用
WorkManager
,支持异步任务调度。
3. Service Timeout(服务处理超时)
问题描述
服务必须在规定时间内完成启动或执行任务:
- 前台服务:20 秒。
- 后台服务:200 秒。
触发条件
- 服务启动时进行复杂的初始化操作。
- 服务中执行了长时间未完成的任务。
源码分析
服务启动逻辑由 ActiveServices
管理:
void startServiceLocked() {
if (timeoutOccurred) {
throwAnrException(); // 抛出 ANR 异常
}
}
如果服务未在规定时间内完成启动,则记录日志并显示 ANR 对话框:
if (timeoutHappens) {
logServiceTimeout();
showAnrDialog();
}
解决方案
- 优化服务启动逻辑避免在
onStartCommand()
或onCreate()
方法中执行耗时任务:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(() -> performHeavyInitialization()).start();
return START_STICKY;
}
- 分解复杂任务将任务拆分为多个小任务,并通过定时器或线程池异步执行。
- 使用 WorkManager 替代长时间运行的服务对于需要后台执行的任务,推荐使用
WorkManager
,其生命周期更加安全可靠。
二、ANR 的排查与分析
1. 使用日志定位
在 logcat 中,ANR 相关日志通常以以下关键字开头:
- “ANR in”:发生 ANR 的应用包名。
- “Reason”:触发 ANR 的原因。
- Reason: Input dispatching timed out (App did not respond to an input event within 5.0s)
- Reason: Broadcast of Intent { act=android.intent.action.BOOT_COMPLETED }
- Reason: executing service com.example.app/.MyService
- “Load”:设备负载情况(CPU 使用率等)。
示例:
ANR in com.example.app
PID: 12345
Reason: Input dispatching timed out (App did not respond to an input event within 5.0s)
Load: 5.68 / 4.23 / 2.11
2. Trace 文件分析
trace.txt
文件是 Android 系统在发生 ANR 时自动生成的日志文件,记录了系统中所有线程的堆栈信息,特别是 主线程的状态。通过分析这个文件,开发者可以定位导致 ANR 的根本原因。以下是详细讲解分析 trace.txt
的步骤。
2.1 如何获取 trace.txt 文件
- 从设备中提取使用 ADB 命令获取 ANR 堆栈:
adb pull /data/anr/traces.txt
- 在 logcat 中标记文件生成路径每次发生 ANR 时,
logcat
会记录trace.txt
的路径。 - 在调试工具中直接查看部分集成开发环境(如 Android Studio)可以在运行时直接查看 ANR 信息。
2.2 trace.txt 的结构
trace.txt
文件主要包括以下内容:
- 线程信息每个线程的状态以线程名开头,如:
"main" prio=5 tid=1 Native
- "main":线程名称。主线程通常是 main。
- prio=5:线程优先级。
- tid=1:线程 ID。
- Native/VM:线程运行环境。Native 表示本地代码,VM 表示虚拟机代码。
- 线程状态紧随线程信息的是其状态,如:
| state=RUNNABLE
常见状态:
- RUNNABLE:线程正在运行或等待 CPU 分配。
- WAITING/TIMED_WAITING:线程等待某个条件或超时时间到达。
- BLOCKED:线程被阻塞,通常因为锁争用。
- 堆栈信息每个线程的堆栈信息按调用顺序列出,从最顶层开始,如:
at android.os.SystemClock.sleep(SystemClock.java:112)
at com.example.app.MainActivity.longRunningTask(MainActivity.java:42)
at com.example.app.MainActivity.onClick(MainActivity.java:30)
2.3 如何分析主线程的堆栈
- 定位主线程
主线程通常标识为:
"main" prio=5 tid=1 Native
主线程是 ANR 问题的重点,因为 UI 操作、输入事件、服务启动等都依赖于主线程。如果主线程被长时间阻塞,系统会认为应用无响应。
- 查找线程状态
主线程的状态直接决定了问题的方向:
- RUNNABLE:线程在运行或等待 CPU。原因:可能是计算任务过于耗时或 CPU 资源不足。
- WAITING:线程正在等待另一个线程的信号或资源。原因:通常是锁等待或阻塞操作。
- BLOCKED:线程正在尝试获取另一个线程持有的锁。原因:锁争用或死锁问题。
- 分析堆栈信息
从主线程堆栈中,重点关注以下几点:
- 调用路径是否正常:是否存在循环调用或无意义的深层递归。
- 方法是否耗时:堆栈中是否包含 I/O 操作、网络请求、大量数据处理等。
- 第三方库问题:检查堆栈中是否包含第三方库的调用,它们可能引发性能问题。
2.4 示例分析
假设 trace.txt
文件中记录了以下主线程堆栈:
"main" prio=5 tid=1 Native
| state=WAITING
at java.lang.Object.wait(Native Method)
- waiting on <0x12345> (a java.lang.Object)
at com.example.app.Worker.performTask(Worker.java:50)
- locked <0x12345> (a java.lang.Object)
at com.example.app.MainActivity.onButtonClick(MainActivity.java:32)
at android.view.View.performClick(View.java:7125)
at android.view.View$PerformClick.run(View.java:28314)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:268)
at android.app.ActivityThread.main(ActivityThread.java:7870)
分析步骤:
- 线程状态
- state=WAITING 表明主线程正在等待另一个线程释放资源。
- 定位阻塞点
- java.lang.Object.wait():主线程在等待一个 java.lang.Object 锁(对象 ID 为 <0x12345>)。
- 锁被 com.example.app.Worker.performTask 方法持有。
- 分析调用链
- 主线程调用了 MainActivity.onButtonClick(),而此方法依赖了 Worker.performTask()。
- 堆栈显示 Worker.performTask 持有锁 <0x12345> 并未释放,导致主线程阻塞。
- 推断问题
- 问题根源是 Worker.performTask() 中未能及时释放锁。
- 解决方法:优化 performTask 方法,确保锁释放及时或改用非阻塞式并发工具。
2.5 综合分析多个线程
ANR 问题可能涉及多个线程的交互。例如,主线程等待某个工作线程,而该工作线程又被其他线程阻塞。这种情况下,需要结合其他线程堆栈信息:
示例
"WorkerThread" prio=5 tid=12 Native
| state=BLOCKED
at com.example.app.DatabaseHelper.query(DatabaseHelper.java:120)
- waiting to lock <0x54321>> (a java.lang.Object) held by tid=14
- WorkerThread 被阻塞,原因是它在 query() 方法中试图获取锁 <0x54321>,而该锁被线程 tid=14 持有。
- 继续查找 tid=14 的堆栈信息,找到问题根源。
2.6 常见问题与解决策略
问题类型 | 表现 | 解决策略 |
---|---|---|
主线程阻塞 | 堆栈显示主线程处于 WAITING/BLOCKED | 检查锁争用,优化主线程任务逻辑。 |
后台线程卡死 | 辅助线程长时间运行,导致主线程等待 | 使用线程池或优化线程任务分配。 |
耗时操作未异步化 | 堆栈中出现 sleep() 或 I/O 操作 | 将耗时操作移至工作线程。 |
网络或数据库瓶颈 | 堆栈中显示网络或数据库方法长时间运行 | 优化查询逻辑或使用缓存。 |
3. 使用 Systrace 或 Perfetto
通过 Systrace 或 Perfetto 工具分析应用的线程调度和性能瓶颈,定位主线程阻塞原因。后续文章会详细讲解Perfetto的相关知识。
三、总结
- ANR 的本质是主线程未能在规定时间内完成预期任务或响应输入。
- 常见类型包括输入事件分发超时、广播处理超时和服务启动超时。
- 解决方案的核心是优化主线程逻辑,将耗时任务移至后台线程,并充分利用 Android 提供的异步工具(如 AsyncTask、HandlerThread 和 WorkManager)。
通过合理设计代码和使用调试工具,开发者可以有效避免和解决 ANR 问题,提升应用性能和用户体验。