4、Input事件超时:输入事件分发路径、系统如何判定超时、KeyEvent/触摸事件ANR案例

4.1 输入事件分发路径

Input 事件从硬件到应用,到底走了多远?很多人以为就是屏幕点一下,直接回调 onTouchEvent。其实背后是一条很长的链路。我把它拆成三个阶段:内核捕获 → Native 分发 → 应用处理。每个阶段都可能卡住,最终引发 ANR。

核心要点:Input ANR 的本质是事件从 InputDispatcher 派发到应用处理完成,超过 5 秒没有反馈。

4.1.1 内核层:硬件事件怎么变成输入流?

触屏或按键按下时,硬件中断触发,内核输入子系统将事件写入 /dev/input/ 设备节点。InputReader 线程通过 epoll 监控这些节点,读取出原始事件数据。

// 内核上报的原始事件格式(struct input_event)
struct input_event {
    struct timeval time;  // 时间戳
    __u16 type;          // 类型:EV_KEY / EV_ABS / EV_REL
    __u16 code;          // 键值或轴编号
    __s32 value;         // 按下/抬起/坐标值
};

4.1.2 Native 层:InputReader 与 InputDispatcher 的分工

InputReader 负责读取原始数据,将其封装成 KeyEvent 或 MotionEvent,然后扔给 InputDispatcher。InputDispatcher 负责找到当前焦点窗口,把事件派发过去。这两兄弟配合不好,ANR 就来了。

组件职责线程关键方法
InputReader读取、解码、分类事件InputReaderThreadloopOnce() → processEventsLocked()
InputDispatcher窗口查找、事件派发、超时监控InputDispatcherThreaddispatchEventLocked() → startDispatchCycleLocked()
InputChannel跨进程传输事件(Socket 对)—sendMessage() / receiveMessage()
WindowInputEventReceiver应用端接收事件入口主线程 LooperonInputEvent()

4.1.3 应用层:事件如何到达具体的 View?

ViewRootImpl 中有一个 InputEventReceiver,它把事件从 Native 层拉到 Java 层。然后通过 DecorView 分发给当前 Focused View 或 TouchTarget。

// 应用层接收事件的核心路径
ViewRootImpl$WindowInputEventReceiver.onInputEvent()
  → ViewRootImpl.enqueueInputEvent()
    → ViewRootImpl.processInputEvent()
      → ViewRootImpl.deliverInputEvent()
        → View.dispatchTouchEvent()  /  View.dispatchKeyEvent()
💡 我个人建议:分析 Input ANR 时,优先关注 InputDispatcher 的日志,它记录了每次派发的目标窗口和超时状态,比应用层日志更可靠。

4.2 系统如何判定超时

判定逻辑其实不复杂。InputDispatcher 给每个派发的事件挂一个 5 秒的计时器。5 秒内收不到应用的「处理完成」信号,就判定超时。

4.2.1 关键数据结构:Connection 与 InputChannel

每个窗口对应一个 Connection 对象。InputDispatcher 通过 InputChannel 与应用的 InputEventReceiver 通信。

// InputDispatcher.cpp 中的核心判断
status_t InputDispatcher::startDispatchCycleLocked(
    nsecs_t currentTime, const sp<Connection>& commandConnection) {
    // 记录派发时间
    commandConnection->inputPublisherTouched = true;
    commandConnection->inputPublisherWait = currentTime;
    
    // 启动超时监控(5 秒后回调)
    setAnrTimeoutsLocked(commandConnection);
    return OK;
}

void InputDispatcher::setAnrTimeoutsLocked(
    const sp<Connection>& connection) {
    nsecs_t timeout = 5 * 1000 * 1000000LL;  // 5 秒
    connection->responseTime = systemTime() + timeout;
    // 向 Looper 注册一个超时回调
    mLooper->removeMessages(connection->inputChannel->getFd());
    mLooper->addFd(connection->inputChannel->getFd(), ...);
}

4.2.2 超时判定的两条路径

  • 分发超时:事件已经通过 InputChannel 发给应用,但应用没有在规定时间内返回 finished 信号。这是最常见的 Input ANR 类型。
  • 队列积压超时:应用主线程卡住,上一个事件还没处理完,新的 Input 事件在队列里排着,导致后续事件整体超时。说白了,就是「前面堵车,后面的车过不去」。

4.2.3 超时后的处理流程

