Android ANR 排查指南:思路、方法与实战案例
核心 :解决 ANR 不是「看到堆栈就改代码」,而是先回答三个问题------谁阻塞了主线程 、阻塞了多久 、为什么没及时返回 。
本质:系统要求某个线程/组件在规定时间内响应,但它没有完成。
先建立一条最短排查路径
如果是第一次系统地排查 ANR,不必急着把整份文档从头到尾读完。先把下面 5 步记住,后面的内容再逐段展开:
- 先确认
Reason/ ANR 类型 - 立刻保留现场 :
logcat、traces.txt、bugreport - 先看
main栈顶 10~20 行 - 如果
main在等,就顺着追责任线程或对端 - 责任链确认后,再回到代码修复
一句话先记住:
谁让
main等,谁就是重点。
先统一几个常见术语
初次接触 ANR 时,最容易被术语绊住。下面这张表只追求先建立直觉,不是严格下定义:
| 术语 | 先这样理解 |
|---|---|
| ANR | App 或系统组件在规定时间内没响应,系统把它判成"无响应" |
main 线程 |
UI 主线程;页面展示、点击响应、很多生命周期都依赖它 |
traces.txt |
某一时刻所有线程"正在做什么"的快照 |
Reason: |
系统给你的第一条线索,先用它判断 ANR 大类 |
| Binder | 进程间调用通道;可先理解成"我问另一个进程要结果" |
system_server |
Android 里很多系统服务所在的核心进程 |
| HAL / vendor | 更靠近硬件和厂商实现的一层,常见于相机、音视频等链路 |
held by thread 48 |
这把锁现在在 48 号线程手里 |
Blocked / Waiting / Runnable |
常见线程状态;别只看状态名,要结合栈顶方法一起判断 |
阅读建议
| 区块 | 用途 |
|---|---|
| 第一部分 | 类型速查、一张主流程图、根因脑图;先建立整体判断框架,再回看具体现场 |
| 第二部分 | 按真实排查顺序展开:从确认现场、阅读主线程、追责任线程,到修复与验证,适合在手里有问题时直接对照 |
| 第三部分 | 拿到 traces.txt 后:如何逐段读线程块、判断锁等待 / Binder / 死锁并顺藤摸瓜 |
| 第四部分 | 真实风格案例:从普通 App 场景到相机链路场景,按「怎么看 → 结论 → 修复」完整展开 |
| 附录 | 命令合集、修复优先级表、反面代码示例 |
可以按下面的顺序阅读
- 第一次系统读 ANR:先看先建立一条最短排查路径 → [清单 1~4](#清单 1~4) → [普通 App 场景案例](#普通 App 场景案例)。
- 手里已经有
traces.txt:直接看第三部分 的"固定套路"和第四部分案例。 - 已经有一些排查经验:把第二部分当排查清单,把第三部分和第四部分当作查阅与对照材料。
目录
开始前
第一部分 · 思维与参考
- [ANR 类型速查](#ANR 类型速查)
- [ANR 排查主流程(整合流程图)](#ANR 排查主流程(整合流程图))
- 常见根因(Mindmap)
- 主线程「看起来正常」时
第二部分 · 实战排查清单
- [清单 1 --- 先确认 ANR 基本信息](#清单 1 — 先确认 ANR 基本信息)
- [清单 2 --- 第一优先级:拿现场资料](#清单 2 — 第一优先级:拿现场资料)
- [清单 3 --- 先看主线程,不要先看业务代码](#清单 3 — 先看主线程,不要先看业务代码)
- [清单 4 --- 主线程卡住后,继续追到责任线程](#清单 4 — 主线程卡住后,继续追到责任线程)
- [清单 5 --- 按 ANR 类型继续缩小范围](#清单 5 — 按 ANR 类型继续缩小范围)
- [清单 6 --- 线程与锁专项检查](#清单 6 — 线程与锁专项检查)
- [清单 7 --- Binder / 系统服务专项检查](#清单 7 — Binder / 系统服务专项检查)
- [清单 8 --- 启动场景专项检查](#清单 8 — 启动场景专项检查)
- [清单 9 --- UI 卡顿导致 ANR 的专项检查](#清单 9 — UI 卡顿导致 ANR 的专项检查)
- [清单 10 --- CPU / 内存 / GC / IO 环境检查](#清单 10 — CPU / 内存 / GC / IO 环境检查)
- [清单 11 --- 日志排查](#清单 11 — 日志排查)
- [清单 12 --- Perfetto / Systrace](#清单 12 — Perfetto / Systrace)
- [清单 13 --- 修复方案](#清单 13 — 修复方案)
- [清单 14 --- 验证](#清单 14 — 验证)
- [清单 15 --- 复盘时必须回答的 5 个问题](#清单 15 — 复盘时必须回答的 5 个问题)
- [清单 16 --- 团队 ANR 分析模板](#清单 16 — 团队 ANR 分析模板)
- [清单 17 --- 排查口诀](#清单 17 — 排查口诀)
第三部分 · 逐行阅读 traces.txt
- 总览:目标与阅读顺序
- 线程块:五个关注点
- [第一步:定位进程与 main](#第一步:定位进程与 main)
- 锁等待
- [Object.wait / await / get / join](#Object.wait / await / get / join)
- [Binder 阻塞](#Binder 阻塞)
- 死锁
- 四步读法套路
- [完整示例:锁 / Binder / 死锁](#完整示例:锁 / Binder / 死锁)
- 易误判
- [traces 填写模板与速查表](#traces 填写模板与速查表)
第四部分 · traces 真实案例拆解
- 总览
- [案例一:普通 App 点击后同步查库](#案例一:普通 App 点击后同步查库)
- 案例二:主线程抢锁
- [案例三:
openCamera()Binder 阻塞](#案例三:openCamera() Binder 阻塞) - [案例四:
Future.get()伪异步](#案例四:Future.get() 伪异步) - 四类快速区分
- 固定读法五步走
- 最后用两句话收束
附录
- [附录 A --- 常用命令与工具](#附录 A — 常用命令与工具)
- [附录 B --- 修复手段优先级与反面示例](#附录 B — 修复手段优先级与反面示例)
第一部分 · 思维与参考
1. ANR 类型速查
| 类型 | 含义 |
|---|---|
| Input dispatching timed out | 主线程未及时分发/处理输入;UI 卡死、锁、Binder、重计算等 |
| Broadcast of Intent ... | BroadcastReceiver 执行过久 |
| executing service ... | Service 生命周期或处理过慢 |
| ContentProvider not responding | Provider 初始化或 query/insert 等阻塞 |
| Foreground service did not start in time | 前台服务未在时限内启动完成 |
| Job / 系统调度 | 部分系统场景也会表现为 ANR |
logcat / ANR 信息里先看 Reason:,再对照上表。
2. ANR 排查主流程(整合流程图)
ANR 排查看起来分支很多,但真正常用的路径其实很稳定:先确认类型与现场 → 看 main → 按栈分类 → 对症处理 → 复现验证。下面把这条路径收束成一张图。
锁 BLOCKED wait park
Binder transact
IO DB decode commit
UI inflate measure draw
重 CPU JSON 遍历等
nativePollOnce 看似空闲
发现 ANR
logcat: Reason / 确认 ANR 类型
收集现场 traces、log、bugreport
定位进程 看 main 栈顶
main 属于哪一类
持锁线程 死锁 主线程等后台
对端进程与服务 本进程 Binder 池
异步化 缓存 减同步等待
减层级 首屏分阶段 列表 bind
移后台 拆任务 UI 仅刷新
扩查其他线程 系统日志
对症修复
复现并加日志或抓 trace
验证 ANR 与卡顿是否消除
读图顺序 :从左到右一条线走完------先 Reason 与现场,再 main 分类 ;每一支既对应「要查什么」(如持锁线程、Binder 对端),也对应「常见修什么」(异步、减锁、移主线程等);最后复现 + 验证,避免只消 ANR 不消卡顿。
3. 常见根因(Mindmap)
ANR 根因
主线程重活
大计算
JSON解析
图片解码
初始化过重
锁竞争
synchronized
ReentrantLock
CountDownLatch
Future.get
Binder阻塞
系统服务慢
跨进程调用慢
对端死锁
IO慢
文件读写
SharedPreferences commit
SQLite
fsync
UI过重
inflate过深
measure/layout/draw耗时
RecyclerView绑定过重
线程模型错误
主线程等后台
后台又等主线程
死锁
启动链路过重
Application
ContentProvider
首页onCreate
第三方/系统依赖
SDK阻塞
WebView
Camera/Media服务
4. 主线程「看起来正常」时
有时主线程停在 Looper.loop、nativePollOnce,不代表没问题,可能包括:
- 关键消息迟迟未投递
- 其他线程持锁,主线程后续无法执行
- Binder 对端阻塞
RenderThread/ Binder 线程 / 工作线程异常
继续看 :RenderThread、Binder:*、FinalizerDaemon、业务线程池、DB 线程、相机/GL 线程、HandlerThread 等。
第二部分 · 实战排查清单
这一部分按照日常分析 ANR 时更接近真实的思路来安排:先确认现场,再判断主线程状态,然后继续追到责任线程,最后再回到修复与验证。真正排查时不必机械照背,但大多数问题都可以沿着这条顺序逐步收敛。
清单 1 --- 先确认 ANR 基本信息
建议先确认的信息
- ANR 时间点
- 机型 / Android 版本
- App 版本
- 是否必现 ;发生场景 (可多选对照)
- 冷启动 / 切前后台 / 页面跳转
- 点击拍照、录像 / 切换摄像头
- 设置页打开 / 广播触发 / Service 启动
- 是否只在某一项目、某一平台出现(如 MTK 、Qualcomm 、特定 ROM)
先判断 ANR 类型
看 logcat / ANR 信息里的 Reason:,常见类型见[第一部分 · ANR 类型速查](#第一部分 · ANR 类型速查)。
清单 2 --- 第一优先级:拿现场资料
建议优先收集的现场
- logcat
- traces.txt /
/data/anr/anr_* - bugreport(条件允许)
- 发生前后 10~30 秒日志
- CPU / 内存 / Binder / 磁盘相关信息(能从 bugreport、dumpsys、trace 里尽量捞全)
常用命令(详见[附录 A](#常用命令(详见附录 A)))
bash
adb logcat -d > logcat.txt
adb shell ls /data/anr
adb pull /data/anr/anr_xxx ./anr.txt
adb shell dumpsys activity > activity.txt
adb shell dumpsys input > input.txt
adb shell dumpsys cpuinfo > cpuinfo.txt
如果 user 版本拿不到 /data/anr
至少保住:关键 log 、App 自埋点 、DropBoxManager / bugreport 、复现时的 Perfetto。
已有 traces.txt / bugreport 里的 trace 时,可对照 [第三部分 · 逐行阅读 traces.txt](#第三部分 · 逐行阅读 traces.txt) 做锁 / Binder / 死锁的精读。
清单 3 --- 先看主线程,不要先看业务代码
拿到 traces.txt 之后,最先需要回答的问题其实很简单:ANR 发生的那个瞬间,主线程到底在做什么。判断这件事时,先看 main 线程栈,通常比先翻业务代码更有效。(栈上关键字与顺藤摸瓜的方法见 第三部分。)
A. 锁等待
| 项目 | 内容 |
|---|---|
| 关键词 | BLOCKED、waiting to lock、waiting for monitor entry、Object.wait、LockSupport.park、CountDownLatch.await、FutureTask.get |
| 结论 | 主线程往往不是「自己慢」,而是在等别人 |
| 排查重点 | 找持锁线程 ;找被 await/get 的任务线程 ;查是否死锁 / 环形等待 |
B. Binder 阻塞
| 项目 | 内容 |
|---|---|
| 关键词 | BinderProxy.transact、transactNative、ContentResolver、PackageManager、ActivityManager、CameraService、SurfaceFlinger 等 |
| 结论 | 主线程在做跨进程调用 ,需查对端是否卡住 |
| 排查重点 | 能否移到后台线程 ;查 system_server / cameraserver / Provider 等对端日志 |
C. IO / DB
| 项目 | 内容 |
|---|---|
| 关键词 | read、write、open、fsync、SQLite、SharedPreferences.commit、BitmapFactory.decode 等 |
| 结论 | 主线程做了慢 IO 或大图解码 |
| 排查重点 | 异步化 ;缓存 ;减少同步落盘;commit() → apply() |
D. UI 构建 / 绘制过重
| 项目 | 内容 |
|---|---|
| 关键词 | performTraversals、measure、layout、draw、inflate、RecyclerView、ConstraintLayout 等 |
| 结论 | 页面太重 或刷新过频 |
| 排查重点 | 查布局层级 、首屏初始化 、列表 bind 、自定义 View |
E. 业务重计算
| 项目 | 内容 |
|---|---|
| 关键词 | 大量遍历、JSON 解析、图片处理、排序、配置组装、算法预处理等 |
| 结论 | 主线程做了 CPU 重活 |
| 排查重点 | 后台化 ;分段 ;结果回主线程只更新 UI |
清单 4 --- 主线程卡住后,继续追到责任线程
只看 main 往往只能看到表象,真正让 ANR 成立的原因,常常还藏在持锁线程、后台任务线程,或者 Binder 的对端进程里。在 traces.txt 里如何搜 held by thread、跟 Binder 对端、画等待链,见 [第三部分(§3 锁、§5 Binder、§6 死锁、§7~§8 套路与示例)](#第三部分(§3 锁、§5 Binder、§6 死锁、§7~§8 套路与示例))。
若 main 在等锁
- 查锁对象 ;在 trace 里搜谁持有该锁
- 看持锁线程当时在干什么:是否在 IO 、是否在 Binder 、是否又在等别的锁
若 main 在等异步任务
例如:Future.get()、CountDownLatch.await()、Semaphore.acquire() 等。
- 后台任务线程有没有真正跑起来
- 线程池是否满 、任务是否被串行堵死
- 后台任务是否反过来等主线程
若 main 在 Binder
- 对端是哪个进程;对端是否繁忙
- Binder 线程池 是否耗尽;系统服务是否阻塞
清单 5 --- 按 ANR 类型继续缩小范围
当 main 的状态已经大致判断出来之后,就可以结合 Reason: 再把范围缩小一层。不同类型的 ANR,排查重点并不完全相同。
A. Input dispatching timed out(最常见)
无论业务类型如何,都建议先看这些问题
- 主线程是否在处理点击/触摸时做重活
- 是否在
onCreate/onResume/onStart/onClick里同步做耗时任务 - 是否主线程在等后台结果
- 是否主线程在等相机 / DB / Provider / Binder 返回
- 是否大布局 inflate 、首帧过重
- 是否频繁
notifyDataSetChanged/requestLayout - 是否存在锁竞争
如果是普通 App 场景,还可以继续留意
- 点击后是否直接在主线程查 Room / SQLite
- 是否在主线程读 SharedPreferences、文件、JSON 配置
- 是否在主线程做列表数据排序 / 过滤 / 分组 / 映射
- 是否把网络结果回调后的数据组装全放在主线程
- 是否把首屏"先显示一点"做成了"等所有数据准备好再显示"
如果是相机场景,下面这些链路尤其值得关注
- 切模式是否阻塞 UI 线程
open/closecamera 是否主线程同步等待session configure是否串在主线程GLSurfaceView/ RenderThread / main 是否互等- 拍照前参数整理是否在主线程
- Provider 读配置是否同步卡住
B. BroadcastReceiver ANR
这类问题的共性是:入口很轻,但在 onReceive() 内部把事情做重了。
onReceive()里是否做了耗时逻辑- 是否磁盘 / DB / 网络
- 是否启动流程过重
- 是否同步等待第三方 SDK 初始化
- 是否有 Binder 卡住
- 是否应改为 WorkManager / 后台线程
原则 :onReceive() 只做轻逻辑,快进快出。
C. Service ANR
很多人会天然把 Service 理解成"后台执行",但 Service 的生命周期回调默认仍然跑在主线程。
onCreate/onStartCommand/onBind是否在主线程做重活- 前台服务是否启动不及时
- Service 启动后是否立刻大 IO / 大计算
- 是否同步等待某初始化完成
- 是否误以为 Service 默认异步 (默认仍在主线程)
D. ContentProvider ANR
Provider 的问题往往隐蔽,因为它既可能出现在业务读写阶段,也可能在应用启动前就提前暴露出来。
Provider.onCreate()是否重初始化query/update/insert是否查库太慢- 是否缺索引 、是否大结果集
- 是否被其他进程频繁调用
- Provider 是否在 App 启动前初始化,拖慢启动
清单 6 --- 线程与锁专项检查
如果 main 看起来是在"等",锁和线程模型通常就是最需要优先怀疑的两个方向。
- 主线程是否使用
synchronized(或参与同一把锁) - 锁里是否做了 IO
- 锁里是否做了 Binder
- 锁里是否回调了 UI
- 是否多把锁嵌套、锁顺序不一致
- 后台线程持锁时间是否过长
- 主线程是否
join/get/await - 条件变量是否永远等不到 signal
下面这些写法如果出现在主线程附近,通常需要优先提高警惕
java
synchronized (lock) {
readFile();
remoteCall();
doHeavyWork();
}
// 以及主线程上的:
future.get();
latch.await();
thread.join();
清单 7 --- Binder / 系统服务专项检查
当主线程的等待指向跨进程调用时,排查思路就不能只停留在当前进程内部。
- 主线程是否频繁调用系统服务
- 是否多次小 Binder 串行执行
- 能否批量 取数、是否有缓存未用
- 对端进程 是否卡住;Binder 线程池是否满
- 跨进程 Provider 查询是否过慢
- 某个系统服务本身是否异常
在相机链路里尤其值得重点关注
- camera open / close
- createCaptureSession
- Surface 配置
- provider manager / service manager 查询
- 媒体服务 / cameraserver / vendor 是否异常
清单 8 --- 启动场景专项检查
很多 ANR 都发生在启动阶段,因为这时最容易把初始化、配置加载和首屏渲染堆在同一条时间线上。
Application是否初始化过多- 是否多个 ContentProvider 提前初始化
- 首页首帧前是否大量读设置
- 是否大量 decode Bitmap
- 是否同步加载滤镜 / 贴纸 / 预设 / 配置
- 线程池、日志、DB、网络 SDK 初始化是否过重
- 是否等待某异步初始化完成才继续显示页面
onCreate是否做了重型 Binder
修复方向 :延迟初始化、按需初始化、首屏最小可用 、非关键任务挪到首帧后。
清单 9 --- UI 卡顿导致 ANR 的专项检查
有些 ANR 的根因并不是线程互等,而是 UI 链路本身过重,最终把响应时限拖穿。
- 布局层级是否过深
RecyclerViewitem 是否过重onBindViewHolder是否耗时- 自定义 View 的
onMeasure/onLayout/onDraw是否重算 - 是否频繁创建对象导致 GC 抖动
- 是否过度刷新 UI
- 页面切换是否一次性加载过多模块
清单 10 --- CPU / 内存 / GC / IO 环境检查
ANR 不一定是「逻辑卡死」,也可能是环境拖慢。
- ANR 时 CPU 是否打满
- 系统整体负载是否很高
- 是否频繁 GC、大内存分配
- 磁盘是否写满 / 空间不足
- I/O wait 是否很高
- 低内存回收导致系统抖动
特别注意(相机场景) :存储空间不足、媒体写入异常、日志写爆等,都可能把时序拖垮,最终表现为 ANR。
清单 11 --- 日志排查
日志不能替代 trace,但它往往能帮助你把 trace 里的"发生了什么"补齐成"为什么会这样"。
logcat 重点关键词
ANR in、Reason:、Input dispatching timed out、Broadcast of Intent、executing service、ContentProvider not responding、am_anr、traces、ActivityManager、InputDispatcher、WindowManager、Choreographer、Skipped frames、binder、deadlock
相机项目额外搜
CameraService、cameraserver、configureStreams、openCamera、closeCamera、Session、Surface、ImageReader、EGL、BufferQueue、Frame available
清单 12 --- Perfetto / Systrace
如果能抓到 Perfetto 或 Systrace,很多"看起来像猜"的判断会立刻变得具体。
看什么
- 主线程是长时间没切出去还是一直忙
- 是否被 Binder block 、被锁卡住
- 是否有阶段极长
- 首帧前有哪些大块任务
- camera / render / binder / main 的时间关系
重点线程
main、RenderThread、Binder:*、业务 HandlerThread、GLThread、相机相关线程、线程池 worker。
清单 13 --- 修复方案
定位完成之后,修复不要只停在"改成子线程"这类表面动作,而要回到真正的责任链上。
- 是否真正移出主线程
- 主线程是否还在等待子线程结果
- 是否缩小锁范围
- 是否去掉锁内 IO / Binder
- 是否延迟初始化
- 是否有缓存兜底
- 是否减少同步跨进程调用
- 是否拆分大任务
- 是否加超时 与失败降级
清单 14 --- 验证
修复完成并不意味着问题已经结束,验证的目标是确认"ANR 消失"与"体验恢复"同时成立。
- 原路径是否不再 ANR
- 是否仍有明显卡顿
- 是否引入新时序 问题、状态不一致
- 是否在低端机验证
- 是否在低存储 / 高负载场景验证
- Monkey / 压测 / 长稳是否覆盖
- 是否覆盖切前后台、旋转、锁屏、权限弹窗等边界
清单 15 --- 复盘时必须回答的 5 个问题
一份完整的 ANR 复盘,最好最终都能落到下面 5 个问题上:
- ANR 类型是什么?
- 主线程当时卡在哪里?
- 真正责任线程 / 责任模块是谁?
- 根因是线程模型、锁、Binder、IO、UI 还是启动过重?
- 修复后如何证明问题消失?
清单 16 --- 团队可直接套用的 ANR 分析模板
text
【ANR 类型】
Input dispatching timed out / Broadcast / Service / Provider / Foreground service ...
【发生场景】
例如:进入录像模式后点击开始录像,低存储场景概率复现
【现场信息】
机型:
系统版本:
App 版本:
发生时间:
复现概率:
【主线程堆栈】
main 线程阻塞在:
- ...
- ...
【责任线程分析】
主线程正在等待:
持锁线程 / 对端线程为:
该线程正在执行:
【根因分析】
例如:
1. 主线程在启动录像时同步等待底层初始化结果;
2. 底层线程持有状态锁同时执行耗时 IO;
3. 主线程超过输入响应时限,触发 Input dispatching timed out。
【修复方案】
1. 将 xxx 移至后台线程;
2. 去除主线程同步等待;
3. 缩小锁粒度,避免锁内 IO;
4. 增加失败兜底与超时保护。
【验证结果】
1. 原场景连续复现 xx 次未再出现;
2. Monkey / 压测 xx 小时未出现;
3. 页面响应正常,无额外功能异常。
清单 17 --- 排查口诀
日常脑内快速过一遍:
先看类型,再看 main;main 在等,就找责任线程;main 在跑,就找重活代码;锁、Binder、IO、启动,是四个最高频方向。
与主栈对照时可用(细一点):
ANR 先看 main,main 不行看锁,锁不行看 Binder,Binder 不行看对端,最后回到线程模型。
第三部分 · 逐行阅读 traces.txt:锁等待 / Binder / 死锁
目标 不是「看懂每一行」,而是做到三件事:快速找到 ANR 关键线程 ;判断是锁等待 / Binder 阻塞 / 死锁 ;顺着线程关系把根因找出来 。
阅读原则:不要从上往下机械读完 ;顺序永远是「先进程 → 再 main → 再按栈定性 → 再追责任线程/对端」。
锁等待
Binder 调用
Future/get/await
UI/IO/重计算
nativePollOnce/看似空闲
拿到 traces.txt
先找发生 ANR 的进程
先看 main 线程
main 卡在哪里?
找持锁线程
找对端服务/进程
找被等待的任务线程
定位具体耗时代码
继续看其他关键线程
判断是否锁竞争或死锁
判断是否 Binder 阻塞
判断是否线程模型错误
给出优化方案
结合 logcat 和系统进程 trace 继续定位
1. traces.txt 里「一个线程块」怎么看
典型形态如下(示意,版本间细节可能略有差异):
text
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x7381a000 self=0x7f8b2c0000
| sysTid=12345 nice=-10 cgrp=top-app sched=0/0 handle=0x7fa1bcd4f8
| state=S schedstat=( ... ) utm=10 stm=2 core=6 HZ=100
| stack=0x7fd1234000-0x7fd1236000 stackSize=8192KB
| held mutexes=
native: #00 pc ... /apex/.../libc.so (__epoll_pwait+8)
...
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.Looper.loop(...)
at android.app.ActivityThread.main(...)
建议优先看这 5 类信息:
| 关注点 | 作用 |
|---|---|
| 线程名 | "main"、RenderThread、Binder:...、FinalizerDaemon、业务名如 CameraHandler、pool-3-thread-1 → 判断角色 |
| sysTid | 系统线程号;可与 log、Perfetto、native trace 对齐 |
| state= | 第一眼参考:不要单靠 state 下结论 ,必须与 Java/Native 栈一起看 |
| held mutexes= | 若有内容,说明线程可能持有 native mutex,往往很有价值 |
| 调用栈(Java / Native,尤其栈顶附近) | 核心:在干什么 、是自己干活 还是等别人 |
state= 常见取值(先建立直觉)
| 值 | 大致含义 |
|---|---|
| R | Running,可运行 / 正在跑 |
| S | Sleeping,休眠 / 等待 |
| D | 不可中断睡眠,常与内核等待、IO 相关 |
| B | Blocked |
| W | Waiting |
| T | Stopped |
2. 拿到 traces.txt 后的第一步
第 1 步:先锁定发生 ANR 的进程
文件中常出现多段进程,格式类似:
text
----- pid 12345 at 2026-04-16 10:21:33 -----
Cmd line: com.example.app
确认 pid / Cmd line / 时间点是否为你的 App。若怀疑系统侧,还要继续看 system_server、cameraserver、相关 Provider/Service 进程段。
第 2 步:在该进程内先搜 "main"
第一入口永远是主线程 。看:main 栈顶是什么------属于 wait / transact / sleep / inflate / query / decode 等哪一类场景。
3. 如何判断「锁等待」
锁等待在 ANR 里非常高频。
3.1 典型栈上关键词
waiting to lock、waiting for monitor entry、blocked on、Object.wait(需结合上下文)、LockSupport.park、ReentrantLock.lock、CountDownLatch.await、FutureTask.get、Thread.join 等。
3.2 最关键的一行(Java 监视器锁)
若出现类似:
text
- waiting to lock <0x08a1b2c3> (a java.lang.Object) held by thread 42
含义一目了然:
- 当前线程在等一把锁
- 锁对象
<0x08a1b2c3> - 持锁线程是 thread 42
下一步 :立刻在全文搜 thread 42 或 <0x08a1b2c3>,看持锁线程在干什么。
3.3 持锁线程在干什么的典型结论
若持锁线程栈里出现 - locked <0x08a1b2c3> 且正在 IO / 解析 / 重逻辑 ,而 main 在同锁上 waiting to lock,则根因往往是:后台持锁过久 + 锁粒度过大,而不是「主线程那一行业务代码写得慢」。
4. Object.wait() / await / get / join(同步等异步)
Object.wait()
主线程若在 wait() 上卡住,本质是在等条件/结果/通知 ,不一定是传统「抢锁」问题。要继续看:waitResult() 一类逻辑在等谁 、谁负责 notify/notifyAll、对应线程是否执行到 、是否存在永远不发信号。
CountDownLatch.await() / Future.get() / Thread.join()
可视为高危信号 :几乎都是主线程同步等待后台完成。继续查:
- 后台线程是否真的开始跑
- 线程池是否满、任务是否被串行堵死
- 后台是否反过来等主线程(环路)
- 任务内是否有 IO / Binder / 锁
5. 如何判断「Binder 阻塞」
相机、媒体、系统服务交互多时尤其常见。
5.1 主线程典型栈特征
例如:BinderProxy.transact / transactNative / Binder.execTransact、ContentResolver.query、ActivityManager、PackageManager、CameraManager、SurfaceControl、MediaProvider、SettingsProvider 等。
理解 :主线程发起 Binder ,对端(如 CameraService)未及时返回 → 主线程被卡住。
5.2 继续往下看的思路
锁等待
IO/DB
再调别的 Binder
死锁
main 卡在 BinderProxy.transact
从栈识别接口/服务
判断对端进程是谁
查对端进程 trace / log
对端在干什么?
继续追锁
对端慢操作
链式 Binder 阻塞
含跨进程死锁
5.3 如何从栈里猜对端
| 栈上线索 | 对端倾向 |
|---|---|
ICameraService$Stub$Proxy... |
cameraserver 等相机链路 |
IActivityManager$Stub$Proxy.startService 等 |
system_server |
ContentResolver.query |
Provider 所在进程 或系统 Provider |
5.4 Binder 卡住的常见根因
- 对端自身慢(DB、磁盘 IO、大计算、等锁)
- 对端 Binder 线程池忙满,请求排队无人处理
- 对端又同步调回你(跨进程环路等待)
- 对端死锁/卡死(如 cameraserver、system_server)
结论 :有时 App main 只是受害者 ,真正慢在 cameraserver / HAL / vendor ,需结合对端 trace 与日志,不能只改 UI 线程表面逻辑。
6. 如何判断「死锁」
一句话 :A 等 B,B 又等 A,或形成环路等待,即死锁。
6.1 trace 上的典型形态(双向)
text
"main" ...
at com.xxx.ManagerA.call(ManagerA.java:45)
- locked <0x1000>
- waiting to lock <0x2000> held by thread 21
"Worker-21" ...
at com.xxx.ManagerB.call(ManagerB.java:61)
- locked <0x2000>
- waiting to lock <0x1000> held by thread 1
若 thread 1 即 main:main 持有 <0x1000> 等 <0x2000> ,Worker 持有 <0x2000> 等 <0x1000> → 经典双向死锁。
等待 lock2000
被 Worker 持有
等待 lock1000
被 main 持有
main 持有 lock1000
lock2000
Worker 持有 lock2000
lock1000
6.2 常见模式速记
| 模式 | 说明 |
|---|---|
| 锁顺序不一致 | 线程 A:先 A 锁再 B 锁;线程 B:先 B 锁再 A 锁 |
| 主线程等后台,后台又等主线程 | 如 Future.get() + 后台 runOnUiThread 再同步等 UI |
| 跨进程 | App 主线程同步 Binder 调 system_server,对端又同步 Binder 回调你的进程,关键线程占满 → 互等(需多进程 trace 一起看) |
7. 逐行读的「固定套路」(四步)
- 只看
main栈顶约 10~20 行 ,扫关键词:锁类(waiting to lock...)、Binder 类(BinderProxy.transact...)、IO 类(read/SQLite/commit...)、UI 类(inflate/performTraversals...)。 - 给
main定性:锁等待 / Binder / 同步等异步 / 主线程重活 / 看似空闲(需扩查)。 - 顺着被等待对象追 :
held by thread X、锁地址、Future 所在线程池、Binder 对端进程与对端栈顶。 - 画等待链 (纸笔即可):
main → 等 lock1 → CameraThread 持有 → ...链一清,根因往往就明。
8. 三类压缩示例
示例 A:锁等待(锁内 IO)
- main :
waiting to lock <0x0a1b2c3d> held by thread 39 - thread 39(如 CameraWorker) :
- locked <0x0a1b2c3d>且栈在FileInputStream.read/ConfigParser.parse - 结论 :持锁线程在锁内做 IO + 解析 ,主线程抢同锁 → 锁粒度过大。
- 方向 :锁内只保护共享状态;IO / parse 移出锁;减少主线程参与该锁。
示例 B:Binder 阻塞(openCamera)
- main :
BinderProxy.transact→ICameraService...connectDevice→CameraManager.openCamera→ 业务openRearCamera - 结论 :主线程卡在 Binder 未返回 ;需继续看 cameraserver / HAL 是否
wait、配流卡住等。 - 方向 :App 侧避免主线程高风险同步;底层查 session 串行、锁、配流超时 等。
示例 C:死锁
- main :
locked <0x1000>且waiting to lock <0x2000> held by thread 21 - Worker-21 :
locked <0x2000>且waiting to lock <0x1000> held by thread 1 - 结论 :标准双向死锁。
- 方向:统一锁顺序、减少嵌套锁、主线程少持复杂业务锁、锁内不做回调与慢操作。
9. 容易误判的几点
| 现象 | 说明 |
|---|---|
nativePollOnce + Looper.loop |
常表示在等消息,不等于一定无问题;需结合是否消息未投递、他线程持锁、抓取时刻偏差、对端阻塞等 |
state=Runnable |
不一定真在跑业务;可能仍在 native poll 或内核等待 → 以栈顶方法为准 |
| 主线程栈「不慢」 | 仍可能是:后台持锁导致 main 抢不到、Binder 线程堵了 system_server、Provider 卡别人等 → 责任不一定在 main 栈顶那一行 |
10. 建议每次排 traces 写下来的模板 + 关键词速查
固定六步(可复制)
text
1. ANR 进程
进程名:
pid:
时间:
2. main 线程栈顶
main 卡在:
属于:锁等待 / Binder / await / IO / UI / 计算
3. 被等待对象
锁对象 / Future / Latch / Binder 服务:
4. 责任线程
线程名:
tid:
正在做什么:
5. 是否存在环路等待
main -> X -> Y -> main ?
6. 根因结论
锁粒度过大 / 主线程同步等异步 / Binder 对端慢 / 死锁 / 启动重活 / ...
「看到这些先想什么」速查
| 看到这些 | 先想 |
|---|---|
waiting to lock、held by thread、blocked on、locked <0x...> |
锁 |
Object.wait、CountDownLatch.await、FutureTask.get、Thread.join、Semaphore.acquire、LockSupport.park |
主线程同步等待 |
BinderProxy.transact、transactNative、ContentResolver.query、ICameraService、IActivityManager、IPackageManager |
Binder |
inflate、performTraversals、measure/layout/draw、BitmapFactory.decode、SQLite、SharedPreferences.commit、JSONObject |
主线程重活 |
| A 等 B 的锁,B 又等 A 的锁 | 死锁 |
一句话抓住本质
看 traces.txt,不是看「哪个函数名熟悉」,而是看 「谁在等谁,谁又迟迟不返回」 。
实战版 :先看 main ,判断是在干活 还是在等 ;若在等,就一路顺到最后真正卡住的那个线程/对端。
更多带 log + 双线程/多进程栈 + 归因与修复 的完整拆解,见下方 [第四部分 · 真实案例拆解](#第四部分 · 真实案例拆解)。
第四部分 · Android ANR traces.txt 真实案例拆解
下面几组案例都按同一条路径展开:先给简化但真实风格的 trace → 再解释怎么看 → 然后落到结论与修复思路。顺序上先从普通 App 场景开始,再进入更复杂的工程实战场景。类名与包名仅为教学示意,实际项目请以你方代码与现场栈为准。
案例一:普通 App 点击后同步查库导致 ANR
先从一个最常见、也最容易建立直觉的场景开始:没有锁、没有 Binder、也没有系统进程,就是主线程自己在做不该做的事。
1. ANR 现场(log)
text
ANR in com.example.notes
Reason: Input dispatching timed out
用户点击搜索或进入列表页后出现。
2. traces.txt 片段
main 线程
text
"main" prio=5 tid=1 Runnable
| sysTid=14620 ...
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native method)
at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:1000)
at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:838)
at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62)
at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:153)
at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:140)
at androidx.room.RoomSQLiteQuery.acquire(RoomSQLiteQuery.java:...)
at com.example.notes.data.NoteDao_Impl.search(NoteDao_Impl.java:84)
at com.example.notes.ui.SearchViewModel.load(SearchViewModel.java:52)
at com.example.notes.ui.SearchActivity.onClick(SearchActivity.java:118)
...
at android.app.ActivityThread.main(ActivityThread.java:...)
3. 这段 trace 怎么看
- 栈里直接出现了 SQLite / Room DAO / 业务点击链路。
- 这里没有
waiting to lock,也没有BinderProxy.transact,说明它不像是在等别人。 - 第一反应应该是:主线程自己在查库,而且这次查询耗时已经长到挡住了输入响应。
4. 根因结论
| 结论 | 说明 |
|---|---|
| 直接表述 | 点击后在主线程同步查库,查询慢时直接把 UI 卡住 |
| 本质 | 这是主线程重活,不一定需要追别的线程,先把这条调用链移开 |
5. 修复思路
- 查询放到后台线程或协程里,不要在点击回调里同步拿结果。
- 页面先展示加载中 / 缓存结果,不要"查完再显示"。
- 如果是搜索、筛选、分页,优先检查是否能加索引、缩小结果集。
6. 这一类先怎么判断
如果 main 栈顶已经直接落在 SQLite / Room / 文件 / JSON 解析 / Bitmap 解码 这类方法上,往往先按主线程重活处理,不要先怀疑死锁或系统服务。
案例二:主线程抢锁导致 ANR
最常见的一类 :表面是主线程卡住,往往是后台线程持锁过久,主线程在抢同一把锁。
1. ANR 现场(log)
text
ANR in com.example.camera
Reason: Input dispatching timed out
在这类输入超时场景里,第一反应仍然应该是先看 main。
2. traces.txt 片段
main 线程
text
"main" prio=5 tid=1 Blocked
| sysTid=18642 ...
at com.example.camera.state.CameraStateRepository.getCurrentState(CameraStateRepository.java:142)
- waiting to lock <0x0b7a4c11> (a java.lang.Object) held by thread 48
at com.example.camera.ui.CameraActivity.updateModeUi(CameraActivity.java:820)
at com.example.camera.ui.CameraActivity.onResume(CameraActivity.java:645)
at android.app.Activity.performResume(Activity.java:...)
...
at android.app.ActivityThread.main(ActivityThread.java:...)
thread 48(Preset-Loader)
text
"Preset-Loader" prio=5 tid=48 Runnable
| sysTid=18791 ...
at java.io.FileInputStream.readBytes(Native method)
at java.io.FileInputStream.read(FileInputStream.java:386)
...
at com.example.camera.preset.PresetParser.readJson(PresetParser.java:96)
at com.example.camera.preset.PresetParser.parse(PresetParser.java:61)
at com.example.camera.state.CameraStateRepository.reload(CameraStateRepository.java:118)
- locked <0x0b7a4c11> (a java.lang.Object)
at com.example.camera.preset.PresetDataManager.refreshPreset(PresetDataManager.java:203)
...
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
...
3. 这段 trace 怎么看
main关键句:- waiting to lock <0x0b7a4c11> held by thread 48→ 主线程在等锁,持锁方是 thread 48 ;此时不必先在onResume业务细节上纠结,应立刻跳到 thread 48。- thread 48 :
- locked <0x0b7a4c11>,且栈在 读文件 + JSON 解析 (FileInputStream.read→PresetParser→CameraStateRepository.reload)。
4. 根因结论
| 结论 | 说明 |
|---|---|
| 直接表述 | 不是「主线程那几行代码慢」,而是 后台持锁时间过长,主线程抢锁失败,最终输入超时 ANR |
| 本质 | 常伴随 锁粒度过大 、锁内 IO/解析 、主线程也访问同锁保护的数据 |
5. 等待关系(示意)
等待 lock 0x0b7a4c11
被持有
锁内
main updateModeUi
共享状态锁
Preset-Loader
文件读取
JSON 解析
6. 修复思路
不是 笼统说「改异步」------任务已经在后台线程 跑了;要动的是 锁的范围与职责:
- 缩小锁范围 :锁只保护共享状态 的读写;读文件、解析 JSON 放锁外。
- 主线程少碰重锁 :尽量拿快照 ;后台更新完后原子替换引用。
- 避免锁内 链式
refresh → parse → reload → notify一长串。
7. 伪代码对照
不推荐(锁内 IO + 解析)
java
synchronized (mStateLock) {
String json = readFile();
State state = parse(json);
mCurrentState = state;
}
更合理(先 IO/解析,再短锁写状态)
java
String json = readFile();
State newState = parse(json);
synchronized (mStateLock) {
mCurrentState = newState;
}
案例三:openCamera() Binder 阻塞导致 ANR
相机项目里极常见 :栈顶像是 CameraActivity.openCurrentCamera(),实质常是 openCamera 这条 Binder 链路迟迟未返回;慢点可能在 cameraserver / HAL / vendor 、session close/open 串行等。
1. ANR 现场(log)
text
ANR in com.example.camera
Reason: Input dispatching timed out
典型场景:打开相机 或切前后摄时发生。
2. traces.txt 片段(App main)
text
"main" prio=5 tid=1 Native
| sysTid=21456 ...
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(BinderProxy.java:584)
at android.hardware.ICameraService$Stub$Proxy.connectDevice(ICameraService.java:1221)
at android.hardware.camera2.CameraManager.openCameraDeviceUserAsync(CameraManager.java:712)
at android.hardware.camera2.CameraManager.openCameraForUid(CameraManager.java:933)
at android.hardware.camera2.CameraManager.openCamera(CameraManager.java:789)
at com.example.camera.device.CameraDeviceController.open(CameraDeviceController.java:264)
at com.example.camera.mode.BaseCameraMode.openCamera(BaseCameraMode.java:531)
at com.example.camera.ui.CameraActivity.openCurrentCamera(CameraActivity.java:1186)
at com.example.camera.ui.CameraActivity.onResume(CameraActivity.java:708)
...
at android.app.ActivityThread.main(ActivityThread.java:...)
3. 怎么读
看到 BinderProxy.transact + ICameraService...connectDevice + CameraManager.openCamera → 定性为 Binder 阻塞 :主线程调 CameraService ,对端未及时返回,主线程被挂住 → 输入 ANR。
4. 只看 App trace 够不够?
不够。 App 侧只能说明卡在 openCamera,不能单独说明为什么慢。需联动:
- cameraserver 与相关 Binder 线程栈
- system log / vendor camera log
- 若有 system_server trace 一并看
5. 对端 trace 的典型样子(示意)
text
"Binder:camera_service" prio=5 tid=32 Native
| sysTid=1423 ...
at __futex_wait_ex(Native method)
...
at android::camera3::Camera3Device::configureStreamsLocked(...)
at android::Camera3Device::initializeCommonLocked(...)
at android::CameraService::connectHelper(...)
或 vendor log 表现为:配流卡住 、上一 session close 未结束、等 buffer/stream configure 等。
6. 根因结论
| 层级 | 结论 |
|---|---|
| App trace 能确定 | 主线程同步 openCamera(),被 Binder 挂住 |
| 需对端/日志才能定 | 配流慢 、close/open 串行阻塞 、HAL 锁竞争 、vendor pipeline 初始化慢 、底层死等条件 等 |
7. 等待关系(示意)
Binder openCamera
等 configure / close done
未及时返回
Binder 未返回
App main
cameraserver
HAL / Vendor
8. 修复思路
| 层面 | 方向 |
|---|---|
| App | 尽量避免在 onResume 等路径上主线程同步 发起整条 open 链路;主线程只触发流程 ,少承担可阻塞的重链路 ;注意 mode/camera 切换重入 ,避免 close/open 交叠 |
| 底层链路 | 查 close → open → configure 是否串行堵死;cameraserver/HAL 锁与回调;buffer/surface 是否未及时就绪 |
9. 常见误区
看到栈里有 CameraActivity.openCurrentCamera() 就说「Activity 代码慢」不准确 。更贴切的说法是:主线程阻塞在 CameraService Binder 返回;真正慢点常在服务端或底层链路 ------归因时要写清楚,避免误伤 UI 一行代码。
案例四:Future.get() 主线程等后台导致 ANR
这一类问题常被误以为"已经异步化了",但只要主线程立刻 get(),本质上仍然是同步阻塞。
1. ANR 现场(log)
text
ANR in com.example.camera
Reason: Input dispatching timed out
场景:进页面、切模式 、点击等偶发。
2. traces.txt 片段
main
text
"main" prio=5 tid=1 Waiting
| sysTid=23651 ...
at jdk.internal.misc.Unsafe.park(Native method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:211)
at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:447)
at java.util.concurrent.FutureTask.get(FutureTask.java:190)
at com.example.camera.config.RemoteConfigCenter.getConfigSync(RemoteConfigCenter.java:153)
at com.example.camera.mode.VideoMode.initFeatureFlag(VideoMode.java:218)
at com.example.camera.mode.VideoMode.onActivate(VideoMode.java:171)
at com.example.camera.ui.CameraActivity.switchMode(CameraActivity.java:1320)
at com.example.camera.ui.CameraActivity.onClick(CameraActivity.java:1266)
...
at android.app.ActivityThread.main(ActivityThread.java:...)
pool-3-thread-1(示意)
text
"pool-3-thread-1" prio=5 tid=51 Runnable
| sysTid=23740 ...
at android.database.sqlite.SQLiteConnection.nativeExecuteForCursorWindow(Native method)
...
at com.example.camera.config.RemoteConfigDao.queryAll(RemoteConfigDao.java:88)
at com.example.camera.config.RemoteConfigCenter.lambda$getConfigSync$2(RemoteConfigCenter.java:146)
...
at java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
...
3. 第一眼定性
Unsafe.park + FutureTask.awaitDone / FutureTask.get → 主线程在同步等后台任务结束 。不一定是锁或 Binder,但同样会卡死主线程。
4. 关系说明
用户 切模式 → switchMode → getConfigSync() 内部虽用线程池,但 main 上 Future.get() → 工作线程在跑 SQLite (queryAll)→ 查询慢或阻塞 → get() 一直等 → ANR。
5. 等待关系(示意)
Future.get 等结果
SQLite
慢
未返回
main switchMode
线程池任务
数据库
6. 根因结论
伪异步 :任务在后台跑,但主线程立刻同步等结果 ,本质仍是主线程阻塞。
7. 修复方向
错误模式
java
Future<Config> future = executor.submit(() -> loadConfig());
Config config = future.get(); // 主线程阻塞
applyConfig(config);
正确方向 :真正异步(完成后再 回调/切主线程更新 UI );先读缓存再异步刷新 ;避免在点击、切模式、onResume 等链路里硬等远程/本地配置。
示意
java
Config cache = mConfigCache;
if (cache != null) {
applyConfig(cache);
}
executor.execute(() -> {
Config fresh = loadConfig();
mConfigCache = fresh;
mainHandler.post(() -> applyConfigIfNeeded(fresh));
});
四类快速区分(读 trace 时第一反应)
| 类型 | 栈上常见痕迹 | 第一反应 |
|---|---|---|
| 主线程重活 | SQLite、Room、BitmapFactory.decode、JSONObject、read、performTraversals |
先看能否直接移出主线程 |
| 锁等待 | waiting to lock、held by thread、locked <0x...> |
找持锁线程在干什么 |
| Binder 阻塞 | BinderProxy.transact、ICameraService、IActivityManager、ContentResolver.query |
找对端进程/服务与对端栈 |
| 主线程等后台 | FutureTask.get、CountDownLatch.await、Thread.join、Object.wait、Semaphore.acquire |
找被等待的任务线程及栈顶慢点 |
真实排查时的固定读法(五步走)
- 只看
main先问两句 :是在自己干活 ,还是在等别人? - 给
main贴一个优先标签:锁等待 / Binder / await·get·join / IO·DB / UI / 重计算。 - 顺藤摸瓜:锁 → 持锁线程;Binder → 对端进程;get/await/join → 后台任务线程。
- 画等待链 (纸笔即可),例如:
main → 等 lockA → Worker 持锁内读文件,或main → Binder → cameraserver → HAL。 - 再回代码定修复点 :先把责任链走通 ,避免一上来只盯
main栈顶业务函数名。
最后用两句话收束
main 卡锁,看持锁线程;main 卡 Binder,看对端服务;main 卡 get/await,看后台任务。
再压缩一句:
谁让 main 等,谁就是重点。
附录 A --- 常用命令与工具
| 类别 | 命令 / 工具 |
|---|---|
| 日志 | adb logcat -d > logcat.txt |
| ANR 文件 | adb shell ls /data/anr → adb pull /data/anr/anr_xxx ./anr.txt |
| Activity / 输入 / CPU | adb shell dumpsys activity > activity.txt、dumpsys input > input.txt、dumpsys cpuinfo > cpuinfo.txt |
| 综合现场 | bugreport(条件允许必拉) |
| 调度与耗时 | Systrace / Perfetto |
重点看什么:主线程栈、Binder 链、锁等待关系、CPU 是否打满、磁盘 IO、首帧与页面切换时序。
附录 B --- 修复手段优先级与反面示例
| 优先级 | 手段 | 要点 |
|---|---|---|
| 1 | 耗时任务移出主线程 | IO、DB、网络、解码、JSON、大对象、相机参数、算法预处理等;主线程不能再同步等结果 |
| 2 | 锁粒度与用法 | 主线程少参与锁;锁内不做 IO/Binder/UI 回调/复杂逻辑 |
| 3 | 启动优化 | Application 少堆初始化;延迟与懒加载;Provider 轻量;避免主线程同步等 SDK |
| 4 | Binder | 可能慢的调用不放主线程;批量代替频繁小调用;能缓存先缓存 |
| 5 | UI | 布局扁平;首屏分阶段;RecyclerView 用 payload;减过度绘制与频繁 requestLayout |
反面示例:「移线程」却仍阻塞主线程
java
Future<Result> future = executor.submit(task);
Result result = future.get(); // 主线程仍被阻塞
java
synchronized (lock) {
readFile();
queryDb();
remoteService.call();
}
一句话总结
解决 ANR 不是「稍微优化性能」,而是保证关键线程在时限内不被阻塞。
- 主线程只做必须做的事;不等不确定耗时的结果
- 锁里不做慢操作;跨进程调用默认可能慢;初始化默认可能过重
- 先看线程关系,再抠优化细节