Skip to content

09 输入系统:从手指落到屏幕的传送带 ​

本页导读:InputFlinger(输入子系统)把「手指按在屏幕第几个像素」翻译成「哪个窗口收到哪个 MotionEvent/KeyEvent」,路上还要处理设备分类、手势监视、焦点仲裁、超时 ANR。本页按数据流向三段拆解:EventHub(内核设备事件)、InputReader(原始→语义)、InputDispatcher(分发与仲裁),再补上手势导航时代的新变化。基线 Android 16(API 36)。


一、全景管道 ​

text
内核输入设备(/dev/input 系列 event 节点)
  → EventHub          # 读设备、inotify 热插拔、原始事件(struct input_event)
  → InputReader       # 设备分类 + Mapper 链 → NotifyKeyArgs/NotifyMotionArgs
  → InputDispatcher   # 窗口仲裁、焦点、手势、超时、ANR 上报
  → 目标窗口(应用 ViewRootImpl 的输入通道)

Java 侧门面是 InputManagerService(frameworks/base/services/core/java/com/android/server/input/InputManagerService.java):WMS 把窗口清单/焦点交给它,它经 JNI 下到 native InputManager;framework 门面是 frameworks/base/core/java/android/hardware/input/InputManager.java。

三段管道各有一个「最怕的问题」:

段回答的问题最怕
EventHub有哪些设备、事件进没进来驱动/字典缺失,整段静默
InputReader这堆裸事件是什么语义分类错、坐标换算错
InputDispatcher该给哪个窗、超时了吗焦点与 Z 序仲裁争议

记住这张表,输入问题就没地方藏。

二、EventHub:设备与原始事件 ​

  • 启动时 opendir(/dev/input) + epoll/inotify:热插拔设备自动纳入(蓝牙键盘、USB 手柄免重启);
  • 设备字典有加载优先级:/vendor/usr 覆盖 /system/usr 同名文件,OEM 用它做机型适配而不动平台文件;热插拔设备(尤其蓝牙)匹配不到定制 .kl 时退回默认 qwerty 映射,「外接键盘个别键错乱」常源于此;
  • 每个设备加载两份「字典」:.kl(keylayout,扫描码→键值)与 .idc(输入设备配置),搜索路径含 /vendor/usr/keylayout、/system/usr/keylayout,OEM 常在此适配异形键盘(AOSP 自带 qwerty.kl、Virtual.kl 等示例,路径以设备为准);
  • 输出统一的 RawEvent(type/code/value + 时间戳),MT_* 多点触控协议在这里仍是裸事件,尚未成「手」。

多点触控的「成手」发生在 Reader:MT 协议 A/B 用 slot 追踪每根手指,slot→pointerId 的映射表维护在 MultitouchResolver;协议 B(现代触摸屏主流)每次上报完整帧,丢帧容错更好。absinfo(ABS_MT_* 的范围信息)决定坐标换算,这就是「不同分辨率屏复用同一套应用逻辑」的底层契约。

三、InputReader:把裸事件翻译成语义 ​

设备先分类(KEYBOARD/TOUCH/CURSOR/SWITCH/JOYSTICK…),再挂 Mapper 链:

设备类型Mapper产出
触摸屏MultiTouchMapper/CursorInputMapperNotifyMotionArgs(含 pointer 数组)
实体/虚拟键盘KeyboardInputMapperNotifyKeyArgs(含 repeat 计时)
鼠标/触控板CursorInputMapper相对/绝对运动 + 按钮键
霍尔/盖子开关SwitchInputMapper开关事件(可触发亮灭屏策略,第 11 页)
手柄/摇杆Joystick/TouchInputMapper 家族轴向/球心

坐标换算契约:驱动上报的 ABS 原始值经 absinfo 范围缩放为显示坐标,再乘显示变换矩阵(旋转/缩放/折叠分屏)——「横屏坐标错乱」类问题,第一嫌疑是显示矩阵与触摸矩阵不同步。

