Appearance
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/CursorInputMapper | NotifyMotionArgs(含 pointer 数组) |
| 实体/虚拟键盘 | KeyboardInputMapper | NotifyKeyArgs(含 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 全面转向手势导航后,输入侧多了三类事:
- 边缘截胡:左/右边缘 down 被 SystemUI 手势层(spy 窗)跟踪,达到阈值即把后续事件流改道给返回/桌面手势,应用收到 CANCEL;
- 手势区与 insets 联动:
systemGestures/mandatorySystemGesturesinsets(第 07 页)决定哪些边缘可被截胡;OEM 可配置手势区形状,事件仲裁逻辑不变; - 预测式回退(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组 + atraceinput_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」——系统截胡是设计,应用应通过
systemGesturesinsets 让路。
十一、版本注记
- 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):预测式返回手势默认化;
MotionPredictionAPI 引入触控预测。 - 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