故事设定:
想象一个巨大的物流中心(InputDispatcher
),它负责接收全国各地(各种输入设备)发来的包裹(输入事件
),并精准投递到千家万户(App窗口
)。每个家庭(窗口)都有一个专属的快递柜(InputChannel
),用于收发快递。物流中心有一个高效的调度系统。
核心角色:
-
包裹分拣员 (
InputReader
) :监控各种输入设备(鼠标、键盘、触摸屏),当设备有事件(如触摸)时,生成标准包裹(NotifyMotionArgs
),通知调度中心取件。 -
物流中心调度员 (
InputDispatcher
):InputDispatcherThread
:调度员的核心工作线程,不停循环处理任务。mInboundQueue
:待处理包裹的传送带(队列)。mPendingEvent
:当前正在处理的包裹。mWindowHandles
:记录所有家庭(窗口)的最新地址簿(包含InputChannel
地址和家庭属性)。mConnectionsByFd
:快递员(Connection
)通讯录。每个快递员负责一个家庭(窗口)的快递柜(InputChannel
)投递和签收回执。
-
目标家庭 (
App窗口
) :每个家庭(窗口)都有一个唯一的快递柜地址 (InputChannel
)。 -
快递员 (
Connection
) :负责将包裹从物流中心取出,通过专用通道(InputChannel
代表的SocketPair)投递到指定家庭的快递柜 (InputChannel
),并等待签收回执。
故事流程:
第一章:包裹抵达物流中心 (InputReader -> InputDispatcher
)
- 触摸屏感应到你的手指动作(
MotionEvent.ACTION_DOWN
)。 - 分拣员 (
InputReader
) 立刻工作,将原始信号打包成标准物流单据NotifyMotionArgs
(包含事件类型、坐标、时间等)。 - 分拣员通过内部热线 (
InputListener
) 通知调度中心 (InputDispatcher
):notifyMotion(&args)
。 - 调度中心收到通知,根据
NotifyMotionArgs
创建详细的内部派送单MotionEntry
(包含更详细的事件信息)。 - 调度员将这个新的
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
)
-
调度员 (
InputDispatcherThread
线程循环) 醒来,发现传送带 (mInboundQueue
) 上有包裹。 -
调度员从传送带最前面 (
mInboundQueue.front()
) 取出一个包裹,标记为mPendingEvent
(当前正在处理这个包裹)。 -
调度员检查包裹:
- 包裹是否过期? (
isStaleEventLocked
):比如这个触摸事件发生很久了还没处理(屏幕卡死太久),可能直接丢弃 (DROP_REASON_STALE
)。 - 是否App切换中? (
isAppSwitchDue
):用户按了Home键等切换App的关键操作,这个包裹可能会被特殊处理或丢弃 (DROP_REASON_APP_SWITCH
)。 - 是否有更紧急包裹堵在路上? (
mNextUnblockedEvent
):比如上一个包裹还没处理完,这个包裹可能会被暂时阻塞 (DROP_REASON_BLOCKED
)。
- 包裹是否过期? (
-
如果包裹通过了检查,调度员开始查找包裹的目的地 ------ 触摸点坐标落在哪个家庭(窗口)上?
关键代码 (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
)
-
调度员拿出最新的 城市地图 (
mWindowHandles
) 。这张地图是由城市管理局 (WindowManagerService
) 实时更新的,精确记录了:-
每个家庭(窗口)的位置、大小、是否可见 (
InputWindowInfo::visible
)。 -
每个家庭的特殊属性:
FLAG_NOT_TOUCHABLE
:这个家庭不收快递(不可触摸)。FLAG_NOT_FOCUSABLE
/FLAG_NOT_TOUCH_MODAL
:影响"触摸模式"判断。FLAG_WATCH_OUTSIDE_TOUCH
:这个家庭关心落在它院子外面(但靠近它)的快递。
-
每个家庭的快递柜地址 (
InputChannel
)。
-
-
调度员根据包裹 (
MotionEntry
) 上的 触摸点坐标 (x, y) ,在地图 (mWindowHandles
) 上逐层(Z-Order,列表顺序通常反映了Z轴,后添加的在上层)查找:-
跳过: 不同显示区域 (
displayId
)、不可见、明确不收快递 (FLAG_NOT_TOUCHABLE
) 的家庭。 -
检查"触摸模式":
- 可触摸模式 (
isTouchModal
) :如果一个家庭既focusable
(可聚焦) 又touch-modal
(触摸模式),意味着任何落在它区域内的触摸都归它管,并且它上面的透明家庭收不到。相当于这个家庭是不透明的。 - 非触摸模式: 如果一个家庭是
focusable
但not touch-modal
,或者not focusable
,那么它上面的透明家庭也能收到落在它身上的触摸。相当于这个家庭是透明 或半透明的。
- 可触摸模式 (
-
坐标匹配:
- 如果家庭是 可触摸模式 (
isTouchModal
) 或者 触摸点正好落在家庭边界 (touchableRegionContainsPoint(x, y)
) 内,那么这个家庭就是目标!(newTouchedWindowHandle = windowHandle
)。 - 对于
ACTION_DOWN
事件,如果家庭设置了FLAG_WATCH_OUTSIDE_TOUCH
,即使触摸点不在它边界内,也要把它记入一个临时观察列表 (mTempTouchState
),因为它关心附近的事件(比如悬浮窗的边缘操作)。
- 如果家庭是 可触摸模式 (
-
-
找到最上层的目标家庭后,调度员更新 当前触摸状态 (
mTempTouchState
),记录这次触摸序列涉及的所有相关家庭(包括主要目标和外部观察者)。 -
关键检查:家庭是否准备好? (
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
)
-
调度员拿着派送清单 (
inputTargets
),开始安排快递员投递包裹。 -
对于清单里的每个目标 (
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
-
App 窗口 (
ViewRootImpl
内部的InputEventReceiver
) 监控着自己的快递柜 (InputChannel
)。 -
包裹 (
MotionEvent
) 到达快递柜后,InputEventReceiver
的onInputEvent()
方法会被调用。 -
App 开始处理这个触摸事件(事件传递流程:
DecorView -> Activity -> View树
)。 -
处理完毕后,App 必须 通过
InputEventReceiver.finishInputEvent()
方法,通过快递柜 (InputChannel
) 向物流中心 (InputDispatcher
) 发送一个"已签收"的回执 (InputConsumer::sendFinishedSignal
)。 -
快递员 (
Connection
) 收到回执,将对应的包裹从 等待签收队列 (waitQueue
) 移除,并通知AnrTracker
停止追踪这个包裹的倒计时。物流中心知道这个家庭响应正常。 -
如果倒计时结束 (
ANR 超时时间
到达,默认为 5 秒) 还没收到回执:- 物流中心 (
InputDispatcher
) 的AnrTracker
会触发。 InputDispatcher
会记录 ANR 信息(哪个 App 超时了?哪个事件?)。- 系统会弹出 ANR 对话框,提示用户 App 无响应。
- 后续发给这个 App 的事件可能会被丢弃或特殊处理,直到 App 恢复响应。
- 物流中心 (
总结一下关键流程:
-
事件采集 (
InputReader
) :硬件 ->NotifyMotionArgs
->MotionEntry
->mInboundQueue
。 -
事件调度 (
InputDispatcherThread
):-
取事件 (
mPendingEvent = mInboundQueue.dequeue()
) -
检查丢弃 (
DROP_REASON_*
) -
找目标窗口 (
findTouchedWindowTargetsLocked
):- 遍历
mWindowHandles
(WMS提供)。 - 根据坐标、Z-order、窗口属性 (
visible
,touchable
,modal
,outside
) 确定目标。 - 检查目标窗口是否就绪 (ANR检测点!)。
- 生成
InputTarget
列表 (含坐标转换参数)。
- 遍历
-
-
事件分发 (
dispatchEventLocked
):- 为每个
InputTarget
找到对应的Connection
。 - 进行坐标转换 (
xOffset/yOffset/scaleFactor
)。 - 放入
Connection.outboundQueue
。 - 通过
InputChannel
(Socket) 发送事件 (InputPublisher.sendDispatchSignal()
)。 - 事件移入
Connection.waitQueue
,开始 ANR 倒计时。
- 为每个
-
事件处理 (App):
InputChannel
->InputEventReceiver.onInputEvent()
-> View树处理。- 必须调用
finishInputEvent()
发送完成信号!
-
完成确认 (
InputDispatcher
):- 收到
InputConsumer::sendFinishedSignal()
-> 移除waitQueue
中的事件,停止 ANR 倒计时。 - 未收到 -> ANR!
- 收到
通过这个故事和代码解析,你应该能清晰地看到:
-
InputDispatcher
的核心作用: 全局分发枢纽,管理事件队列、寻找目标窗口、处理 ANR。 -
mWindowHandles
的重要性: WMS 提供的窗口信息是正确寻址的关键。 -
InputChannel
的双向通道: 事件下发和完成回执的生命线,ANR 检测的基石。 - 坐标转换的必要性: 屏幕坐标到窗口坐标的转换发生在
InputDispatcher
向Connection
提交事件时。 -
findTouchedWindowTargetsLocked
的复杂性: 涉及 Z-order、窗口属性、区域检测、状态检查(ANR源头之一)。 - ANR 的触发机制: App 未在规定时间内通过
InputChannel
发送完成信号。
理解了这个流程,对于分析触摸事件延迟、ANR 问题、自定义输入处理(如游戏)以及理解 Android 窗口管理 (WMS
) 与输入系统的协作都至关重要。希望这个"物流中心"的故事让复杂的源码变得生动易懂!