Android 输入事件是如何发送到目标窗口的

​故事设定:​

想象一个巨大的物流中心(InputDispatcher),它负责接收全国各地(各种输入设备)发来的包裹(输入事件),并精准投递到千家万户(App窗口)。每个家庭(窗口)都有一个专属的快递柜(InputChannel),用于收发快递。物流中心有一个高效的调度系统。


​核心角色:​

  1. ​包裹分拣员 (InputReader)​ ​:监控各种输入设备(鼠标、键盘、触摸屏),当设备有事件(如触摸)时,生成标准包裹(NotifyMotionArgs),通知调度中心取件。

  2. ​物流中心调度员 (InputDispatcher)​​:

    • InputDispatcherThread:调度员的核心工作线程,不停循环处理任务。
    • mInboundQueue:待处理包裹的传送带(队列)。
    • mPendingEvent:当前正在处理的包裹。
    • mWindowHandles:记录所有家庭(窗口)的最新地址簿(包含 InputChannel 地址和家庭属性)。
    • mConnectionsByFd:快递员(Connection)通讯录。每个快递员负责一个家庭(窗口)的快递柜(InputChannel)投递和签收回执。
  3. ​目标家庭 (App窗口)​ ​:每个家庭(窗口)都有一个唯一的快递柜地址 (InputChannel)。

  4. ​快递员 (Connection)​ ​:负责将包裹从物流中心取出,通过专用通道(InputChannel代表的SocketPair)投递到指定家庭的快递柜 (InputChannel),并等待签收回执。


​故事流程:​

​第一章:包裹抵达物流中心 (InputReader -> InputDispatcher)​

  1. 触摸屏感应到你的手指动作(MotionEvent.ACTION_DOWN)。
  2. 分拣员 (InputReader) 立刻工作,将原始信号打包成标准物流单据 NotifyMotionArgs(包含事件类型、坐标、时间等)。
  3. 分拣员通过内部热线 (InputListener) 通知调度中心 (InputDispatcher):notifyMotion(&args)
  4. 调度中心收到通知,根据 NotifyMotionArgs 创建详细的内部派送单 MotionEntry (包含更详细的事件信息)。
  5. 调度员将这个新的 MotionEntry 包裹放到待处理传送带 (mInboundQueue) 的末尾。

​关键代码 (InputDispatcher::notifyMotion):​

scss 复制代码
cpp
Copy
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
    ... // 参数检查等
    MotionEntry* newEntry = new MotionEntry(...args...); // 创建 MotionEntry
    needWake = enqueueInboundEventLocked(newEntry); // 放入 mInboundQueue
    if (needWake) {
        mLooper->wake(); // 如果调度员在睡觉,唤醒他(唤醒 InputDispatcherThread)
    }
}

​第二章:调度员处理包裹 (dispatchOnceInnerLocked)​

  1. 调度员 (InputDispatcherThread 线程循环) 醒来,发现传送带 (mInboundQueue) 上有包裹。

  2. 调度员从传送带最前面 (mInboundQueue.front()) 取出一个包裹,标记为 mPendingEvent(当前正在处理这个包裹)。

  3. 调度员检查包裹:

    • ​包裹是否过期?​ (isStaleEventLocked):比如这个触摸事件发生很久了还没处理(屏幕卡死太久),可能直接丢弃 (DROP_REASON_STALE)。
    • ​是否App切换中?​ (isAppSwitchDue):用户按了Home键等切换App的关键操作,这个包裹可能会被特殊处理或丢弃 (DROP_REASON_APP_SWITCH)。
    • ​是否有更紧急包裹堵在路上?​ (mNextUnblockedEvent):比如上一个包裹还没处理完,这个包裹可能会被暂时阻塞 (DROP_REASON_BLOCKED)。
  4. 如果包裹通过了检查,调度员开始查找包裹的目的地 ------ ​​触摸点坐标落在哪个家庭(窗口)上?​

