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) 与输入系统的协作都至关重要。希望这个"物流中心"的故事让复杂的源码变得生动易懂!

相关推荐
福柯柯24 分钟前
Android ContentProvider的使用
android·contenprovider
不想迷路的小男孩24 分钟前
Android Studio 中Palette跟Component Tree面板消失怎么恢复正常
android·ide·android studio
餐桌上的王子25 分钟前
Android 构建可管理生命周期的应用(一)
android
菠萝加点糖29 分钟前
Android Camera2 + OpenGL离屏渲染示例
android·opengl·camera
用户20187928316740 分钟前
🌟 童话:四大Context徽章诞生记
android
yzpyzp1 小时前
Android studio在点击运行按钮时执行过程中输出的compileDebugKotlin 这个任务是由gradle执行的吗
android·gradle·android studio
aningxiaoxixi1 小时前
安卓之service
android
TeleostNaCl2 小时前
Android 应用开发 | 一种限制拷贝速率解决因 IO 过高导致系统卡顿的方法
android·经验分享
用户2018792831672 小时前
📜 童话:FileProvider之魔法快递公司的秘密
android
vocal6 小时前
【我的安卓第一课】Android 多线程与异步通信机制(1)
android