IMS 键盘鼠标事件全链路拆解:从 EventHub 到 CursorInputMapper 的配置与验证
1. 从一次鼠标失焦说起IMS 输入链路到底怎么走Android 的输入子系统IMSInput Manager Service负责把内核上报的键盘、鼠标、触摸事件一路送到应用窗口。很多同学在调试时会遇到这样的现象鼠标能移动但点击没反应或者键盘按键在某个界面突然失效又或者外接蓝牙鼠标后光标乱跳。这类问题往往不是应用层写错了而是事件在 EventHub → InputDevice → CursorInputMapper 这条链路的某一环丢失或错配了。这篇内容聚焦 Android IMS 输入子系统沿着 EventHub 采集原始事件、InputDevice 做设备归类、CursorInputMapper 把鼠标事件翻译成上层可识别的 MotionEvent 这条主线把键盘鼠标事件从内核节点到窗口的流转路径拆开讲。适合正在做输入调试、排查光标异常、定位事件失焦的 Android 系统或驱动开发者。文中会给出可复制的 IMS 配置骨架、事件抓取验证命令以及每一环的排障思路帮你判断事件到底卡在哪一层。我试过在 Android 11 release 上追一个蓝牙鼠标点击丢失的问题最后发现是 CursorButtonAccumulator 的按键状态没同步上。下面把整条链路和验证方法完整梳理一遍。2. TaoToken 前置给调试加一个模型对话入口在深入 IMS 源码之前先说一个能明显提升排障效率的辅助手段。调试输入子系统时经常需要查内核 input-event-codes 的定义、确认某个 BTN_ 宏的数值、或者让模型帮你解释一段 InputReader 的调用栈。这时候一个稳定的模型对话入口就很有用。TaoToken 提供统一的模型调用能力官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。你可以把它当成一个随时在线的“输入子系统答疑助手”把 CursorInputMapper::sync 的代码片段贴进去让它帮你逐行解释 synthesizeButtonKeys 的触发条件或者把 getEvents 抓到的 RawEvent 结构体丢进去确认 type/code/value 的含义。如果你更习惯在编辑器里直接对话可以用模型对话入口https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。需要长期做编码和 Agent 类任务的话Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。申请 Key 在控制台完成https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Claude Code 相关配置参考 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。注意TaoToken 在这里的角色是辅助理解源码和日志不替代你对 IMS 链路的实际验证。真正的定位仍然要靠 getevent、dumpsys input 和日志。3. 可复制配置EventHub 到 CursorInputMapper 的链路骨架3.1 EventHub 采集原始事件EventHub 是 IMS 的最底层负责打开 /dev/input/eventX 节点并读取 RawEvent。它的核心结构体定义在 EventHub.hstruct RawEvent { nsecs_t when; int32_t deviceId; int32_t type; int32_t code; int32_t value; };当鼠标接入时EventHub 会扫描 /dev/input 下的设备节点读取其能力位classes、sources。以一只蓝牙鼠标为例日志里能看到鼠标名称MI Mouse BT3.0 分配的 DeviceId11 分配的设备节点/dev/input/event8 classes0x80000008 sources0x00002002其中 classes0x80000008 表示 INPUT_DEVICE_CLASS_CURSORsources0x00002002 对应 AINPUT_SOURCE_MOUSE。EventHub 把这些信息封装成 Device 结构交给 InputReader 做下一步转化。3.2 InputDevice 做设备归类InputReader 拿到 EventHub 的设备列表后会为每个设备创建 InputDevice并根据 classes 和 sources 决定挂载哪些 Mapper。对于 CURSOR 类设备会添加 CursorInputMapper。这一步的关键判断逻辑在 InputDevice::configure 里它会遍历所有 Mapper 工厂调用 supports 方法确认是否匹配。如果这里 classes 或 sources 配错CursorInputMapper 就不会被挂上鼠标事件自然到不了上层。所以排查时第一步就是确认设备节点的 classes 和 sources 是否符合预期。3.3 CursorInputMapper 翻译事件CursorInputMapper 是鼠标事件的核心处理者。它的 process 方法接收 RawEvent交给内部的 CursorButtonAccumulator 和 CursorScrollAccumulator 处理最后在 sync 里合成 MotionEvent。按键事件的处理在 CursorButtonAccumulator.cpp它把内核的 BTN_LEFT、BTN_RIGHT、BTN_MIDDLE 映射成按键状态// external/kernel-headers/original/uapi/linux/input-event-codes.h #define BTN_LEFT 0x110 #define BTN_RIGHT 0x111 #define BTN_MIDDLE 0x112 #define BTN_BACK 0x116 #define BTN_FORWARD 0x115CursorInputMapper::sync 里会对比前后按键状态合成 AKEY_EVENT_ACTION_DOWN 和 AKEY_EVENT_ACTION_UPvoid CursorInputMapper::sync(nsecs_t when) { int32_t lastButtonState mButtonState; int32_t currentButtonState mCursorButtonAccumulator.getButtonState(); mButtonState currentButtonState; // Synthesize key down from buttons if needed. synthesizeButtonKeys(getContext(), AKEY_EVENT_ACTION_DOWN, when, getDeviceId(), mSource, displayId, policyFlags, lastButtonState, currentButtonState); // Synthesize key up from buttons if needed. synthesizeButtonKeys(getContext(), AKEY_EVENT_ACTION_UP, when, getDeviceId(), mSource, displayId, policyFlags, lastButtonState, currentButtonState); }这里有个容易踩的坑synthesizeButtonKeys 只对 BTN_BACK0x116和 BTN_FORWARD0x115生效普通左右中键不会走这条合成路径而是通过 NotifyMotionArgs 直接上报。3.4 悬停与滑动事件鼠标的悬停和滑动走的是 MotionEvent 路径。悬停对应 AMOTION_EVENT_ACTION_HOVER_MOVE条件是 motionEventAction AMOTION_EVENT_ACTION_UP 且 mSource AINPUT_SOURCE_MOUSE。滑动对应 AMOTION_EVENT_ACTION_SCROLL判断依据是bool scrolled vscroll ! 0 || hscroll ! 0;最终 CursorInputMapper 会构造 NotifyMotionArgs通过 connection-inputPublisher.publishMotionEvent 发送出去。这个 publish 的实现在 frameworks/native/libs/input/InputTransport.cpp是事件离开 IMS、进入窗口系统的最后一环。4. 验证请求抓事件、看日志、确认成功结果4.1 用 getevent 抓原始事件最直接的验证方式是在 shell 里抓设备节点adb shell getevent -lt /dev/input/event8移动鼠标、点击按键观察输出。正常应该看到类似[ 12345.678901] EV_REL REL_X 5 [ 12345.678902] EV_REL REL_Y -3 [ 12345.679001] EV_KEY BTN_LEFT 1 [ 12345.679002] EV_SYN SYN_REPORT 0 [ 12345.679100] EV_KEY BTN_LEFT 0 [ 12345.679101] EV_SYN SYN_REPORT 0如果 getevent 有输出但应用没反应说明事件在 EventHub 之后丢了如果 getevent 都没输出问题在内核驱动或设备节点权限。4.2 用 dumpsys input 看设备归类adb shell dumpsys input在输出里找你的设备名确认 DeviceId、Classes、Sources 是否正确。重点看有没有挂上 CursorInputMapperDevice 11: MI Mouse BT3.0 Classes: 0x80000008 Sources: 0x00002002 Mappers: CursorInputMapper如果 Mappers 里没有 CursorInputMapper回到 3.2 检查 classes/sources。4.3 打开 IMS 详细日志在调试版本上可以打开 InputReader 的详细日志adb shell setprop log.tag.InputReader DEBUG adb shell setprop log.tag.CursorInputMapper DEBUG然后观察 logcatadb logcat -s InputReader CursorInputMapper EventHub正常点击时能看到按键状态变化和 NotifyMotionArgs 的构造日志。如果 sync 里 lastButtonState 和 currentButtonState 一直相等说明 CursorButtonAccumulator 没收到按键事件问题在 EventHub 到 Accumulator 之间。4.4 确认事件到达窗口最后一步验证事件是否到达应用。可以在应用侧重写 onGenericMotionEvent 和 onKeyDown打印收到的 MotionEvent 和 KeyEvent。如果 IMS 日志显示 publishMotionEvent 已调用但应用没收到问题在窗口系统的分发环节不在 IMS 本身。5. 本篇常见错排查5.1 鼠标能移动但点击无效优先查 CursorButtonAccumulator 是否收到 BTN_LEFT。用 getevent 确认内核有上报再看 InputReader 日志里 getButtonState 的返回值。如果内核有、Accumulator 没有检查 EventHub 的按键映射表是否覆盖了该 BTN 码。5.2 光标乱跳或坐标异常这类问题多半出在 REL_X/REL_Y 的缩放或加速度配置上。CursorInputMapper 会根据设备的 resolution 和配置计算最终坐标。检查 InputDevice 的配置里有没有正确的 X/Y 轴映射以及是否有加速度曲线干扰。5.3 键盘按键在特定界面失效键盘走的是 KeyboardInputMapper不是 CursorInputMapper。如果只有鼠标正常、键盘失效检查设备的 sources 是否包含 AINPUT_SOURCE_KEYBOARD以及 KeyboardInputMapper 是否被挂载。另外注意焦点窗口是否正确按键事件只会送到有焦点的窗口。5.4 BTN_BACK/BTN_FORWARD 没触发返回这两个键走 synthesizeButtonKeys 合成路径需要确认 CursorInputMapper::sync 里 lastButtonState 和 currentButtonState 的差异检测生效。如果按键状态没变化合成逻辑不会触发。用日志确认这两个 BTN 码是否被正确累加。5.5 publishMotionEvent 调用了但应用收不到这说明事件已经离开 IMS问题在 InputDispatcher 或窗口焦点。检查 dumpsys input 里的 focused window 和 focused application确认事件被分发到了正确的窗口。如果焦点窗口为空事件会被丢弃。6. 继续深入把链路验证变成日常习惯调试 IMS 输入问题最有效的方法不是盯着代码猜而是沿着 EventHub → InputDevice → CursorInputMapper → InputTransport 这条链路逐环验证。getevent 确认内核上报dumpsys input 确认设备归类logcat 确认 Mapper 处理应用侧确认最终接收。四步下来事件在哪一环丢失基本就清楚了。如果你在排查过程中需要快速理解某段 InputReader 源码或者想确认某个 BTN_ 宏的数值含义可以用模型对话入口把代码贴进去问https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。需要长期做系统层编码和 Agent 任务的话Coding Plan 的额度更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。Key 在控制台申请https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。把上面这套抓取命令和日志开关存成脚本下次遇到输入失焦直接跑一遍比翻源码快得多。