26、输入系统调试:InputReader、InputDispatcher、触摸事件、按键映射

输入系统,说白了就是Android怎么知道你点了哪里、按了什么键。我刚开始接触这部分时,觉得它就是个“事件搬运工”,后来才发现,这里面的门道深着呢。你想想看,从手指触摸屏幕,到应用响应,中间要经过多少层?每一层都可能出问题。

今天我们就来聊聊输入系统的两个核心组件:InputReaderInputDispatcher。再加上触摸事件和按键映射这两个高频调试场景。嗯,这些都是我平时排查问题时的“老熟人”了。

输入系统的整体架构

先看一张图,把整体脉络理清楚。我个人习惯,遇到复杂系统先画图,不然脑子容易乱。

Android 输入系统核心流程 硬件设备(触摸屏/键盘) InputReader InputDispatcher App 原始事件 解析/转换/校准 分发/排队/策略 消费 InputReader 内部 EventHub(读取原始事件) InputMapper(转换/校准) 生成 NotifyArgs InputDispatcher 内部 InputChannel(接收事件) QueuedInputDispatcher(排队) 发送到目标窗口

从图上可以看得很清楚:硬件产生原始事件,InputReader负责读取和解析,InputDispatcher负责分发。这两个家伙分工明确,各管一摊。

InputReader:从硬件到事件的“翻译官”

InputReader运行在InputReader线程中。它的核心工作就是轮询EventHub,读取原始输入事件,然后转换成Android能理解的KeyEvent或MotionEvent。

我遇到过一个问题:某款平板触摸不灵敏,点上去没反应。抓了log一看,InputReader一直在报“readEvents: timeout”。后来发现是触控IC的I2C总线被其他设备占用了,导致读取超时。嗯,这种问题光看应用层是看不出来的。

EventHub的读取机制

EventHub通过epoll监听所有输入设备节点(/dev/input/event*)。一旦有数据,就调用readEvents()读取原始input_event结构体。

// 原始input_event结构
struct input_event {
    struct timeval time;  // 时间戳
    __u16 type;           // 事件类型 (EV_KEY, EV_ABS, EV_REL...)
    __u16 code;           // 按键码或轴编号
    __s32 value;          // 值 (按下/抬起/坐标)
};

这里有个坑:时间戳。我曾经排查过一个游戏触控延迟问题,发现InputReader读到的时间戳比实际时间晚了200ms。原因是驱动层上报的时间戳用的是系统启动时间,但上层期望的是CLOCK_MONOTONIC。这种时间偏差,在普通应用中感觉不到,但在高帧率游戏里就非常明显。

注意:如果遇到触摸延迟,先检查dumpsys input中的事件时间戳。如果时间戳异常,问题大概率出在驱动或HAL层,而不是InputReader本身。

InputMapper:事件的“翻译官”

InputReader内部有一组InputMapper,负责把原始事件转换成标准化的Android事件。常见的Mapper有:

  • KeyboardInputMapper:处理按键事件,负责按键映射
  • TouchInputMapper:处理触摸事件,负责坐标转换、多点触控
  • JoystickInputMapper:处理游戏手柄事件

每个Mapper都对应一个设备类型。InputReader根据设备信息(比如idc配置文件)选择合适的Mapper。

InputDispatcher:事件分发的“交通警察”

InputDispatcher运行在单独的Dispatcher线程中。它从InputReader那里拿到事件后,要决定发给哪个窗口、什么时候发、怎么发。

说白了,InputDispatcher就是个“调度中心”。它维护着所有窗口的输入通道(InputChannel),以及窗口的焦点状态。

分发策略

InputDispatcher的分发策略,我总结为三步:

  1. 找目标:根据触摸坐标找到对应的窗口(通过WindowManagerService的窗口层级信息)
  2. 排队:把事件放入目标窗口的输入队列
  3. 发送:通过InputChannel把事件发送到应用进程