​关键代码 (InputDispatcher::dispatchOnceInnerLocked):​

ini 复制代码
cpp
Copy
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    ...
    // 1. 从 mInboundQueue 取出事件成为 mPendingEvent
    mPendingEvent = mInboundQueue.dequeueAtHead();
    ...
    // 2. 检查丢弃原因 (DROP_REASON_*)
    DropReason dropReason = DROP_REASON_NOT_DROPPED;
    if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
        dropReason = DROP_REASON_APP_SWITCH;
    }
    if (dropReason == DROP_REASON_NOT_DROPPED && isStaleEventLocked(...)) {
        dropReason = DROP_REASON_STALE;
    }
    if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
        dropReason = DROP_REASON_BLOCKED;
    }
    ...
    // 3. 根据事件类型分发 (这里看触摸事件)
    case EventEntry::TYPE_MOTION: {
        done = dispatchMotionLocked(...); // 核心分发函数
        break;
    }
}

​第三章:寻找目标家庭 (findTouchedWindowTargetsLocked)​

  1. 调度员拿出最新的 ​​城市地图 (mWindowHandles)​ ​。这张地图是由城市管理局 (WindowManagerService) 实时更新的,精确记录了:

    • 每个家庭(窗口)的位置、大小、是否可见 (InputWindowInfo::visible)。

    • 每个家庭的特殊属性:

      • FLAG_NOT_TOUCHABLE:这个家庭不收快递(不可触摸)。
      • FLAG_NOT_FOCUSABLE/FLAG_NOT_TOUCH_MODAL:影响"触摸模式"判断。
      • FLAG_WATCH_OUTSIDE_TOUCH:这个家庭关心落在它院子外面(但靠近它)的快递。
    • 每个家庭的快递柜地址 (InputChannel)。

  2. 调度员根据包裹 (MotionEntry) 上的 ​​触摸点坐标 (x, y)​ ​,在地图 (mWindowHandles) 上逐层(Z-Order,列表顺序通常反映了Z轴,后添加的在上层)查找:

    • ​跳过:​ ​ 不同显示区域 (displayId)、不可见、明确不收快递 (FLAG_NOT_TOUCHABLE) 的家庭。

    • ​检查"触摸模式":​

      • ​可触摸模式 (isTouchModal)​ :如果一个家庭既 focusable (可聚焦) 又 touch-modal (触摸模式),意味着任何落在它区域内的触摸都归它管,并且它上面的透明家庭收不到。相当于这个家庭是​不透明​的。
      • ​非触摸模式:​ 如果一个家庭是 focusablenot touch-modal,或者 not focusable,那么它上面的透明家庭也能收到落在它身上的触摸。相当于这个家庭是​透明​​半透明​的。
    • ​坐标匹配:​

      • 如果家庭是 ​可触摸模式 (isTouchModal)​ 或者 ​触摸点正好落在家庭边界 (touchableRegionContainsPoint(x, y))​ 内,那么这个家庭就是目标!(newTouchedWindowHandle = windowHandle)。
      • 对于 ACTION_DOWN 事件,如果家庭设置了 FLAG_WATCH_OUTSIDE_TOUCH,即使触摸点不在它边界内,也要把它记入一个​临时观察列表 (mTempTouchState)​,因为它关心附近的事件(比如悬浮窗的边缘操作)。
  3. 找到最上层的目标家庭后,调度员更新 ​​当前触摸状态 (mTempTouchState)​​,记录这次触摸序列涉及的所有相关家庭(包括主要目标和外部观察者)。

  4. ​关键检查:家庭是否准备好?​ ​ (checkWindowReadyForMoreInputLocked)

    • 调度员挨个检查 mTempTouchState 里的家庭:

      • 快递柜 (InputChannel) 是否还连接着 (connection != NULL)?
      • 家庭上次的签收回执 (InputPublisher) 是否超时未回复?(ANR检测的关键!)
      • 家庭是否在处理太多包裹导致快递柜满了 (connection->outboundQueue 是否饱和)?
    • ​如果有一个家庭没准备好 (!reason.isEmpty()):​

      • 记录原因 (injectionResult = handleTargetsNotReadyLocked(...))。这个原因可能就是"签收回执超时"(潜在ANR!)。
      • 设置 nextWakeupTime 为最小时间 (LONG_LONG_MIN),强制调度员立刻醒来重试(处理ANR或重试)。
      • 跳到 Unresponsive 标签,重置临时状态 (mTempTouchState.reset())。
      • 返回 INPUT_EVENT_INJECTION_PENDING 状态,表示事件还在等待投递。
    • ​如果所有家庭都准备好了 (injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED):​

      • 调度员为 mTempTouchState 中的每个目标家庭生成一张 ​​派送任务单 (InputTarget)​​:

        • inputChannel:目标家庭的快递柜地址。
        • flags:派送要求 (如 FLAG_FOREGROUND 表示前台家庭,FLAG_WINDOW_IS_OBSCURED 表示这个家庭被部分遮挡)。
        • xOffset, yOffset, scaleFactor:坐标转换参数!因为包裹上的坐标是​屏幕坐标系​ ,而每个家庭使用的是​自家窗口坐标系​。这些参数用于在投递前进行坐标转换(就像跨国快递要转换货币和地址格式)。
        • pointerIds:涉及哪些手指(多点触摸)。
      • 把这些派送单收集到派送清单 (inputTargets 列表) 中。

