欢迎光临
我们一直在努力

ANR的深层剖析

目录

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, 其实不是, 他是有触发条件的:

  • 条件1: 前面的事件超时
  • 当一个触摸事件分发给了应用窗口, 但在 5s 内未能通过 finishInputEvent 进行消费反馈
  • 条件2: 后面的事件积压
  • 旧事件超 5s 还未来得及反馈消费情况, 新事件又进来了 (例如用户点击后的滑动)
  • 埋雷 & 拆雷

    InputDispatcher 维护着一个 ANR 计时器, 埋雷拆雷针对的就是这个计时器

    • 分发事件 & 埋雷
      • 事件从 InputDispatcher 通过 InputChannel 发送给 App 进程时, InputDispatcher 会记录
        • 事件发给了哪个窗口
        • 分发的时间戳
        • 计算过期时间: currentTime + 5s
    • 点击反馈 & 拆雷
      • 正常情况: 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了
              • 已准备好
                • 正常分发

    好了, 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 中 (这个很关键!)
    • 第二阶段: 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 的倒计时休眠
    • 第三阶段: 死线到达
      • 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, 死锁

      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("主线程永远进不来这里")
      }
      }

      • 导出文件
    • 明确 Waiting Channel
    • 先对 ANR 场景有个认知

      看到
      futex_wait_queue 发现, 哦, 原来是应用进程发生多线程强锁了, 继续往下查究竟怎么回事.

    • 确定是否自己的进程发生了ANR
    • 注意几个Title

      • Process: ANR 的进程包名
      • PID: ANR 的进程ID
      • Activity: ANR 的窗口
      • Reason: ANR 的原因

    • 确定main线程为什么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
      }

    • 明确 Waiting Channel
    • 看到
      binder_thread_read 发现, 哦, 原来是跨进程ANR了, 继续往下查究竟怎么回事.

    • 确定是否自己的进程发生了ANR
    • 看到 ANR 进程的包名了, 确实是自己应用的 ANR

    • 确定main线程为什么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
    • 从 waiting channel 看, 看不出啥, 没事, 我们往下看看 main 线程有哪些问题.

    • 确定是否自己的进程发生了ANR
    • 确定main线程为什么ANR
    • 经过检查发现, 代码在 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
      赞(0)
      未经允许不得转载:171主机测评 » ANR的深层剖析
      分享到: 更多 (0)

      评论 抢沙发

      • 昵称 (必填)
      • 邮箱 (必填)
      • 网址