要点:虚拟键盘(软键盘,VirtualKeyBoard)由 SystemUI/输入法窗口以 InputManager.injectInputEvent 通路注入,与物理键盘在 Dispatcher 汇流;source 字段(TOUCHSCREEN/KEYBOARD/STYLUS/MOUSE)从这里开始一路跟随事件,是应用区分「手指还是手写笔」的依据(MotionEvent.getToolType)。

键盘事件的两件小事:

  • 长按/重复由 Reader 合成(按下后按 repeat 间隔重发,物理键盘靠内核 autorepeat 语义补齐),应用侧只需认 KeyEvent.getRepeatCount();
  • meta state(Shift/Ctrl/Alt 组合态)在 Mapper 内聚合,注意软键盘注入与物理修饰键组合时的同步问题——「外接键盘 + 软键盘」混用是历史重灾区。

四、InputDispatcher:仲裁的艺术 ​

分发核心问题只有一个——这个事件给谁。规则按事件类型分道:

  • 触摸类:down 落在哪个窗就一路给谁(手势期锁定,action_move 不会换窗),窗口重叠按 Z 序命中最上可触者;边缘有 slippery 处理(手指滑出窗后继续跟一段时间);
  • 按键类:只给焦点窗;焦点变更由 WMS 决定(第 07 页),Dispatcher 执行;
  • 焦点抢占:新窗请求焦点时 Dispatcher 会「交还焦点」——应用侧体现为 onWindowFocusChanged(false),正在 down 的触摸序列会被取消(ACTION_CANCEL);
  • 手势监视器(Android 12+ spy):SystemUI 边缘手势层以窥视方式同步收到事件流副本,可决定截胡(返回手势接管)——系统级「先听再定」模式。

通道实现值得记:每个窗口对应一条 InputChannel(socketpair),Dispatcher 写入、应用侧 ViewRootImpl 经 native 输入事件接收器读出并入主线程队列(第 05 页的消息循环)。输入走 socket 不走 Binder,Binder 忙不会卡输入——反过来主线程忙会卡输入(ANR 来源)。

多屏(折叠/桌面模式时代)的路由规则:事件按 displayId 归属窗口集,焦点是「每显示区一个焦点窗」;外接屏点击打到主屏窗口是配置问题(dumpsys input 按 display 分段可见)。折叠态切换时 WMS 会重算窗口归属与焦点,瞬间的「点了不响应」多出自这里的竞态窗口期。

五、超时与 ANR:5 秒的由来 ​

Dispatcher 给每个已投递未消费的输入事件计时:

  • 典型超时 5s(按键分发在 Android 12+ 放宽至 10s 量级;常量 DEFAULT_INPUT_DISPATCHING_TIMEOUT 等,以 AOSP 当前为准);
  • 超时 → 回调 InputManagerService.InputManagerCallback.notifyInputEventTimedOut → 汇入 AMS 的 ANR 流程(第 06 页 AnrHelper),Subject 常为 Input dispatching timed out,报告里会附「哪个窗、等了多久的哪个事件」;
  • 常见误读:FocusedWindow: ... not responding 的窗未必是罪魁——主线程被 Binder 回包/锁拖住的连锁卡同样表现于此。

两个配套机制:焦点窗进入「等结果」状态时,Dispatcher 会为新到达的事件建立 wait queue(超时同样计入 ANR 账);ANR 上报后焦点不变但后续事件延迟派发,体感为「点了没反应」而不是崩溃——这解释了为什么 ANR 对话框出现前常有几秒「假死」。

报告读法示例:Reason: Input dispatching timed out - Waiting Channel(...) 说明事件已投递、应用未在时限内回执;若伴随 Window(...) is not ready 则窗口还没注册就绪,责任在窗口流程而不是应用主线程——同一条 ANR,责任方可能完全不同。

六、按键的「上层拦截」:PhoneWindowManager ​