​关键代码 (InputDispatcher::findTouchedWindowTargetsLocked 核心片段):​

ini 复制代码
cpp
Copy
int32_t InputDispatcher::findTouchedWindowTargetsLocked(...) {
    ... // 获取坐标点 (x, y) 等
    // Part 1: 寻找目标窗口
    for (size_t i = 0; i < mWindowHandles.size(); i++) {
        sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i);
        const InputWindowInfo* windowInfo = windowHandle->getInfo();
        ... // 检查 displayId, visible, FLAG_NOT_TOUCHABLE 等

        // 判断是否可触摸模态
        isTouchModal = (flags & (FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL)) == 0;
        if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
            newTouchedWindowHandle = windowHandle; // 找到目标!
            break;
        }
        // 处理 FLAG_WATCH_OUTSIDE_TOUCH
        if (maskedAction == AMOTION_EVENT_ACTION_DOWN && (flags & FLAG_WATCH_OUTSIDE_TOUCH)) {
            mTempTouchState.addOrUpdateWindow(windowHandle, ...);
        }
    }

    // Part 2: 检查目标窗口是否就绪
    for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
        const TouchedWindow& touchedWindow = mTempTouchState.windows[i];
        if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
            String8 reason = checkWindowReadyForMoreInputLocked(...);
            if (!reason.isEmpty()) {
                injectionResult = handleTargetsNotReadyLocked(...); // ANR检测点!
                goto Unresponsive; // 跳转到处理未响应的代码块
            }
        }
    }

    // Part 3: 生成 InputTargets
    injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;
    for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
        const TouchedWindow& touchedWindow = mTempTouchState.windows.itemAt(i);
        addWindowTargetLocked(touchedWindow.windowHandle, // 核心:为每个目标创建InputTarget
                             touchedWindow.targetFlags,
                             touchedWindow.pointerIds,
                             inputTargets);
    }

Unresponsive:
    mTempTouchState.reset(); // 重置临时状态
    return injectionResult;
}