InputDispatcher 发现超时后,会向 SystemServer 的 InputManagerService 发送 ANR 回调。然后 AMS 开始收集 CPU 信息、各线程堆栈,最终弹出 ANR 对话框。

// InputDispatcher 超时后调用
void InputDispatcher::onAnrLocked(
    const sp<Connection>& connection) {
    InputManagerService* ims = getInputManagerService();
    // 构造 ANR 信息
    int32_t pid = connection->inputChannel->getPid();
    std::string reason = "Input dispatching timed out";
    // 发给 AMS 处理
    ims->notifyANR(pid, reason);
}
⚠️ 避坑指南:我曾经踩过一个坑——Input ANR 的日志里只看到 "Waiting because no window has focus...",以为是窗口焦点问题。查了两天才发现是 InputDispatcher 正在等待一个已经退出的窗口的响应。窗口虽然没了,但 Connection 没有及时清理,导致超时。碰到这种日志,先检查窗口生命周期管理。

4.3 KeyEvent / 触摸事件 ANR 案例

4.3.1 案例一:KeyEvent ANR —— 某个按键在 onKeyDown 中做了 I/O

当时我接手一个智能硬件项目,遥控器按返回键经常卡住 5 秒弹出 ANR。看堆栈发现 onKeyDown 里有一段数据库查询,偶尔因为锁竞争执行超过 5 秒。

// 问题代码(简化版)
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK) {
        // ⚠️ 在主线程做数据库查询,导致 Input 超时
        List<String> data = database.queryLargeData();  
        updateUI(data);
        return true;
    }
    return super.onKeyDown(keyCode, event);
}

解决方式:把数据库查询丢到子线程,用 LiveData 或 Handler 回调更新 UI。同时加了一个「正在退出」的 Toast 让用户有感知。

4.3.2 案例二:触摸事件 ANR —— onTouchEvent 里做网络请求

这其实是一个很经典的错误。我在一个图片编辑 App 里遇到过,用户在画布上拖动时触发 ANR。堆栈指向 MotionEvent.ACTION_MOVE 的分发,主线程卡在 OkHttp 的同步调用上。

// 问题代码(简化版)
@Override
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_MOVE) {
        // ⚠️ 在主线程做网络请求,致命错误
        Response resp = okHttpClient.newCall(request).execute();  
        processResult(resp.body().string());
        return true;
    }
    return super.onTouchEvent(event);
}

解决方式:网络请求必须走异步。另外我建议在 onTouchEvent 里只做 UI 相关的轻量操作,任何超过 20ms 的工作都应该移到子线程或使用 View.post 延迟处理。

💡 经验之谈:碰到 Input ANR,我第一反应不是代码,而是先看 traces.txt 里主线程在干什么。InputDispatcher 的日志会告诉你「等的是哪个窗口的哪个事件」。然后顺着窗口找 View,基本 10 分钟定位。

4.3.3 KeyEvent vs TouchEvent ANR 特征对比

对比维度KeyEvent ANRTouchEvent ANR
触发场景物理按键(返回、菜单、音量等)触摸屏滑动、点击、长按
典型堆栈View.onKeyDown() / Activity.onKeyDown()View.onTouchEvent() / ViewGroup.dispatchTouchEvent()
超时敏感度单次按键超 5s 即触发连续 Move 事件可能累计超时
排查方向焦点窗口的 onKeyDown / onKeyUpTouchTarget 的 onTouchEvent / ScrollView 嵌套
我遇到的频率偏低(约占 Input ANR 的 20%)偏高(约占 Input ANR 的 80%)

4.4 本章小结

Input 事件超时的根因,说白了就是主线程卡住了,没能及时处理派发过来的事件。事件分发路径虽然长,但每个环节都有日志可查。

  • 分发路径:内核 → InputReader → InputDispatcher → 应用主线程 → View
  • 超时判定:InputDispatcher 为每个事件设置 5 秒倒计时,超时即 ANR
  • 排查要点:先看 InputDispatcher 日志,再看主线程堆栈,最后定位到具体的 View 回调

嗯,这一章就到这里。记住一条:主线程别碰 I/O,别碰锁,别碰网络。做到这三点,Input ANR 基本与你无缘。

Input事件分发与ANR判定流程 输入事件产生 (内核/驱动) InputReader (Native) InputDispatcher (Native) 超时监控 (5s倒计时) ANR决策与上报 应用正常处理 超时 正常完成 核心逻辑:每个事件派发后启动5s计时,超时未响应则触发ANR