键盘事件进入应用前还有一道系统级关卡:InputManagerPolicy/SystemUI 侧 PhoneWindowManager 在入队前与分发前各有一次拦截点(interceptKeyBeforeQueueing/interceptKeyBeforeDispatching,机制名以版本为准):HOME、电源、音量、通知栏手势键在这里被系统消化——这也是「自定义键行为要先过系统」的原因,以及 dumpsys input 看不到某些键送达窗口的解释。

键值映射链备忘:内核扫描码 → .kl 的 keycode →(可选 .kcm 字符映射)→ KeyEvent;虚拟键盘直接产 keycode。dumpsys input 里能看到每个设备的分类与映射字典摘要。

七、手势导航下的输入变化 ​

Android 10~16 全面转向手势导航后,输入侧多了三类事:

  1. 边缘截胡:左/右边缘 down 被 SystemUI 手势层(spy 窗)跟踪,达到阈值即把后续事件流改道给返回/桌面手势,应用收到 CANCEL;
  2. 手势区与 insets 联动:systemGestures/mandatorySystemGestures insets(第 07 页)决定哪些边缘可被截胡;OEM 可配置手势区形状,事件仲裁逻辑不变;
  3. 预测式回退(Android 14+):返回手势在「回退动画开始前」把控制权交给应用动画(OnBackInvokedDispatcher),输入层负责在动画期间维持事件流的确定性——细节见官方预测式返回文档。

触控笔/手写板另有一族行为:getToolType 区分手指/笔、悬停(HOVER)事件不抢焦点、悬浮菜单;这是很多手写 App「时灵时不灵」的排查起点。

应用侧的手势识别(GestureDetector/GestureDetectorCompat)跑在窗口收到的 MotionEvent 流之上,属于「二级加工」:VelocityTracker、SlidingPaneLayout 类组件都在用户态;系统截胡(第六节)发生时它们只会看到 ACTION_CANCEL——写「下拉刷新 + 侧滑返回」冲突处理时,这是第一需要接受的事实。

八、注入与调试通路 ​

  • injectInputEvent:系统权限或 adb(input tap/swipe/keyevent 即它的小封装);注入事件带 INJECTED 标记,安全应用(密码框)可拒绝注入事件——自动化测试被拒就是这条防线;
  • getevent/sendevent:绕开 framework 直接读写 /dev/input 节点,验证「事件到底有没有到系统层」的最短路径;
  • dumpsys input:窗口清单(含 frame/visibility/pause)、焦点窗、监视器列表、最近 ANR 的等待原因,是输入问题的第一现场;
  • 事件流可视化:perfetto 的 input 数据源(ftrace input 组 + atrace input_event)可回放事件时间线(数据源名以工具版本为准);
  • dumpsys input 阅读顺序建议:先看 Windows 段(清单/焦点/paused),再看 Monitoring 段(spy 窗),最后 Timing 与最近 ANR 段;三段对齐前,别急着改代码。

代码地图(AOSP 主干,native 在 frameworks/native/services/inputflinger/):

text
EventHub.cpp                 # 设备与原始事件
reader/                      # InputReader 与 Mapper 家族
dispatcher/                  # InputDispatcher、通道与仲裁(目录化后的新布局)
InputManager.cpp / JNI       # system_server 门面之下的 C++ 总入口
frameworks/base/core/java/android/hardware/input/  # Java 门面与策略接口

九、实战判例 ​

判例表的用法:先用症状锁行,再沿「getevent → dumpsys input → 应用日志」三级取证,任何一级异常即可就地收敛。

症状判读路径
触屏完全失灵getevent 有无原始事件 → EventHub 设备字典 → 驱动/供电
部分区域失灵dumpsys input 的窗口 frame 异常(悬浮窗/隐形窗吃掉触摸)
能点不能滑手势截胡与 systemGestures 区冲突,对照边缘设置
游戏方向键失灵一半KeyEvent repeat/长按由 KeyboardInputMapper 合成,焦点中途被抢会断流
外接键大小写混乱.kl/.idc 与 modifier 映射;dumpsys input 查设备分类
手写笔延迟悬停/预测路径:MotionPrediction(Android 14+)与厂商滤波层叠加
注入被吞安全窗口拒绝注入;input -d <display> 确认多屏注入目标
双击/长按失灵Reader 的 repeat/timeout 配置与 .kl tracking 项
折叠屏切换后焦点丢窗口重排期与焦点迁移竞态,看 dumpsys input 两段快照对比
触摸漂移/跳点MT slot 映射错乱(驱动固件问题居多),getevent 先自证

