跳到主要内容

ANR

ANR(Application Not Responding)是 Android 检测到应用没有在规定期限内完成某类响应或组件处理的异常状态。它关注的是系统等待的工作是否及时完成,不只是“某个函数运行得比较慢”。排查时先确定超时类型,再沿执行与等待关系寻找原因,不能看到主线程栈顶就立即下结论。

1. 面试核心​

先记住这几条,再读正文
  • 掉帧、ANR 和崩溃是不同现象,ANR 由特定响应期限触发,不能把所有卡顿都称为 ANR。卡 · 卡顿、ANR 与崩溃
  • 输入、广播、Service 等有不同检测条件,不存在覆盖所有 ANR 的统一“主线程 5 秒规则”。卡 · 先读超时原因
  • 主线程可能在计算,也可能等锁、I/O 或 Binder,把实际工作移到后台后再在主线程同步等待,仍然会阻塞响应。卡 · 执行链与等待链
  • ANR 排查需要类型、时间、全线程栈和系统调度证据,真正根因可能在锁持有线程、远端进程或迟迟未获调度的工作中。卡 · 现场证据与定位
  • nativePollOnce 通常意味着线程在等消息,既不能直接把它当作根因,也不能据此忽略其他线程或已经结束的超时阶段。卡 · nativePollOnce 的解释
  • goAsync() 延后广播完成时点,不会取消广播执行期限,仍需及时调用 PendingResult.finish()。卡 · 广播异步处理的期限
  • StrictMode 和压力测试能提前暴露部分问题,验证应覆盖原来的等待链和真实设备条件,而不只看一次没有弹出对话框。卡 · 预防与修复验证

2. 响应期限与检测类型​

2.1 卡顿、ANR 与崩溃​

卡顿表示用户看到的帧或交互没有按预期及时完成;ANR 表示系统等待的某项工作已经超过对应期限;崩溃则通常意味着未处理异常、非法内存访问等原因使进程异常终止。

一次主线程长任务可能先造成多帧卡顿,持续阻塞后再触发输入 ANR,但两者没有固定的一一对应关系。一个后台广播没有按时结束,即使主线程此刻还能绘制,也可能出现广播 ANR。

ANR 不是在某一行普通业务代码抛出、由外层 try/catch 捕获后就能消除的 Java 异常。系统是否显示对话框、报告怎样采集,还取决于类型、前后台状态和平台行为,不能只用“用户有没有看到弹框”判断是否发生过 ANR。

2.2 先读超时原因​

类型或线索系统主要在等什么优先排查什么
Input dispatch timeout输入得到应用响应主线程工作、锁、同步调用、窗口是否及时就绪
Broadcast receiver timeout广播处理完成onReceive() 所在线程、异步任务、finish()、应用冷启动
Executing service timeoutService 生命周期相关执行完成主线程前序工作、组件初始化、同步 I/O 或依赖调用
Content provider not responding相应 Provider 请求及时完成Provider 初始化、执行请求的 Binder 线程和依赖资源

AOSP 和 Pixel 的输入分发默认超时通常为 5 秒,设备实现可能调整。这个数值不能套给所有组件,也不能解释为“主线程任何时候运行超过 5 秒就必然触发同一种 ANR”。例如没有可接收输入的焦点窗口,也需要检查窗口创建和配置,而不仅是业务方法耗时。

广播期限还与 Intent 是否带有 FLAG_RECEIVER_FOREGROUND 等条件和平台版本有关,不应把它简单等同于“应用前台或后台”。Android 14 的相应机制还会考虑进程是否缺少 CPU 执行机会。面试需要准确说明计时对象与条件;现场定位应以实际系统报告和版本为准。

3. 执行链与等待链​

3.1 主线程忙与主线程等待​

主线程承担输入分发、界面更新和大量组件回调。某项任务长时间占住它,后面的任务只能等待。但长时间占用不一定表现为 CPU 使用率高。

栈或 trace 线索可能含义后续需要的证据
持续出现在业务计算或大型解析CPU 工作过多实际运行时间、热点、输入规模
等待 monitor 或锁其他线程没有释放资源锁持有者、持锁范围及其后续等待
文件或数据库调用中停留同步 I/O 或数据库竞争I/O 耗时、事务、锁与存储状况
同步 Binder 调用等待返回远端处理慢或远端也在等待对端线程、服务负载与调用次数
处于 runnable 却长期没有运行有资格执行,但没有获得足够 CPU调度轨迹、系统负载与线程竞争

