前言摘要
本文面向 Android 客户端工程实践,按"速查→定位→交叉验证→归因与修复"的顺序梳理 ANR 排查方法。全文配套可执行步骤、日志示例与术语释义,以 Reason 与主线程 Busy/Waiting 判别为主线,并结合系统信号进行交叉验证,帮助在有限时间内给出清晰结论与化治理方案。
速查清单
- 确认ANR : 在日志中搜索
ActivityManager: ANR in
、am_anr
、Reason:
。 - 找原因(Reason) : 从
WindowManager
/InputDispatcher
找到Input event dispatching timed out
/FocusEvent
等关键词与超时时间(常见 5s/10s)。 - 锁定受害者/施害者 : 明确是谁"server is not responding",是哪一个
Activity
/进程。 - 看主线程在做什么 : 捞取堆栈文件
/data/anr/anr_*.txt
,定位"main"
线程顶部 10~30 行,识别是否重计算/IO/锁等待。 - 交叉验证 : 是否有
Choreographer Skipped frames
、OpenGLRenderer Davey
、CPU usage
升高、SurfaceFlinger
弹出 ANR 层。 - 初步归因与止血: 若为主线程重任务/同步 IO,先降级/关闭路径或临时异步化,阻断复现;随后进入深度分析。
深入分析路径
第 1 步:确认 ANR 与类型
ANR 分类与触发条件
- 输入分发 ANR(Input dispatching timed out,常见 5s)
- 触发:目标窗口未在时限内处理输入/焦点/按键事件,日志含
server is not responding
、Waited 5000ms for FocusEvent/KeyEvent
。 - 常见原因:主线程重计算/同步 IO/锁竞争/跨进程阻塞,导致消息队列无法及时取出事件。
- 触发:目标窗口未在时限内处理输入/焦点/按键事件,日志含
- BroadcastReceiver 超时(前台 10s / 后台 60s)
- 触发:
onReceive
未在时限内返回,日志常见Broadcast of Intent ... timeout
、BroadcastQueue
相关栈。 - 常见原因:在
onReceive
内做网络/磁盘/解码等耗时,或间接等待锁/跨进程调用。
- 触发:
- Service 执行超时(前台 20s / 后台 200s)
- 触发:
Service
执行过久或onStartCommand/onBind
等未及时处理完成,日志常见executing service
/Service timeout for ...
。 - 常见原因:主线程执行长任务、死循环/锁等待、长 Binder 调用、未切到后台线程。
- 触发:
- ContentProvider 发布/调用超时(10s)
- 触发:
ContentProvider
在onCreate
发布或在query/insert
中长时间阻塞,日志常见ContentProvider not responding
/Timeout executing ContentProvider
。 - 常见原因:Provider 中进行磁盘/网络重操作或初始化复杂组件放在主线程。
- 触发:
第 2 步:从 Reason 锁定"谁卡了谁"
- 关注
WindowManager
/InputDispatcher
的超时日志,通常包含:- 目标窗口/组件(如
.../TargetActivity
) - 角色(
server is not responding
通常指应用端未响应) - 事件与等待时长(如
Waited 5003ms for FocusEvent(hasFocus=false)
)
- 目标窗口/组件(如
plain
09-02 12:03:36.411705 1412 1514 I WindowManager: Input event dispatching timed out sending to com.abc.android.xyz.launcher/com.abc.android.xyz.app.TargetActivity. Reason: ff485e3 com.abc.android.xyz.launcher/com.abc.android.xyz.app.TargetActivity (server) is not responding. Waited 5003ms for FocusEvent(hasFocus=false)
09-02 12:03:36.507450 1412 1514 D WindowManager: notifyANR took 96ms
09-02 12:03:36.508364 1412 5205 I am_anr : [0,2739,com.abc.android.xyz.launcher,685293253,Input dispatching timed out (... Waited 5003ms for FocusEvent(hasFocus=false))]
第 3 步:堆栈看主线程
- 先定位
"main"
线程,然后按两大类判断: - A) 主线程在忙(Busy)
- 判据:
state=R
或长时间Runnable
;目标进程user%
明显偏高- 栈包含密集计算/编解码/图片处理/复杂布局与绘制/字符串或 JSON 重解析
- 主线程出现同步磁盘/网络/数据库调用(虽属 I/O,但"在忙"的常见诱因)
- 典型信号:
at com.abc.android.xyz...encode/decode/...
、ViewRootImpl.doTraversal
、Choreographer Skipped frames
、OpenGLRenderer Davey
- 处理方向:
- 重任务移出主线程(
Dispatchers.Default
/Dispatchers.IO
、线程池、WorkManager
) - 降采样/缓存/批处理;分片+让步(处理 N 个单元后
post/yield
) - 主线程启用
StrictMode
禁同步 I/O;渲染路径减负(减少无效invalidate
、控制位图尺寸/上传)
- 重任务移出主线程(
- 判据:
- B) 主线程在等(Waiting)
- 总判据:
state=Blocked/Waiting/D
,或栈顶出现等待/系统调用/跨进程桥接 - 常见子类(表现 / 处理方向):
- 同步等待/锁竞争:
Object.wait
、ReentrantLock.lock/await
、CountDownLatch.await
、Future.get
、Thread.join
- 处理:UI 线程禁同步等待;缩小临界区与锁粒度;加超时/降级,改回调/异步
- 跨进程(Binder)阻塞:
BinderProxy.transact
,调用ContentResolver/PackageManager/WindowManager/Media
等;对端(如system_server
)负载高- 处理:慢 Binder 后台化并设超时/重试;结果缓存与幂等;拆小 payload、降频
- 同步 I/O 阻塞:
read/write/fsync/epoll
、SQLiteDatabase.query
、OkHttp.execute
;系统侧iowait
偏高或state=D
- 处理:统一 IO 线程池/异步;合并/批量写;内存/磁盘缓存;主线程启用
StrictMode
- 处理:统一 IO 线程池/异步;合并/批量写;内存/磁盘缓存;主线程启用
- 渲染/图形等待:
ThreadedRenderer.nSyncAndDrawFrame
、eglSwapBuffers
、Surface/BufferQueue
、sync-fence;伴随丢帧/Davey
- 处理:降低每帧工作量;控制位图尺寸与上传;合并绘制批次
- GC/堆锁等待:日志含 "Waiting for GC to complete",或类表锁;GC 直方图偏高
- 处理:降分配/对象复用;避免主线程创建大对象与巨图
- 窗口/输入管线同步等待:
Input event dispatching timed out
等待FocusEvent/KeyEvent
;ViewRootImpl
布局/relayoutWindow
同步点停滞- 处理:切页/焦点变更路径移除重任务与阻塞;初始化与 I/O 外移;分片让步
- 死锁/循环依赖:多线程/跨进程互等(互持锁、互相
join/await/transact
)- 处理:统一锁顺序、拆锁/顺序化桥接;跨进程调用加超时;必要时重构打环
- 同步等待/锁竞争:
- 总判据:
plain
"main" prio=5 tid=1 Native
native: #00 ... libandroidndkgif.so ... SimpleGCTGifEncoder::reduceColor
native: #01 ... libandroidndkgif.so ... SimpleGCTGifEncoder::encodeFrame
native: #02 ... libandroidndkgif.so ... GifEncoder_nativeEncodeFrame
at com.waynejo.androidndkgif.GifEncoder.nativeEncodeFrame(Native method)
at com.waynejo.androidndkgif.GifEncoder.encodeFrame(SourceFile:99)
at com.abc.android.xyz.utils.BitmapUtilsKt.saveGIF(SourceFile:423)
at com.abc.android.xyz.utils.BitmapUtilsKt.saveToFile(SourceFile:339)
... at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7705)
- 初步结论:主线程正在进行 GIF 编码与图片写入(计算+IO),致使输入/焦点事件无法在 5 秒内被处理,可以初步定位为(Busy)状态。
第 4 步:交叉验证
- 目标:验证(Busy)初步结论:主线程因计算/渲染链路过重而无法在时限内响应
- 观察项与口径:
- Choreographer:
Skipped 297 frames
→ 主线程卡顿显著,符合重计算/主线程阻塞特征 - InputDispatcher:
... spent 7387ms processing FocusEvent
→ 输入管线等待被动方在应用侧(目标窗口未及时处理),而非输入系统自身故障 - OpenGLRenderer:
Davey! duration≈7447ms
→ 单帧渲染(含测量/布局/绘制/提交)整体拉长,常与主线程业务/绘制过重一致 - ActivityManager CPU usage:进程
≈113%
,构成≈101% user + 12% kernel
,且iowait
低 → 以用户态计算为主,佐证"在忙"而非主要等 I/O - SurfaceFlinger:
createLayer Application Not Responding
→ ANR 弹窗创建时间与上述信号对齐,可作为时间锚点
- Choreographer:
plain
I Choreographer: Skipped 297 frames! The application may be doing too much work on its main thread.
I InputDispatcher: ... spent 7387ms processing FocusEvent(hasFocus=false)
I OpenGLRenderer: Davey! duration=7447ms; ...
plain
E ActivityManager: ANR in com.abc.android.xyz.TargetActivity
E ActivityManager: PID: 2739
E ActivityManager: CPU usage from ...
E ActivityManager: 113% 2739/com.abc.android.xyz.launcher: 101% user + 12% kernel
plain
D SurfaceFlinger: createLayer: name=Application Not Responding: com.abc.android.xyz.launcher
第 5 步:归因与修复
- 主线程在干重活
- 判断:栈现 encode/decode/measure/layout/draw 等重计算;进程 user% 高;伴随丢帧/Davey
- 动作:重任务出主线程(
Dispatchers.Default
/Dispatchers.IO
、WorkManager
);降采样/缓存;分片让步
- 同步 IO 阻塞(文件/网络/数据库)
- 判断:栈现
read/write/fsync/epoll
、SQLiteDatabase
、OkHttp.execute
;线程state=D
或系统iowait
偏高 - 动作:全量改异步/IO 线程池;合并/批量写;启用
StrictMode
禁主线程 IO;必要时降级
- 判断:栈现
- 锁竞争/死锁
- 判断:
Blocked/Waiting
+wait/await/join/get
;多线程互等或锁顺序不一致 - 动作:缩小临界区、拆锁/顺序化;回调/异步替代等待;所有等待加超时;必要时重构打环
- 判断:
- 跨进程阻塞(Binder)
- 判断:栈顶
BinderProxy.transact
;对端system_server/他进程
负载或锁高 - 动作:慢 Binder 后台化+超时/重试;结果缓存与幂等;拆小 payload、降频
- 判断:栈顶
- 渲染管线瓶颈
- 判断:
ThreadedRenderer.nSyncAndDrawFrame
、eglSwapBuffers
、BufferQueue
;大量丢帧/Davey - 动作:降低每帧工作量(布局/绘制/动画);控制位图尺寸与上传;合并绘制批次
- 判断:
扩展阅读
堆栈详解
进程头(Header)
plain
----- pid 2739 at 2025-09-02 12:03:36 -----
Cmd line: com.abc.android.xyz.launcher
Build fingerprint: 'device/product:11/XYZ/20250817:user/test-keys'
ABI: 'arm'
Build type: optimized
Libraries: ... libandroidndkgif.so ... libmmkv.so ... (省略)
Heap: 28% free, 60MB/84MB; 508568 objects
- pid : 进程 ID。用于与
ActivityManager: PID:
、CPU usage
等对齐。 - Cmd line : 进程名/包名。例如:
com.abc.android.xyz.launcher
。 - Build fingerprint: 设备/系统构建指纹,定位系统版本、ROM 差异。
- ABI : 架构,如
arm/arm64
,决定符号/库位数。 - Build type :
user/userdebug/eng
,影响日志与调试能力。 - Zygote loaded classes / Classes initialized: ART 类加载与初始化统计,通常用于诊断冷启动问题。
- Intern table / JNI: 运行时内部状态,只在极端内存/JNI 泄漏分析时参考。
- Libraries : 已加载本/三方
.so
列表。用于判断是否命中某个 native 组件。示例:
plain
Libraries: ... base.apk!/lib/armeabi-v7a/libandroidndkgif.so ... libmmkv.so ... libwebviewchromium.so ...
- Heap :
28% free, 60MB/84MB; 508568 objects
,表示堆使用率、对象数,可用于判断是否因 GC 频繁导致抖动。 - GC timings: GC 各阶段耗时直方图,定位 GC 压力。
线程头(Thread Header)
plain
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7217f360 self=0xe5800e10
| sysTid=2739 nice=-10 cgrp=default sched=0/0 handle=0xee056470
| state=R schedstat=( 184997268070 19701663585 123413 ) utm=14650 stm=3849 core=4 HZ=100
| stack=0xff639000-0xff63b000 stackSize=8192KB
| held mutexes=
- name : 线程名,如
"main"
。 - prio: Java 层线程优先级(数值越小优先级越高)。
- tid : ART 线程 ID(与 Linux
sysTid
不同)。 - Native/Waiting/Runnable: 标识当前在 native/Java 或等待/可运行等。
- group: 线程组名。
- sCount/dsCount: suspend 计数/调试 suspend 计数。
- flags: 线程标志位(内部调度用途)。
- obj/self: Java 对象地址/ART 线程对象地址(调试符号分析用)。
- sysTid: Linux 线程 ID(/proc 可查询)。
- nice: 调度 nice 值(-20 至 19,值越小优先级越高)。
- cgrp: 所属 cgroup(调度/资源控制组)。
- sched : 调度策略/参数(如
0/0
代表 CFS 默认)。 - handle: 线程句柄地址。
- state : 线程状态:
R
(Running)、S
(Sleeping)、D
(Uninterruptible IO)、T
(Stopped)、Z
(Zombie)。 - schedstat :
(sum_exec_runtime run_delay? switches)
的内核统计,单位纳秒/次数,用于大致估算调度与运行时间。 - utm/stm : 用户态/内核态 CPU 时间(以
HZ
为时间基,如HZ=100
即 1tick=10ms)。 - core: 最近运行的 CPU 核心 ID。
- stack/stackSize: 栈地址范围与大小,用于识别栈溢出风险。
- held mutexes: 当前持有的互斥锁列表(为空表示无持有)。
堆栈帧(Frames)
plain
native: #00 pc 00011476 .../base.apk!libandroidndkgif.so (... SimpleGCTGifEncoder::reduceColor+254)
...
at com.waynejo.androidndkgif.GifEncoder.nativeEncodeFrame(Native method)
at com.abc.android.xyz.utils.BitmapUtilsKt.saveGIF(SourceFile:423)
- native:: 本地帧,包含指令地址(pc)、库名、符号与偏移。可用于符号化与火焰图。
- at com...nativeEncodeFrame(Native method): Java → Native 桥接调用点。
- at com...saveGIF(SourceFile:423): 业务 Java/Kotlin 方法栈,直观定位业务代码位置(含 SourceFile:行号)。
Waiting Channels(等待通道)
plain
----- Waiting Channels: pid 2559 at 2025-09-02 12:03:38 -----
Cmd line: com.abc.devicestat
sysTid=2559 SyS_epoll_wait
sysTid=2578 do_sigtimedwait
sysTid=2579 futex_wait_queue_me
...
sysTid=2591 binder_ioctl_write_read
- 字段说明:
- pid :本段快照对应的进程 ID;需先确认它是否为目标进程或关键对端(如
system_server
)。 - Cmd line :该进程的进程名/包名(示例为
com.abc.devicestat
)。 - sysTid=XXXX:该进程内每个线程的系统 TID 与其当前"等待通道/系统调用"。
- pid :本段快照对应的进程 ID;需先确认它是否为目标进程或关键对端(如
- 常见等待通道解读:
SyS_epoll_wait
:事件循环的空闲等待,通常表示线程在等事件/IO,常见于非问题线程。do_sigtimedwait
:Signal Catcher 等待信号,多见于框架线程,通常可忽略。futex_wait_queue_me
:futex 锁等待,可能提示锁竞争/死锁,需要结合持锁方线程栈进一步分析。binder_ioctl_write_read
:Binder 线程在收发/等待对端,若主线程在transact
,应联动查看对端进程(如system_server
)的 Binder 线程与服务栈。- 其他:如
read/write/fsync/__io_schedule
(I/O 等)、poll_schedule_timeout
(超时轮询)等,结合上下文判断。
- 使用建议:
- 优先查看"目标进程"和
system_server
的 Waiting Channels;其他进程仅在它作为关键对端时值得深挖。 - 将等待通道与线程头的
state
(如D/S/Blocked/Waiting
)和CPU usage
的iowait
/kernel%
一起对照,判断是 I/O 等待、锁等待还是跨进程阻塞。
- 优先查看"目标进程"和
用户态 vs 内核态:含义与在 ANR 排查中的价值
- 是什么
- 用户态(user space):应用与大多数 Java/Kotlin/业务 native 代码运行的空间。系统调用需"陷入"内核。
- 内核态(kernel space):内核与驱动、VFS、Binder 驱动、网络栈、调度器等执行空间。
- 在哪儿看
- 日志:
ActivityManager: CPU usage
行:如101% user + 12% kernel + 0.4% iowait + 0.6% irq + 0.3% softirq
。 - 堆栈:线程头的
utm/stm
:分别是该线程的用户态/内核态累计 CPU 时间;state=D
常伴随 I/O 等待。
- 日志:
- 怎么解读
- user% 高 :多为应用计算密集(编解码、图片处理、复杂布局/绘制、JSON 解析、加解密、热循环)。
- 方向:移出主线程、降采样/缓存、算法/结构优化、采样/Trace 找热点。
- kernel% 高 :频繁系统调用或内核繁忙(频繁小块
read/write/fsync
、Binder 调用密集/大 payload、ioctl
图形/相机、锁争用落到内核)。- 方向:合并/批量化 I/O、减少系统调用次数、优化 Binder 交互、检查对端系统服务/驱动路径。
- iowait 高 :在等块设备 I/O;常见于同步文件访问、大量缺页/磁盘抖动。
- 方向:异步 I/O、内存/磁盘缓存、预加载,禁止主线程磁盘访问。
- irq/softirq 高 :中断负载(网络/存储/显示)偏高,系统层面拥塞。
- 方向:关注全局负载,避开热点时段,降低频繁中断源带来的抖动。
- user% 高 :多为应用计算密集(编解码、图片处理、复杂布局/绘制、JSON 解析、加解密、热循环)。
- 与 ANR 的关系
- 输入分发 ANR :目标进程
user%
高 + 主线程重计算栈,或iowait
偏高 + 主线程卡在read/write/ioctl/epoll
。 - 跨进程阻塞 :应用
user%
不高,但system_server
或对端进程user/kernel%
升高,主线程在transact
;应联合对端堆栈定位。
- 输入分发 ANR :目标进程
- 小示例(解读口径)
113% 目标进程: 101% user + 12% kernel
→ 以应用计算为主,少量内核开销;若主线程在图片/GIF 编码,优先迁移后台并降采样/分片,让出主线程。