十、误区清单 ​

  • 「触摸事件给最上层窗口」——只在可触区与 Z 序联合命中时成立,FLAG_NOT_TOUCHABLE 的浮层形同空气;
  • 「ANR 就是应用崩了」——输入 ANR 只说明通道回执超时,进程往往活得好好的;
  • 「getevent 没数据 = 硬件坏」——也可能是节点权限/SELinux 拒读,先 ls -Z /dev/input;
  • 「注入测试通过 = 真机没问题」——注入绕过 EventHub/Reader 整段,驱动层问题测不出来;
  • 「手势导航吃掉边缘滑动是 bug」——系统截胡是设计,应用应通过 systemGestures insets 让路。

十一、版本注记 ​

  • Android 10(API 29):手势导航引入;MotionEvent 携带更完整的工具类型/轴信息。
  • Android 12(API 31):spy 监视窗;按键分发 ANR 阈值放宽(量级变化,常量以 AOSP 为准)。
  • Android 13(API 33):键盘类设备(外接键盘桌面化)体验强化,IME 与键事件路由继续打磨。
  • Android 12(API 31):触控笔悬停/橡皮筋等工具类型行为细化(hover 路由调整,细节以 AOSP 为准)。
  • Android 14(API 34):预测式返回手势默认化;MotionPrediction API 引入触控预测。
  • Android 15~16(API 35~36):大屏/键鼠桌面化路径继续推进;触控预测与手写笔链路细化;输入子系统骨架(三段管道)保持十年稳定。

本页小结 ​

十二、自测清单 ​

  • 能画出三段管道,并说出每段最典型的故障形态;
  • 能解释输入通路为什么用 socketpair 而不是 Binder;
  • 能用 dumpsys input 把「点击无响应」定性为:窗口未就绪 / 焦点不在 / 主线程忙;
  • 能说明手势截胡发生时,应用为什么会收到 ACTION_CANCEL;
  • 能回答 getevent 与 input 注入命令分别绕过/经过了哪几层;
  • 能解释 MT 协议 slot 与 pointerId 的关系及出错症状;
  • 能说清 interceptKey 两道关卡与 App 内 dispatchKeyEvent 的三层顺序;
  • 拿到一条新的 ANR 报告,能独立完成责任定性练习(第五章报告读法)。

再进一步:把第 05 页的消息队列与本页第 81 行「输入入队」连起来读一遍——两条传送带在应用主线程交汇,Android 前端卡顿的真相九成藏在这次交汇里。

本页小结:输入是一条三段传送带:EventHub 管「有什么」,InputReader 管「是什么」,Dispatcher 管「给谁」。触摸认窗口、按键认焦点、手势先窥视后截胡;ANR 的 5 秒钟在 Dispatcher 处计时,但被告席上站着的通常是应用主线程。排障顺序永远:getevent → dumpsys input → 应用侧。

参考 ​

  • 触摸与手势官方指南:developer.android.com/guide/topics/gestures
  • InputDispatcher 源码:cs.android.com/android/platform/frameworks/native/+/main/services/inputflinger/dispatcher/InputDispatcher.cpp
  • InputReader 源码:cs.android.com/android/platform/frameworks/native/+/main/services/inputflinger/reader
  • 预测式回退:developer.android.com/guide/ui/predictive-back-gesture
  • 输入子系统(AOSP):source.android.com/docs/core/inputs
  • MotionEvent 参考:developer.android.com/reference/android/view/MotionEvent
  • ViewRootImpl 输入接收(应用侧终点):cs.android.com/android/platform/frameworks/base/+/main/core/java/android/view/ViewRootImpl.java

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处