​第四章:派出快递员投递 (dispatchEventLocked -> prepareDispatchCycleLocked)​

  1. 调度员拿着派送清单 (inputTargets),开始安排快递员投递包裹。

  2. 对于清单里的每个目标 (InputTarget):

    • 调度员根据目标家庭的快递柜地址 (inputTarget.inputChannel),在快递员通讯录 (mConnectionsByFd) 里找到负责这个地址的专属快递员 (Connection)。

    • 调度员把包裹 (MotionEntry)、派送单 (InputTarget) 交给快递员 (prepareDispatchCycleLocked)。

    • 快递员 (Connection) 拿到包裹后:

      • 根据派送单 (InputTarget) 上的坐标转换参数 (xOffset, yOffset, scaleFactor),​​把包裹上的屏幕坐标转换成目标家庭内部的窗口坐标​​。

      • 将转换好坐标的包裹放入自己携带的​​专用派件箱 (connection->outboundQueue)​ ​。这个箱子通过专用通道 (InputChannel SocketPair) 直连目标家庭的快递柜 (InputChannel)。

      • 快递员启动派件流程 (startDispatchCycleLocked):

        • 从派件箱拿出包裹。
        • 通过专用通道 (InputChannel) 将包裹推送到目标家庭的快递柜里。
        • ​等待签收回执:​ 快递员会记录发出包裹的时间,并设置一个​倒计时 (ANR 超时时间)​ 。如果家庭 (App) 没有在规定时间内通过这个专用通道发回"已签收"的回执 (InputConsumer::sendFinishedSignal),物流中心 (InputDispatcher) 就会认为这个家庭​无响应 (ANR)​

​关键代码 (InputDispatcher::dispatchEventLocked & prepareDispatchCycleLocked):​

ini 复制代码
cpp
Copy
void InputDispatcher::dispatchEventLocked(...) {
    ...
    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);
        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel); // 找 Connection
        if (connectionIndex >= 0) {
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget); // 交给快递员
        }
    }
}

void InputDispatcher::prepareDispatchCycleLocked(...) {
    ...
    enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget); // 放入快递员的outboundQueue
    ...
}

// (在 enqueueDispatchEntriesLocked 内部会最终调用 startDispatchCycleLocked)
void InputDispatcher::startDispatchCycleLocked(...) {
    ...
    // 1. 转换坐标 (在 publishMotionEvent 内部)
    status = connection->inputPublisher.publishMotionEvent(...,
            inputTarget->xOffset, inputTarget->yOffset, inputTarget->scaleFactor ...);
    ...
    // 2. 发送事件 (通过 InputChannel 的 Socket)
    status = connection->inputPublisher.sendDispatchSignal();
    ...
    // 3. 记录发送时间,开始 ANR 倒计时
    connection->outboundQueue.head->dispatchInProgress = true;
    connection->waitQueue.add(connection->outboundQueue.dequeueAtHead()); // 移到等待签收队列
    mAnrTracker.insert(connection->inputPublisher.getPendingEventId(), ...); // 开始追踪超时
}

​第五章:家庭签收与ANR​

  1. App 窗口 (ViewRootImpl 内部的 InputEventReceiver) 监控着自己的快递柜 (InputChannel)。

  2. 包裹 (MotionEvent) 到达快递柜后,InputEventReceiveronInputEvent() 方法会被调用。

  3. App 开始处理这个触摸事件(事件传递流程:DecorView -> Activity -> View树)。

  4. 处理完毕后,App ​​必须​ ​ 通过 InputEventReceiver.finishInputEvent() 方法,通过快递柜 (InputChannel) 向物流中心 (InputDispatcher) 发送一个"已签收"的回执 (InputConsumer::sendFinishedSignal)。

  5. 快递员 (Connection) 收到回执,将对应的包裹从 ​​等待签收队列 (waitQueue)​ ​ 移除,并通知 AnrTracker 停止追踪这个包裹的倒计时。物流中心知道这个家庭响应正常。

  6. ​如果倒计时结束 (ANR 超时时间 到达,默认为 5 秒) 还没收到回执:​

    • 物流中心 (InputDispatcher) 的 AnrTracker 会触发。
    • InputDispatcher 会记录 ANR 信息(哪个 App 超时了?哪个事件?)。
    • 系统会弹出 ​ANR 对话框​,提示用户 App 无响应。
    • 后续发给这个 App 的事件可能会被丢弃或特殊处理,直到 App 恢复响应。