例如主线程为了读取配置取得 configLock,后台线程却持有该锁执行网络请求。表面上主线程只是一个很短的读取,实际响应时间取决于网络请求何时结束。修复应缩小持锁范围或改变数据交付方式,不能仅优化读取方法本身。

3.2 异步必须沿调用链成立​

把请求交给线程池后立即在主线程执行 Future.get()、join() 或等待锁,主线程仍然需要等结果。工作位置改变了,等待依赖没有改变。

同样,suspend 只描述可挂起的调用形式,不保证函数内部所有代码都在后台线程执行。生命周期协程通常从主线程开始,阻塞 I/O 和大量计算需要明确放到合适的执行上下文;结果准备好后再异步更新 UI。

同步 Binder 调用也要沿依赖检查。应用主线程等待远端服务,远端服务又等待应用回调,可能形成互相等待。只看应用这一侧的一个栈,无法解释整个链路,参见 Binder。

3.3 广播异步处理的期限​

BroadcastReceiver.onReceive() 默认在主线程执行;动态注册时可以指定其他 Handler。同步处理的完成时点通常是回调返回,调用 goAsync() 后则把完成责任交给返回的 PendingResult。

异步路径必须在所有退出分支及时执行 finish(),通常用可靠的清理结构保证。但即使 finally 中写了 finish(),任务长时间排在线程池队列里还没开始,也仍然可能超时。

这说明广播 ANR 不一定是主线程正在执行慢方法:可能是工作线程池已被其他任务占满,也可能是异步 I/O 没有结束。goAsync() 提供短暂异步处理能力,不是无限后台执行许可;可延后的长任务应交给合适的 Android 后台任务 机制。

4. 现场证据与定位​

4.1 保留同一次事件的上下文​

先确定一次事件的包名、进程、版本、发生时间、用户操作和系统超时描述。再把线程栈、日志和 trace 对齐到这一段时间,避免把重启后的空闲栈解释成故障当时的状态。

可在已连接并授权调试的设备上采集 bugreport:

adb bugreport

报告包含系统日志、线程栈等诊断信息,具体内容和访问能力受设备与系统版本影响。它不是只包含某一条 ANR 的小文件;查找时应对应事件时间、进程和组件,分享前处理无关的设备与用户信息。

线上报告还需要对应发布构建及符号信息。混淆后的 Java/Kotlin 栈应使用匹配构建的 mapping 还原,native 栈需要匹配二进制和符号;错误版本的符号文件会把定位带到无关代码。

4.2 从受阻线程追到持有者​

线程栈显示的是采样时正在执行或等待的位置,不自动告诉你前面发生了多久。主线程若等待锁,沿对象标识和线程信息找持锁线程,再继续看它是在计算、I/O、等待另一个锁,还是同步调用远端。

Perfetto 等系统 trace 可以补充时间关系:线程何时被唤醒、何时真正运行、某个同步 Binder 调用占了多久,以及其他线程和系统进程是否同时拥塞。这样才能区分“应用做了太多工作”和“工作具备执行条件但没有被调度”。

没有完整 trace 时,要把结论分成已知与待验证。比如“主线程当时等待某个锁”可以由栈支持;“后台网络请求持锁 8 秒”则需要持锁线程、日志或复现进一步确认,不能凭一个方法名猜出时长。

4.3 nativePollOnce 的解释​

MessageQueue.nativePollOnce() 是消息循环等待工作的路径,正常空闲时也会出现,见 Android 消息机制。它本身不能证明主线程执行了一个有问题的死循环。

ANR 报告出现这个栈,可能因为堆栈采集偏晚,主线程已经恢复空闲;也可能因为真正超时的是后台广播或 Provider 的工作线程。应回到超时类型检查相关线程和时间,而不是一律忽略该报告。

如果 UI 线程约束被破坏,还应检查 View 层级修改等问题是否影响消息处理。只有取得具体证据后,才进一步分析同步屏障或平台实现;不能看到 nativePollOnce 就直接断言“同步屏障泄漏”。

5. 预防与修复验证​

5.1 开发阶段暴露阻塞操作​

StrictMode 可以在开发阶段检测部分不合适的线程操作,例如主线程磁盘和网络访问。下面的函数应在调试构建的主线程初始化时调用:

import android.os.StrictMode

fun enableDebugThreadChecks(debugBuild: Boolean) {
if (!debugBuild) return
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}

这里设置的是调用线程的策略,示例记录问题而不直接终止进程。它不能替代完整 ANR 检测:锁等待、CPU 计算、某些 native 行为与调度问题不一定由这些检查覆盖。

5.2 修复必须改变阻塞关系​

