目录
ANR类型
ANR成因
输入型ANR
大致触发条件
埋雷 & 拆雷
抛砖引玉
引出源码
总结与巩固
规避ANR
ANR预防
耗时预防
UI预防
锁预防
StrictMode
线程策略 (ThreadPolicy)
磁盘读写
网络访问
自定义耗时
虚拟机策略 (VmPolicy)
使用
触发
ANR监控
Watchdog
SIGQUIT
Looper耗时足迹
ANR提取 & 分析
文件提取
文件检查
bugreport (推荐)
dropbox
文件展示
文件分析
场景模拟
futex_wait_queue
binder_thread_read
0
ANR三方监控方案
Crashlytics
Matrix
面试题
ANR类型
- InputDispatching(输入型ANR): 触摸事件或按键事件分发超时 -> 5s
- BroadcastTimeout -> 10s
- ServiceTimout -> 20s
- ContentProvider -> 10s
这里主要分析输入型ANR
ANR成因
输入型ANR
大致触发条件
很多人认为主线程卡住5s就会弹出ANR, 其实不是, 他是有触发条件的:
埋雷 & 拆雷
InputDispatcher 维护着一个 ANR 计时器, 埋雷拆雷针对的就是这个计时器
- 分发事件 & 埋雷
- 事件从 InputDispatcher 通过 InputChannel 发送给 App 进程时, InputDispatcher 会记录
- 事件发给了哪个窗口
- 分发的时间戳
- 计算过期时间: currentTime + 5s
- 事件从 InputDispatcher 通过 InputChannel 发送给 App 进程时, InputDispatcher 会记录
- 点击反馈 & 拆雷
- 正常情况: App 处理完事件后, InputChannel 会回传一个 finish 信号, InputDispatcher 收到信号后, 将事件从 WaitQueue 中移除, 计时器重置
- 反馈: App(finishInputEvent) -> InputChannel(Client) -> InputChannel(Server) -> InputDispatcher -> finish(从等待队列移除事件 && 计时器重置)
- 异常情况: 首先说明, InputDispatcher 是等焦点窗口的新事件进来再判断触发, 细节如下
- InputDispatch 每当新事件进来, 会执行下面代码流程
- findFocusedWindowTargetsLocked: 找到焦点窗口 (新Key事件进来才会触发), 对于Motion事件而言, 调用的是findTouchedWindowTargetsLocked, 但最后殊途同归, 都会执行 checkWindowReadyForMoreInputLocked 检查窗口是否准备好
- checkWindowReadyForMoreInputLocked: 判断焦点窗口是否已准备好接收新事件
- 未准备好
- handleTargetsNotReadyLocked: 判断 WaitQueue 队列头部事件分发时间是否超时5s
- 否: 等待5s后回头看看是否头部事件已超过5s
- 是: onANRLocked: 通知 IMS ANR了
- handleTargetsNotReadyLocked: 判断 WaitQueue 队列头部事件分发时间是否超时5s
- 已准备好
- 正常分发
- 未准备好
- checkWindowReadyForMoreInputLocked: 判断焦点窗口是否已准备好接收新事件
- findFocusedWindowTargetsLocked: 找到焦点窗口 (新Key事件进来才会触发), 对于Motion事件而言, 调用的是findTouchedWindowTargetsLocked, 但最后殊途同归, 都会执行 checkWindowReadyForMoreInputLocked 检查窗口是否准备好
- InputDispatch 每当新事件进来, 会执行下面代码流程
- 正常情况: App 处理完事件后, InputChannel 会回传一个 finish 信号, InputDispatcher 收到信号后, 将事件从 WaitQueue 中移除, 计时器重置
好了, ANR的大概路子已经梳理完毕
抛砖引玉
啥? 没看到Timer 计时器? 那ANR 是究竟是怎么个计时法?
另外, 为什么有时候手机卡了十几秒,但没报 ANR? 当你再点一下屏幕,ANR 弹窗才出来? 计时器不是定时触发的吗?
❓❓❓❓❓❓❓❓❓❓❓
❓❓❓ 😵💫 😵💫 😵💫 ❓❓❓
❓❓❓❓❓❓❓❓❓❓❓
接下来, 我们从里到外把ANR的触发流程扒干净, 但再次之前先让大家先有个大概认知:
- 为什么卡住但不报ANR?
- 当前面事件发出后, 因为某些原因卡住了
- InputDispatcher会把事件丢进 WaitQueue, 等待 finish 指令清空
- 如果此次你不再移动手指, mInboundQueue 为空,dispatchOnce 提取不到新事件,也就不会进入 dispatchOnceInnerLocked。
- InputDispatcherThread 此时处于静默等待状态, 不执行寻址、判断WaitQueue头部事件超时等操作,所以哪怕卡了 10 秒,只要没有新动作,ANR 就不会被“引爆”。
- 当前面事件发出后, 因为某些原因卡住了
- 为什么 “再点一下, 就会爆”?
- 点击事件被InputReader丢进了mInboundQueue, 并唤醒了 InputDispatcherThread 循环, 执行了dispatchOnce
- dispatchOnce 从 mInboundQueue中 拿到新事件, 执行了 dispatchOnceInnerLocked 尝试进行分发
- dispatchOnceInnerLocked 会调用寻址函数查找目标窗口, 然后扫描目标窗口的WaitQueue头部事件的分发时间是否超过了5s
- “诶? 这里有个时间超过5s没完成?” 超时啦! 执行 onANRLocked 通知 IMS ANR
- IMS 通知 AMS dump堆栈信息、cpu使用情况、弹出ANR弹窗
引出源码
/**
InputDispatcherThread
system_service -> InputDispatcher线程 -> 死循环
*/
void InputDispatcherThread::threadLoop() {
mDispatcher->dispatchOnce(); // 一次循环任务
}
/**
InputDispatcher.dispatchOnce
主要任务:
1、处理命令:比如 onANRLocked 这种通知逻辑,为了不阻塞主分发链路,会包装成 Command 异步执行。
2、队列移动: 从 mInboundQueue(收到的原始事件队列)移动到 WaitQueue(等待 App 确认的队列)。
3、节拍器: 计算下一次什么时候该醒来。如果没有事件,它就长眠;如果有 pending 的事件,它会精准地在 5 秒超时那一刻醒来检查。
*/
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX; // 预设下次醒来时间为无穷大
{
std::scoped_lock _l(mLock);
// 节点 A: 优先处理异步指令(如 notifyANR 回调)
if (runCommandsLockedInterruptible()) {
nextWakeupTime = LONG_LONG_MIN; // 如果有指令处理了,立即再次循环
}
// 节点 B: 提取事件 (Event Extraction)
if (!mPendingEvent) {
if (mInboundQueue.empty()) {
// 队列空,没活干
} else {
mPendingEvent = mInboundQueue.front(); // 拿出一个新事件
mInboundQueue.pop_front();
resetANRTimeLocked(); // 拿到新事件,重置旧的 ANR 等待计时
}
}
// 节点 C: 执行分发大脑
if (mPendingEvent) {
dispatchOnceInnerLocked(&nextWakeupTime);
}
}
// 节点 D: 智能休眠 (Adaptive Sleep)
// 根据 nextWakeupTime 决定睡多久:可能有事件待发,也可能在等 ANR 死线
mLooper->pollOnce(timeoutMillis);
}
/**
InputDispatch 执行一次轮训任务前的代办事项
每个是想都是通过异步执行命令方式执行
例如: 执行 notifyANR 逻辑, 链路是InputDispatcher -> IMS -> AMS (dump堆栈、计算CPU情况、弹出ANR弹窗)
*/
bool InputDispatcher::runCommandsLockedInterruptible() {
if (mCommandQueue.empty()) return false;
// InputDispatch 在进行事件分发前 "待办事项"
do {// 循环取出所有命令
// 1. 弹出队头命令
CommandEntry* commandEntry = mCommandQueue.front();
mCommandQueue.pop_front();
// 2. 执行命令(这里是一个函数指针的调用)
commandEntry->commandFunction(*this, commandEntry);
// 3. 释放资源
delete commandEntry;
} while (!mCommandQueue.empty());
return true;
}
/**
InputDispatcher 事件分发前的检查
1、检查事件有效性
2、判断事件是Key还是Motion, 决定使用不同的"寻址函数"
3、在寻址函数中, 判断窗口是否 ANR,
3.1、是
3.1.1、更新 ANR 死线时间: mInputTargetWaitTimeoutTime = currentTime + 5s
3.1.2、设置闹钟, 要求线程5s后醒来确定是否超时 (dispatchOnce 待办清单中notifyANR命令)
3.1.3、中断分发: 直接return
4、正常分发
4.1 通过InputChannel分发事件到App
4.2、清空一些缓存, 例如 mPendingEvent
*/
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
nsecs_t currentTime = now();
// 1. 丢弃检查 (如果系统正在切换或事件无效,直接释放)
if (shouldDropEventLocked(mPendingEvent)) {
releasePendingEventLocked();
return;
}
// 用于存储寻址结果的状态码
int32_t injectionResult;
std::vector<InputTarget> targets;
// 2. 类型分发路由
switch (mPendingEvent->type) {
case EventEntry::Type::KEY: {
KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);
// 【关键点】:调用 Key 的寻址函数
injectionResult = findFocusedWindowTargetsLocked(currentTime, *typedEntry,
targets, nextWakeupTime);
break;
}
case EventEntry::Type::MOTION: {
MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
// 【关键点】:调用 Motion 的寻址函数
injectionResult = findTouchedWindowTargetsLocked(currentTime, *typedEntry,
targets, nextWakeupTime);
break;
}
default:
return;
}
// 3. ANR 核心逻辑判定
if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
// 说明目标窗口还没准备好(可能卡住了)
// handleTargetsNotReadyLocked 已经在内部计算好了 mInputTargetWaitTimeoutTime (即5s死线)
// 【ANR 关键代码】:更新下一次唤醒时间
// 告诉 dispatchOnce 里的 pollOnce:在死线到来的那一刻准时醒来,不要睡过头
if (mInputTargetWaitTimeoutTime < *nextWakeupTime) {
*nextWakeupTime = mInputTargetWaitTimeoutTime;
}
// 直接返回,不执行后面的 dispatchEventLocked
// 这意味着 mPendingEvent 依然保留,下次循环继续尝试分发这个同一个事件
return;
}
// 4. 寻址成功,开始分发到 Socket
if (injectionResult == INPUT_EVENT_INJECTION_SUCCEEDED) {
dispatchEventLocked(currentTime, mPendingEvent, targets);
// 分发完成,清除当前处理中的事件,下次循环可以取新事件了
releasePendingEventLocked();
}
}
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry, …) {
// …
// 寻找分发目标:即寻找当前有焦点的窗口
std::vector<InputTarget> targets;
int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime, *entry, targets, nextWakeupTime);
// 如果返回 PENDING,说明焦点窗口由于 WaitQueue 超时还没准备好
if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
return false;
}
// 如果找到了目标,则正式开始分发
dispatchEventLocked(currentTime, entry, targets);
return true;
}
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry, …) {
std::vector<InputTarget> targets;
// 节点 H: 寻址 (Target Identification)
int32_t injectionResult = findTouchedWindowTargetsLocked(currentTime, *entry, targets, nextWakeupTime);
// 节点 I: ANR 挂起判定 (ANR Pending)
if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
// 【核心细节】:此时 handleTargetsNotReadyLocked 已经更新了 nextWakeupTime
// 这个 return 会让 dispatchOnce 里的 pollOnce 精准休眠到 5s 结束的那一刻
return false;
}
// 节点 J: 准备执行 (Execution)
if (injectionResult == INPUT_EVENT_INJECTION_SUCCEEDED) {
dispatchEventLocked(currentTime, entry, targets);
}
return true;
}
// InputDispatcher.cpp
/** 接收到新事件, 判断ANR || 正常分发 */
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime, const EventEntry& entry, …) {
// 1. 检查焦点窗口是否准备好接收新事件
std::string reason = checkWindowReadyForMoreInputLocked(currentTime, focusedWindowHandle, entry, "focused window");
if (!reason.empty()) {
// 2. 如果窗口没准备好(reason 不为空),进入处理“目标未就绪”的逻辑
return handleTargetsNotReadyLocked(currentTime, entry, nullptr, focusedWindowHandle, reason);
}
// … 正常分发逻辑
}
// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
const MotionEntry& entry, std::vector<InputTarget>& targets,
nsecs_t* nextWakeupTime) {
// 1. 如果是 ACTION_DOWN,说明是手势开始,需要进行命中测试 (Hit Testing)
if (isDown) {
int32_t x = entry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_X);
int32_t y = entry.pointerCoords[0].getAxisValue(AMOTION_EVENT_AXIS_Y);
// 遍历所有窗口(按 Z-order 从上到下)
for (const sp<WindowInfoHandle>& windowHandle : mWindowHandles) {
const WindowInfo* info = windowHandle->getInfo();
// 检查坐标是否落在窗口范围内,且窗口是否可见、可接收触摸
if (info->visible && info->touchableRegion.contains(x, y)) {
// 找到了被点中的窗口
mTempTouchState.addOrUpdateWindow(windowHandle, …);
break;
}
}
}
// 2. 获取当前正在被触摸的窗口(之前保存好的状态)
const std::vector<TouchedWindow>& touchedWindows = mTempTouchState.windows;
// 3. 【ANR 关键校验点】:检查选中的窗口是否可以接收新输入
for (const TouchedWindow& touchedWindow : touchedWindows) {
sp<WindowInfoHandle> windowHandle = touchedWindow.windowHandle;
// 核心函数:检查该窗口的 WaitQueue 是否积压或正在忙碌
std::string reason = checkWindowReadyForMoreInputLocked(currentTime,
windowHandle, entry, "touched window");
if (!reason.empty()) {
// 【触发点】:如果窗口没准备好,返回 PENDING 并启动/更新 ANR 倒计时
return handleTargetsNotReadyLocked(currentTime, entry,
nullptr, windowHandle, reason);
}
}
// 4. 寻址成功:将选中的窗口封装成 InputTarget
for (const TouchedWindow& touchedWindow : touchedWindows) {
addWindowTargetLocked(touchedWindow.windowHandle, …, targets);
}
return INPUT_EVENT_INJECTION_SUCCEEDED;
}
/** 检查焦点窗口是否准备好接收新事件 */
std::string InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime, const sp<WindowInfoHandle>& windowHandle, …) {
// 如果该窗口的等待分发队列(WaitQueue)不为空
if (!connection->waitQueue.empty()) {
/*
检查WaitQueue队头的事件(即最早发出的那个事件)是否已经超时
entry->eventTime 是事件产生的时间,timeout 是 5秒
*/
if (currentTime >= connection->waitQueue.front()->deliveryTime + timeout) {
return StringPrintf("Waiting because the %s has not finished "
"processing the input events that were previously delivered to it.", …);
}
}
return "";
}
/** '目标未就绪' 逻辑 */
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime, …) {
// 1. 如果是第一次发现超时,记录下超时开始的时间点
if (mInputTargetWaitCause == INPUT_TARGET_WAIT_CAUSE_NONE) {
mInputTargetWaitStartTime = currentTime;
mInputTargetWaitTimeoutTime = currentTime + timeout; // 设定 5秒后的闹钟
mInputTargetWaitCause = …;
}
// 2. 检查是否真的到了该“爆炸”的时间, 但它并不会立刻弹窗,而是维护一个 ANR 倒计时
if (currentTime >= mInputTargetWaitTimeoutTime) {
// 【关键点】:调用此方法触发系统层的 notifyANR
onANRLocked(currentTime, applicationHandle, windowHandle, …);
return dispatchOnceInnerLocked;
}
// 3. 如果还没到 5秒,就让当前线程等一会(通过 Looper 唤醒)
return INPUT_EVENT_INJECTION_PENDING;
}
/** 添加 "notifyANR" 命令到 dispatchOnce 的"待办清单"中 */
void InputDispatcher::onANRLocked(nsecs_t currentTime,
const sp<ApplicationInfoHandle>& applicationHandle,
const sp<WindowInfoHandle>& windowHandle,
const std::string& reason) {
// 1. 构造一个 CommandEntry
// 关键点:绑定执行函数 doNotifyANRLockedInterruptible
std::unique_ptr<CommandEntry> commandEntry = std::make_unique<CommandEntry>(
&InputDispatcher::doNotifyANRLockedInterruptible);
// 2. 封装参数:将是谁卡了、卡了多久、卡的原因(WaitQueue超时)封装进命令
commandEntry->inputApplicationHandle = applicationHandle;
commandEntry->inputWindowHandle = windowHandle;
commandEntry->reason = reason;
// 3. 【核心入口】:将命令压入队列并唤醒分发线程
// ⚠️: 这里也仅仅是丢进队列,并没有发生跨进程调用
// 真正的 ANR 通知给 IMS, 是在 runCommandsLockedInterruptible 中
postCommandLocked(std::move(commandEntry));
}
/** 入队与唤醒 */
void InputDispatcher::postCommandLocked(std::unique_ptr<CommandEntry> commandEntry) {
// 将命令放入队尾
mCommandQueue.push_back(commandEntry.release());
// 【点火动作】:唤醒 Looper
// 这一步至关重要!它会让 dispatchOnce 末尾的 pollOnce 立即返回,
// 从而让线程快速开启下一轮 dispatchOnce 循环,去执行节点 A 的 runCommandsLockedInterruptible。
mLooper->wake();
}
总结与巩固
结合上面代码, 这里提个问题巩固一下:
问题: 当Down事件阻塞了主线程, 接下来的MOVE事件会触发ANR吗? 流程是怎样的?
问题解析:
- 第一阶段: DOWN事件分发
- 用户手指按下, Down 事件被分发到App, 但未知原因主线程卡死
- 手指触摸电容屏, 产生触摸信号
- linux内核将触摸的坐标数据, 输出到 /dev/input/eventX 设备节点
- 经过system_service的EventHub、InputReader 将数据经过校准、去抖等操作输出成KeyEvent、MotionEven, 并调用InputDispatcher::enqueueInboundEventLocked将数据插到mInboundQueue
- InputDispatcher::dispatchOnce: 从 mInboundQueue 取出 DOWN 事件
- 执行寻址函数, 找到目标窗口
- 将窗口记录到 mTempTouchState 中 (方便后续MOVE事件拿到目标窗口)
- 通过 InputChannel 将 Down 事件发送给 App && Down 事件进入WaitQueue
- App 收到 Down 事件, 但因为主线程卡死, 导致一直未调用 finishInputEvent 回传信号给 InputChannel
- 这导致 Down 事件一直在 WaitQueue 中 (这个很关键!)
- 用户手指按下, Down 事件被分发到App, 但未知原因主线程卡死
- 第二阶段: MOVE事件分发 & 寻址 & ANR超时检查
- 用户手机滑动, 产生新的 Move 事件
- 硬件 -> 内核 -> EventHub \\ InputReader -> mInboundQueue
- InputDispatcher::dispatchOnce: 取出mInboundQueue中的MOVE事件
- 执行dispatchOnceInnerLocked -> dispatchMotionLocked -> 执行寻址函数
- 寻址函数中通过 mTempTouchState 直接拿到之前 Down 事件的目标窗口
- 调用 checkWindowReadyForMoreInputLocked
- 检查该窗口的 WaitQueue
- 发现队列头部的 Down 事件已处于 WaitQueue 超过5s了
- 返回失败原因 (“Waiting because the %s has not finished …
- handleTargetsNotReadyLocked
- 第一次发现该窗口不响应, 当即设置死线: mInputTargetWaitTimeoutTime = currentTime + 5s
- 返回 ANR 的Pendding: INPUT_EVENT_INJECTION_PENDING
- 回到 dispatchOnceInnerLocked
- 逻辑被 INPUT_EVENT_INJECTION_PENDING 中断, 不在继续分发
- pollOnce: 让 InputDispatcherThread 线程进入 5s 的倒计时休眠
- 用户手机滑动, 产生新的 Move 事件
- 第三阶段: 死线到达
- 5s 后, InputDispatcherThread 被唤醒
- dispatchOnce 被重新执行, 提取到之前那个 MOVE 事件
- 如果WaitQueue头部事件还是那个Down事件, 那就再次进入 handleTargetsNotReadyLocked,
- 检查发现 currentTime > mInputTargetWaitTimeoutTime (死线时间)
- 调用 onANRLocked, 创建 notifyANR 指令并插入mCommonQueue
- 压入完成后, 会执行 mLooper->wake(); 快速进入下一循环
- 返回 PENDDING, 继续拦截分发
- 第四阶段: 跨进程通知
- 当前循环结束, 进入下一循环
- dispatchOnce 执行 runCommandsLockedInterruptible, 将命令从待办清单拿出来执行
- 拿到notifyANR命令, 执行 doNotifyANRLockedInterruptible
- 通过 JNI 调用 IMS.notifyANR
- AMS 接管: dump 堆栈、计算CPU情况、弹出 ANR 弹窗
规避ANR
想要规避ANR, 需要从多层维度入手, 即: 事前预防、事中监控&提取、事后分析
ANR预防
接下来操作都是针对的主线程
耗时预防
- 内部避免进行磁盘I/O、数据库、网络请求操作
- 使用 IdleHandler 延时加载
UI预防
- 避免过度绘制
- 使用ConstrainLayout减少View树层级
- 避免onDraw中创建对象
锁预防
- 避免持锁
- 禁止锁等待
- 推荐使用协程锁(Mutex), 避免使用线程锁
- 不要再主线程使用 runBlocking
- runBlocking 会把协程的挂起, 转换成线程的硬等待(直到内部逻辑结束), 一旦主线程陷入锁等待, 就容易引起ANR
- 如果在内部使用了协程锁 Mutex.lock(), 那就直接死锁了
- 主线程所在的“协程”在等 mutex。
- runBlocking为了等这个协程,把“主线程”给停住了。
- good, 死锁
- 如果在内部使用了协程锁 Mutex.lock(), 那就直接死锁了
- runBlocking 会把协程的挂起, 转换成线程的硬等待(直到内部逻辑结束), 一旦主线程陷入锁等待, 就容易引起ANR
StrictMode
有时候我们在主线程写了耗时操作, 我们并不知道, 这时候就需要一个自动检查机制帮我们review出问题所在, 刚好, Google 的StrictMode 支持了我们的需求
StrictMode可以监控两大策略:
线程策略 (ThreadPolicy)
专注于主线程耗时操作检测
磁盘读写
抓取主线程IO读写
网络访问
抓取主线程网络请求
自定义耗时
抓取主线程耗时长逻辑
虚拟机策略 (VmPolicy)
专注于内存泄漏和对象管理
Activity 泄漏
SQLite 游标未关闭、
对象超出限制
使用
Application onCreate 中初始化
if (BuildConfig.DEBUG) {
// 设置线程策略
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads() // 监控读磁盘
.detectDiskWrites() // 监控写磁盘
.detectNetwork() // 监控网络访问
.detectCustomSlowCalls() // 监控耗时调用
.penaltyLog() // 违规了就打印 Logcat
.penaltyFlashScreen() // 违规了手机屏幕闪烁(警告最明显)
.penaltyDeath() // 违规了直接让 App 崩溃(最严厉,防止开发者无视)
.build()
)
// 设置虚拟机策略
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build()
)
}
触发
Application 配置 “严格模式” 后, 打开某个 Activity, StrictMode 会检查当前Activty的代码是否符合规范
如果日志窗口搜索 "StrictMode" , 出现下面问题, 那就需要开发者优化代码了:
|
线程策略 (ThreadPolicy) |
||
|
DiskReadViolation |
磁盘读取违规 在主线程读取文件、数据库查询等 |
|
|
NetworkViolation |
网络请求违规 主线程进行 HTTP 请求、Socket 连接等 |
|
|
CustomSlowCallViolation |
自定义耗时调用违规 方法第一行调用 StrictMode.noteSlowCall("MyCustomHeavyTask"), 当主线程执行到这里, 就会打印方法执行耗时 |
|
|
ResourceMismatchViolation |
资源匹配违规 为了捕捉那些动态获取资源时的错误: // 动态拼凑资源名获取 ID val resId = resources.getIdentifier("my_icon", "string", packageName) // my_icon 实际上是一个 drawable val icon = resources.getDrawable(resId, null) |
|
|
虚拟机策略 (VmPolicy) |
||
|
LeakedSqlLiteObjectViolation |
SQLite 对象泄漏 打开了数据库或游标(Cursor)却忘记调用 close() |
|
|
LeakedClosableObjectViolation |
资源未关闭违规 FileInputStream、OutputStream 等实现了 Closeable 接口的对象未被关闭。 |
|
|
ActivityLeakViolation |
Activity 内存泄漏 Activity 已经销毁,但在内存中依然存在(比如被静态变量持有) |
|
|
InstanceCountViolation |
类实例超限违规 监控特定类在内存中的实例数量是否超过设定上限 |
|
|
RegistrationLeakViolation |
注册对象泄漏 注册了 BroadcastReceiver 或 ServiceConnection 却忘记在 onDestroy 中注销 |
|
|
CleartextNetworkViolation |
明文网络传输违规 使用了http协议进行通信 |
ANR监控
Watchdog
子线程循环 + 主线程Handler
原理:
- 子线程开启一个死循环, 每次循环当作一个循环单元
- 循环单元逻辑组成
- 通过主线程Handler post 一个任务, 任务内是简单的标志位修改
- post 完成后线程 speep 500ms
- 500 ms 回来后标志位未修改, 视为卡顿
- 10 次连续卡顿视为即将发生ANR
SIGQUIT
通过 Native hook 拦截系统给应用进程发送的 Signal 3 (SIGQUIT 信号)
原理: Input 类型 ANR 时, AMS 会向 ANR 进程发送 SIGQUIT 信号, 流程如下.
InputDispatcher 识别到 ANR
-> IMS Native 层 -> IMS Java 层
-> 跨进程通知 AMS Java 层
-> AMS 调用 app.appNotResponding()
-> AMS 确定需要 Dump 的进程列表
-> AMS 按优先级给进程列表调用 Process.sendSignal(pid, Process.SIGNAL_QUIT)
-> APP 进程的Signal Catcher 线程接收到 SIGQUIT 信号
-> APP 暂停虚拟机, 收集所有线程堆栈
-> APP 进程将收集到的堆栈信息写入 /data/anr/traces.txt
-> 一切完成, AMS 弹出 ANR 弹窗
总结: SIGQUIT 是ANR时由 AMS 给关键应用进程发送的的自查信号, App 进程收到信号会将堆栈信息输出到预设的文件下, 虽然App基于 SIGQUIT 会存在误报的情况, 但是经过一系列判断后, 依然是线上监控 ANR 最精准的着力点.
Looper耗时足迹
Looper 在处理每一个Message的前后, 都预留了埋点, 代码如下:
// Looper.loop
public static void loop() {
// …..
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
// Looper.loopOnce
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
Message msg = me.mQueue.next(); // 可能会阻塞
// …..
final Printer logging = me.mLogging;
if (logging != null) {
// 【关键点 A】:分发前打印
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
// …..
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
}
// …..
if (logging != null) {
// 【关键点 B】:分发后打印
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// …..
return true;
}
查看关键点A、B, 可以看到, Framework 是给我们预留了钩子, 在执行Message前后做一些操作的,
我们可以通过Looper.getMainLooper().setMessageLogging(printer), 注入一个自定义的Printer, 通过计算A、B两点的时间差, 确定任务执行耗时
方案优点:
- 确定性高: 能精准知道具体哪个消息导致了阻塞
方案缺点:
- 性能损耗风险: A、B亮点, 传入的 String 涉及字符串拼接, 在高频消息下会产生大量临时对象, 甚至拖慢UI
- 如果 dispatchMessage 卡死, B 点将一直不会打印, 所有, 这个方案适合计算Message耗时, 但无法判定最终哪个Message卡死了
ANR提取 & 分析
文件提取
检查是否已经dump了anr文件
adb shell "ls -lt /data/anr/ | head -n 5"
//-rw——- 1 system system 2214816 2026-01-08 17:04 anr_2026-01-08-17-03-53-474
//-rw——- 1 system system 1950465 2026-01-08 17:02 anr_2026-01-08-17-02-47-945
//-rw——- 1 system system 2493189 2026-01-08 16:57 anr_2026-01-08-16-57-27-909
//-rw——- 1 system system 1941047 2026-01-08 11:08 anr_2026-01-08-11-08-32-397
bugreport (推荐)
pull 的 zip 文件大, 耗时长, 通常是 1-2 分钟
// 复现前, 清空缓冲区, 避免太大
adb logcat -c
// 把 ANR 当下能拿到的信息都塞给 anr_report.zip
adb bugreport ./anr_report.zip
dropbox
只 pull anr 堆栈信息, 耗时短, 但因Android的限流, 可能拿不到最新的
// 把 anr 堆栈信息放到 anr.txt
adb shell dumpsys dropbox –print data_app_anr > anr.txt
文件展示
解压打开 anr_report/FS/data/anr 里面的文件
打开后如下
文件分析
注意, 文件中的 futex_wait_queue 叫做 Waiting Channel,
记录的就是这个线程在进入睡眠状态前,最后停留在内核里的位置, 他有几种状态,如下:
|
futex_wait_queue |
有人抢我锁 |
检查多线程锁竞争、死锁 |
|
binder_thread_read |
系统服务卡了 |
检查是否频繁调用 getSystemSerivce 或 IPC |
|
0 |
未知原因 |
无法直观知道什么导致的ANR, 只能优先看看main线程有什么信息 |
场景模拟
futex_wait_queue
- 模拟代码
/** 触发 futex_wait_queue (死锁/锁竞争), 让主线程去争夺一个已经被其他线程死死占住的 Java 锁。 */
val lock = Object()
private fun testFutexWaitQueue() {
// 1. 先找个子线程把锁占了,死都不放
Thread {
synchronized(lock) {
println("子线程拿到了锁,开始无限睡眠")
Thread.sleep(Long.MAX_VALUE)
}
}.start()
// 2. 给子线程一点时间先拿锁
Thread.sleep(500)
// 3. 主线程去抢锁,直接进 futex_wait_queue
synchronized(lock) {
println("主线程永远进不来这里")
}
}
- 导出文件
先对 ANR 场景有个认知
看到
futex_wait_queue 发现, 哦, 原来是应用进程发生多线程强锁了, 继续往下查究竟怎么回事.
注意几个Title
- Process: ANR 的进程包名
- PID: ANR 的进程ID
- Activity: ANR 的窗口
- Reason: ANR 的原因
通过这个正则, 找到主线程堆栈模块, 看看发生了什么:
^"main"[\\s\\S]*?tid=1[\\s\\S]*?sysTid=20785\\b
// 注意把 20785 更换为 [你的应用进程ID]
匹配结果如下:
可以看到, 应用进程的 main 线程, 在 'TestAnrActivity.testFutexWaitQueue' 中发生了ANR
原因是 ‘waiting to lock <0x01a8df8b>’, 即正在等待锁: 0x01a8df8b
行, 那我们看看这个锁被谁持有了, 都干了些什么事.
查找锁 0x01a8df8b 的信息, 如下:
发现, 有个 'Thread-5' 正在持有锁(locked) 0x01a8df8b,
持锁位置在 'TestAnrActivity.testFutexWaitQueue$lambda$2'
至此破案啦, 主线程与子线程存在锁竞争的情况, 且主线程、子线程锁使用的位置也找到了, 接下来就是优化对应代码即可
binder_thread_read
代码
/////////////////////////////////////////////////////////
// 触发 binder_thread_read (跨进程阻塞)////////////////////
// 调用一个极其耗时的系统服务(IPC)//////////////////////////
/////////////////////////////////////////////////////////
private var mMyService: IMyANRService? = null
private val mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
mMyService = IMyANRService.Stub.asInterface(service) // 获取 AIDL 接口实例
}
override fun onServiceDisconnected(name: ComponentName?) {
mMyService = null
}
}
fun initService() {
val intent = Intent(this, TestBinderAnrService::class.java)
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
super.onDestroy()
unbindService(mServiceConnection)
}
fun triggerBinderANR() {
try {
mMyService?.blockMe() //【致命调用】主线程同步调用远程耗时方法
} catch (e: Exception) {
e.printStackTrace()
}
}
// AIDL
interface IMyANRService {
void blockMe();
}
// Service
class TestBinderAnrService : Service() {
private val binder = object : IMyANRService.Stub() {
override fun blockMe() {
Thread.sleep(60_000) // 【核心】在服务端睡 60 秒,不给客户端任何回信
}
}
override fun onBind(intent: Intent?): IBinder = binder
}
看到
binder_thread_read 发现, 哦, 原来是跨进程ANR了, 继续往下查究竟怎么回事.
看到 ANR 进程的包名了, 确实是自己应用的 ANR
正则找到 main 线程堆栈
^"main"[\\s\\S]*?tid=1[\\s\\S]*?sysTid=32243\\b
到这里直接破案, 就是 'TestAnrActivity.triggerBinderANR' 跨进程调用了'IMyANRService$Stub$Proxy.blockMe(', 内部有个耗时操作, 导致结果一直未返回, 这种有几种方式可以处理.
|
耗时操作丢到子线程执行 |
|
|
使用非阻塞API |
interface IMyService { // 异步调用,调用后立即返回,不等待执行结果 oneway void logData(String data); } |
|
添加协程耗时检查 |
try { // 3秒超时 withTimeout(3000) { myService.doSomething() } } catch (e: TimeoutCancellationException) { // 记录日志,提醒用户或切换备用逻辑 } |
0
代码
/////////////////////////////////////////////////////////
// 在主线程进行高频、大量的同步磁盘写入 ///////////////////////
/////////////////////////////////////////////////////////
fun triggerIoWaitANR() {
val file = File(this.filesDir, "toxic.txt")
// 在主线程同步循环写入大数据,强行塞满闪存写缓冲区
repeat(100000) {
// appendText 内部会进行 open/write/close,产生极高的 IO 压力
file.appendText("这是一段让 IO 爆炸的文字".repeat(1000))
}
}
从 waiting channel 看, 看不出啥, 没事, 我们往下看看 main 线程有哪些问题.
经过检查发现, 代码在 TestAnrActivity 的点击监听器中,直接在主线程执行了大规模的字符串重复(repeat)操作。这导致主线程被该计算任务长时间占据,无法及时回到 Looper 循环去分发用户的手势事件(MotionEvent),最终触发系统的 5 秒超时保护机制。
ANR三方监控方案
Crashlytics
Google 官方的 ANR 线上监控方案
Matrix
国内知名全面的 ANR 线上监控方案, 基于 ASM(字节码插桩) + Looper耗时足迹 + SIGQUIT信号
面试题
- 面试官: Dialog 窗口卡住了,Activity 窗口还会响应吗?
- 理论上InputDispatcher可以分发, 任务消费不了, 最后大概率也是ANR的命运
- 虽然每个窗口都有自己的WaitQueue, Dialog 有自己的, Activity 有自己的, ANR机制检查的也是看各窗口WaitQueue头部的事件是否待得时间超过5s, 从而判断是否ANR, 但在同个应用中, Dialog、Activity共用一个主线程, Dialog 在主线程的任务卡住了, Activity 的任务即使分发下去了, 肯定也是无法按时反馈消息分发结果的, 等Activity后续事件冲进来一样ANR
- 理论上InputDispatcher可以分发, 任务消费不了, 最后大概率也是ANR的命运