​总结一下关键流程:​

  1. ​事件采集 (InputReader)​ ​:硬件 -> NotifyMotionArgs -> MotionEntry -> mInboundQueue

  2. ​事件调度 (InputDispatcherThread)​​:

    • 取事件 (mPendingEvent = mInboundQueue.dequeue())

    • 检查丢弃 (DROP_REASON_*)

    • 找目标窗口 (findTouchedWindowTargetsLocked):

      • 遍历 mWindowHandles (WMS提供)。
      • 根据坐标、Z-order、窗口属性 (visible, touchable, modal, outside) 确定目标。
      • 检查目标窗口是否就绪 (​ANR检测点!​)。
      • 生成 InputTarget 列表 (含坐标转换参数)。
  3. ​事件分发 (dispatchEventLocked)​​:

    • 为每个 InputTarget 找到对应的 Connection
    • 进行坐标转换 (xOffset/yOffset/scaleFactor)。
    • 放入 Connection.outboundQueue
    • 通过 InputChannel (Socket) 发送事件 (InputPublisher.sendDispatchSignal())。
    • 事件移入 Connection.waitQueue,开始 ​ANR 倒计时​
  4. ​事件处理 (App)​​:

    • InputChannel -> InputEventReceiver.onInputEvent() -> View树处理。
    • ​必须调用 finishInputEvent() 发送完成信号!​
  5. ​完成确认 (InputDispatcher)​​:

    • 收到 InputConsumer::sendFinishedSignal() -> 移除 waitQueue 中的事件,停止 ANR 倒计时。
    • 未收到 -> ​ANR!​

​通过这个故事和代码解析,你应该能清晰地看到:​

  • InputDispatcher 的核心作用:​ 全局分发枢纽,管理事件队列、寻找目标窗口、处理 ANR。
  • mWindowHandles 的重要性:​ WMS 提供的窗口信息是正确寻址的关键。
  • InputChannel 的双向通道:​ 事件下发和完成回执的生命线,ANR 检测的基石。
  • ​坐标转换的必要性:​ 屏幕坐标到窗口坐标的转换发生在 InputDispatcherConnection 提交事件时。
  • findTouchedWindowTargetsLocked 的复杂性:​ 涉及 Z-order、窗口属性、区域检测、状态检查(ANR源头之一)。
  • ​ANR 的触发机制:​ App 未在规定时间内通过 InputChannel 发送完成信号。

理解了这个流程,对于分析触摸事件延迟、ANR 问题、自定义输入处理(如游戏)以及理解 Android 窗口管理 (WMS) 与输入系统的协作都至关重要。希望这个"物流中心"的故事让复杂的源码变得生动易懂!

相关推荐
用户2018792831673 小时前
通俗易懂的讲解:Android系统启动全流程与Launcher诞生记
android
二流小码农3 小时前
鸿蒙开发:资讯项目实战之项目框架设计
android·ios·harmonyos
用户2018792831674 小时前
WMS 的核心成员和窗口添加过程
android
用户2018792831675 小时前
PMS 创建之“软件包管理超级工厂”的建设
android
用户2018792831675 小时前
通俗易懂的讲解:Android APK 解析的故事
android
渣渣_Maxz5 小时前
使用 antlr 打造 Android 动态逻辑判断能力
android·设计模式
Android研究员5 小时前
HarmonyOS实战:List拖拽位置交换的多种实现方式
android·ios·harmonyos
guiyanakaung5 小时前
一篇文章让你学会 Compose Multiplatform 推荐的桌面应用打包工具 Conveyor
android·windows·macos
恋猫de小郭5 小时前
Flutter 应该如何实现 iOS 26 的 Liquid Glass ,它为什么很难?
android·前端·flutter
葱段5 小时前
【Compose】Android Compose 监听TextField粘贴事件
android·kotlin·jetbrains