不同原因需要不同修复:主线程大量计算应减少工作或移到后台;锁竞争应缩小临界区并避免持锁 I/O;线程池饱和要检查排队、并发预算和任务分类;慢 Binder 调用要减少同步依赖或改为合适的异步交付。

不要用无限增加线程数处理所有等待。更多线程可能增加竞争和内存,甚至加剧系统负载。任务拆分后,也要保留错误、取消、顺序和过期结果处理。

验证时重复原操作,覆盖冷启动、较大数据量、慢存储、弱网和并发等与故障有关的条件。对照主线程占用、锁等待、排队与任务完成时间,确认原来的超时链已经缩短或消除;同时检查新实现没有破坏功能。

开发机上没有弹出一次 ANR 对话框,只是有限证据。线上还需要观察相同构建范围内的事件类型、发生率和影响场景,避免不同版本或用户群混在一起造成错误结论。

6 · 面试题卡片​

本页题目与参考回答

6.1 ANR、卡顿和崩溃有什么区别?​

考察重点:用户体验、系统响应期限和进程终止的区别。

相关内容:卡顿、ANR 与崩溃。

参考回答

卡顿是帧或交互延迟,ANR 是系统等待的某项应用处理超过期限,崩溃通常是异常或非法访问导致进程异常终止。 长主线程任务可能先卡顿再触发输入 ANR,但不能把所有卡顿都等同于 ANR。

后台组件超时也可能产生 ANR,且不一定总以同样的对话框呈现。

6.2 ANR 是否都由主线程阻塞 5 秒触发?​

考察重点:检测类型、计时对象和版本条件。

相关内容:先读超时原因。

参考回答

不是。5 秒是 AOSP/Pixel 输入分发默认超时的典型值,不是所有 ANR 的统一规则。 广播、Service 和 Provider 等具有不同完成条件及期限,设备实现也可能不同。

广播还要区分优先级标记与平台版本,不能只按应用是否在前台套数值。排查先读报告中的超时原因,再选择相关线程与执行链。

6.3 工作已放到子线程,为什么仍可能发生 ANR?​

考察重点:工作执行位置与同步等待依赖。

相关内容:执行链与等待链。

参考回答

子线程执行不代表主线程不再等待。 主线程调用 Future.get()、join() 或等待子线程持有的锁,仍然无法及时响应。

另外,后台广播任务自身也有完成期限。应该沿等待关系检查锁持有者、线程池排队、同步 Binder 与任务完成责任,而不只确认创建过子线程。

6.4 怎样排查 ANR?​

考察重点:事件类型、现场关联、线程与系统证据。

相关内容:现场证据与定位、修复必须改变阻塞关系。

参考回答

先确认超时类型、时间、版本和用户操作,再结合全线程栈、日志和 trace 追踪受阻工作。 主线程计算就查热点;等锁就找持锁线程;同步 Binder 慢就继续查对端;runnable 长时间没运行则看调度与系统负载。

修改后用同一场景和相关压力条件对照等待时间与完成情况,同时验证功能。不能只凭栈顶方法名,或一次没有弹框就判断修复完成。

6.5 ANR 栈顶是 nativePollOnce,能得出什么结论?​

考察重点:采样时刻、消息循环与相关线程。

相关内容:nativePollOnce 的解释。

参考回答

它通常说明采样时线程在等待消息,不能单独认定这是 ANR 根因。 可能采样偏晚,也可能超时责任在广播工作线程或其他组件中。

应重新对齐事件时间和类型,检查相关线程与系统 trace。它既不是“死循环导致 ANR”的直接证据,也不是可以忽略报告的充分理由。

6.6 goAsync 能让广播无限期在后台处理吗?​

考察重点:广播完成责任、线程池排队与超时。

相关内容:广播异步处理的期限。

参考回答

不能。goAsync() 把完成时点延后到 PendingResult.finish(),仍需在相应期限内结束。 忘记调用、异步 I/O 卡住,或者工作在线程池中长期排队,都可能超时。

需要覆盖所有退出分支并尽快完成;长时间可延后任务应交给合适的持久任务机制,而不占用广播处理窗口。

6.7 StrictMode 能证明应用不会发生 ANR 吗?​

考察重点:检测能力与修复证据的边界。

相关内容:预防与修复验证。

参考回答

不能。StrictMode 能暴露配置范围内的部分不当操作,但不覆盖全部锁、计算、调度和 native 问题。 没有日志不等于不存在 ANR 风险。

还需对关键场景做 trace、压力复现和发布后的事件观察,验证实际等待链与任务完成时间。

7. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明