Android InputDispatcher 跨 Display 触摸事件丢失分析
每个 Display 都拥有自己独立且完整的生态具体表现为各自管理着一组 Window。1. 引子从一个拖拽 Bug 说起用户在 Launcher 中进行拖拽操作时如果发生了 Display 焦点切换拖拽的 View 会卡在屏幕上无法消失。2. Android Input 管道全景网上已有大量关于此主题的文章可供参考本文仅作简要概述。Android 输入事件的处理管道可分为五个层次硬件层负责接收原始的触摸、按键等输入信号例如手指触摸屏幕产生一个触摸事件。Linux 内核层接收硬件信号并将其封装成input_event结构写入到如/dev/input/eventX之类的设备节点。Android Native 层负责从设备节点如/dev/input/eventX读取原始的输入事件。Android Service 层进行事件的解析、分发并将其派发给上层的应用。应用层最终接收并消费处理输入事件。3. TouchState每个 Display 独立记账3.1 mTouchStatesByDisplay 数据结构mTouchStatesByDisplay是一个std::unordered_map其键key为displayId。这意味着每个显示屏拥有自己独立的触摸状态彼此互不干扰。std::unordered_mapint32_t, TouchState mTouchStatesByDisplay GUARDED_BY(mLock);3.2 TouchState 结构体struct TouchState { bool down; // 是否有手指按下 bool split; // 是否开启了分屏触摸 int32_t deviceId; // 是哪个输入设备的触摸防止多设备冲突 uint32_t source; // 输入源触摸屏鼠标 int32_t displayId; // 是哪个显示屏的触摸 std::vectorTouchedWindow windows; // 当前触摸了哪些窗口 std::vectorspInputWindowHandle portalWindows; // 穿过了哪些 portal window std::vectorTouchedMonitor gestureMonitors; // 手势监听器 };4. 根因分析findTouchedWindowTargetsLockedvoid InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) { // ... done dispatchMotionLocked(currentTime, typedEntry, dropReason, nextWakeupTime); } int32_t InputDispatcher::dispatchMotionLocked(...) { injectionResult findTouchedWindowTargetsLocked(...); }当后续事件携带的displayId在mTouchStatesByDisplay中不存在对应的TouchState时find(displayId)会返回end()此时oldState不会从该 Display 获取到已有触摸状态Step 1拷贝旧状态const TouchState* oldState nullptr; TouchState tempTouchState; auto oldStateIt mTouchStatesByDisplay.find(displayId); // 使用 displayId 查询 map if (oldStateIt ! mTouchStatesByDisplay.end()) { oldState (oldStateIt-second); tempTouchState.copyFrom(*oldState); }Step 2判断是否是新手势bool newGesture (maskedAction AMOTION_EVENT_ACTION_DOWN || maskedAction AMOTION_EVENT_ACTION_SCROLL || isHoverAction);Step 3设备切换检测bool switchedDevice tempTouchState.deviceId 0 tempTouchState.displayId 0 (tempTouchState.deviceId ! entry.deviceId || tempTouchState.source ! entry.source || tempTouchState.displayId ! displayId); // ← 此处 displayId 不同会触发如果switchedDevice true且当前有手指按下tempTouchState.down true则会执行以下逻辑if (switchedDevice tempTouchState.down !down !isHoverAction) { ALOGI(Dropping event because a pointer for a different device is already down in display % PRId32, displayId); injectionResult INPUT_EVENT_INJECTION_FAILED; goto Failed; // ← 直接丢弃事件 } Failed: // 1. 检查权限 if (injectionPermission ! INJECTION_PERMISSION_GRANTED) { return injectionResult; // ← 直接返回什么都不做 } // 2. 更新状态但 wrongDevice 时不更新 if (!wrongDevice) { // 更新 mTouchStatesByDisplay // 但没有发送 CANCEL 事件 } return injectionResult;问题所在当后续事件的displayId与当前TouchState中记录的displayId不一致时会使switchedDevice为true。如果此时tempTouchState.down仍为true且当前事件不是新的DOWN、也不是 Hover 事件则该事件会进入Failed路径并以INPUT_EVENT_INJECTION_FAILED结束处理。进一步影响该路径不会按照正常的触摸事件分发流程继续处理当前事件因此原有拖拽手势无法正常完成后续的事件状态转换。如果拖拽 View 依赖后续的CANCEL事件清理自身状态而该CANCEL又没有在后续流程中产生或送达应用层就可能出现拖拽 View 残留在屏幕上的现象。5. 调试方法dumpsys input