1. 从一次鼠标失焦说起:IMS 输入链路到底怎么走
Android 的输入子系统(IMS,Input 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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。你可以把它当成一个随时在线的“输入子系统答疑助手”:把 CursorInputMapper::sync 的代码片段贴进去,让它帮你逐行解释 synthesizeButtonKeys 的触发条件;或者把 getEvents 抓到的 RawEvent 结构体丢进去,确认 type/code/value 的含义。
如果你更习惯在编辑器里直接对话,可以用模型对话入口:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。需要长期做编码和 Agent 类任务的话,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。申请 Key 在控制台完成:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。Claude Code 相关配置参考 https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 。
注意:TaoToken 在这里的角色是辅助理解源码和日志,不替代你对 IMS 链路的实际验证。真正的定位仍然要靠 getevent、dumpsys input 和日志。
3. 可复制配置:EventHub 到 CursorInputMapper 的链路骨架
3.1 EventHub 采集原始事件
EventHub 是 IMS 的最底层,负责打开 /dev/input/eventX 节点并读取 RawEvent。它的核心结构体定义在 EventHub.h:
struct 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 分配的 DeviceId:11 分配的设备节点:/dev/input/event8 classes=0x80000008 sources=0x00002002其中 classes=0x80000008 表示 INPUT_DEVICE_CLASS_CURSOR,sources=0x00002002 对应 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_UP:
void 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_BACK(0x116)和 BTN_FORWARD(0x115)生效,普通左右中键不会走这条合成路径,而是通过 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 是否正确。重点看有没有挂上 CursorInputMapper:
Device 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然后观察 logcat:
adb 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_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。需要长期做系统层编码和 Agent 任务的话,Coding Plan 的额度更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。Key 在控制台申请:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入细节看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
把上面这套抓取命令和日志开关存成脚本,下次遇到输入失焦直接跑一遍,比翻源码快得多。