这里有个关键点:ANR。如果应用在5秒内没有消费完上一个事件,InputDispatcher就会触发ANR。我在项目中遇到过一种情况:某个页面加载时主线程卡住了,但触摸事件已经发过去了。结果就是用户点了一下,5秒后弹出ANR对话框。嗯,这种问题其实可以通过优化应用启动逻辑来避免。

调试技巧:使用dumpsys input可以查看当前InputDispatcher的状态,包括等待队列、正在处理的事件、每个窗口的输入通道状态。这是排查输入ANR的第一利器。

触摸事件调试

触摸事件调试,说白了就是搞清楚“用户点的是A,但应用收到的是B”这种问题。我把它分为两类:坐标不准和事件丢失。

坐标不准

坐标不准通常和屏幕校准有关。Android通过idc文件(Input Device Configuration)来配置设备的参数。比如:

# 一个典型的触摸屏idc配置
touch.deviceType = touchScreen
touch.orientationAware = 1
touch.size.calibration = diameter
touch.pressure.calibration = amplitude
touch.pressure.scale = 0.005

如果坐标不准,先检查idc文件是否正确。我遇到过一台设备,触摸坐标总是偏右50像素。查了半天,发现是touch.orientationAware设置成了0,导致旋转后坐标没有做相应转换。

事件丢失

事件丢失更隐蔽。用户明明滑动了,但应用只收到部分事件。这种情况,我建议先抓getevent日志:

# 抓取原始输入事件
adb shell getevent -lt /dev/input/event2

如果getevent显示事件是连续的,但应用收不到,那问题出在InputDispatcher或应用自身。如果getevent本身就有丢事件,那问题在驱动层。

我曾经遇到一个案例:某款手机在快速滑动时,触摸事件会“跳点”。用getevent一看,发现驱动上报的事件间隔不均匀,有时两帧之间差了30ms。这就是驱动层的采样率不够,或者中断处理有延迟。

按键映射调试

按键映射,说白了就是把物理按键的扫描码(scancode)转换成Android的按键码(keycode)。这个映射关系定义在.kl文件(Key Layout)中。

kl文件示例

# 一个标准的kl文件片段
key 114   VOLUME_DOWN
key 115   VOLUME_UP
key 116   POWER
key 158   BACK
key 172   HOME
key 217   MENU

左边是扫描码,右边是Android按键码。如果按键没反应,先检查kl文件是否存在、是否被正确加载。

我遇到过一个问题:某款外接键盘的“ESC”键按了没反应。用getevent查看,发现扫描码是1,但kl文件里没有定义这个扫描码。加上key 1 ESC后,问题解决。

调试命令汇总:
  • adb shell getevent:查看原始输入事件
  • adb shell dumpsys input:查看InputDispatcher状态
  • adb shell dumpsys window windows:查看窗口层级
  • adb shell cat /proc/bus/input/devices:查看输入设备列表
  • adb shell ls /system/usr/keylayout/:查看按键映射文件

实战:一个触摸不灵的排查过程

最后,分享一个我实际排查过的案例。用户反馈:手机横屏玩游戏时,触摸偶尔失灵。

排查步骤:

  1. 抓取dumpsys input:发现事件正常上报,没有丢事件
  2. 检查窗口焦点:发现焦点在游戏窗口上,没问题
  3. 查看事件时间戳:发现事件时间戳有跳跃,有时两帧间隔100ms
  4. 抓取getevent:确认驱动上报的事件本身就有间隔不均的问题
  5. 分析驱动日志:发现触控IC在横屏模式下,因为散热问题导致采样率下降

最终结论是硬件问题,不是软件能解决的。但通过这个排查过程,我排除了InputReader和InputDispatcher的嫌疑,帮硬件团队缩小了范围。

嗯,输入系统调试就是这样,一层一层往下挖。从应用层到框架层,再到驱动层,每个环节都可能出问题。掌握了InputReader和InputDispatcher的工作原理,你就能快速定位问题出在哪一层。


公众号:蓝海资料掘金营,微信deep3321