4. 事件输入流程:从硬件中断到应用层事件分发
事件输入这条路,说白了就是一条从物理世界到软件世界的通信管道。你按下一个按键,或者点一下触摸屏,这背后是硬件、内核、框架一层层接力跑完的。我最早调WMS的时候,总以为事件分发只是应用层的事,直到有一次触摸屏出现"幽灵点击"——点屏幕左边,右边却响应了。追到驱动层才发现是中断号配错了。嗯,从此我就养成了看完整条链路的习惯。
4.1 整体链路:一条事件的生命旅程
一张图比千言万语管用。下面这个svg展示了我做优化时常挂在墙上的标准流程——每个箭头都代表一次数据搬运或状态切换。
我以前踩过一个坑:某款触摸屏在低电量下从硬件中断到内核上报花了约 12ms,加上后面的队列处理直接超了 16ms 阈值,导致画面掉帧。所以每个节点的时间戳我都在日志里打了出来。
4.2 硬件中断层:第一棒谁在跑?
你按屏幕,触控 IC 会拉高一个 GPIO 电平。CPU 检测到后暂停当前任务,跳到中断服务程序。我建议你在调试时先 cat /proc/interrupts 看看中断号对不对。
关键点:
- 中断触发方式:边沿触发 vs 电平触发 —— 电平触发容易产生重复中断
- 中断共享:多设备共用中断线时,需要轮询所有注册设备
- 中断底半部(Bottom Half):用 tasklet 或 workqueue 避免在中断上下文做耗时操作
4.3 内核驱动与 Input 子系统
中断来了,驱动要做三件事:读硬件寄存器拿到坐标/按键值 -> 转换为 Linux 标准 input_event 结构体 -> 通过 input_report_key/input_report_abs 上报。你想想看,这一步如果用了 printk 来调试,整个系统的响应时间会被拖垮。我习惯用 ftrace 跟踪 input 事件时间戳。
struct input_event {
struct timeval time; // 硬件时间戳
__u16 type; // EV_KEY, EV_ABS, EV_REL ...
__u16 code; // 具体键值或坐标轴代码
__s32 value; // 0/1 或坐标值
};
内核中 /dev/input/eventX 节点的 poll/read 是同步的。多个设备同时上报时,读出的数据是交错的,必须靠 time 字段区分。我遇到过某厂商驱动忘了填充 time,导致 EventHub 解析乱序。
4.4 EventHub:用户空间的第一站
Android 里 EventHub 是一个 native 服务。它通过 epoll 同时监听所有 /dev/input/event* 节点。一旦有数据可读,它就调用 read() 获取 input_event 结构体,然后封装成 RawEvent 发给 InputReader。
曾经有个坑: 如果某个驱动输入节点在拔掉后没有正确注销,epoll 会一直返回 EPOLLHUP。我曾经被这个卡了两天,最后发现是驱动移除后没有调用 input_unregister_device。解决方案是在 EventHub 的 getEvents 中检查 fd 状态。
4.5 InputReader 和 InputDispatcher
这两个是 Android Input 系统的核心。把原始事件转换成 KeyEvent / MotionEvent,再根据窗口 Focus 分发到指定应用。
| 组件 | 职责 | 常见瓶颈 |
|---|---|---|
| InputReader | 解析 RawEvent,合成手势(如双指缩放) | 合成算法耗时,尤其在多点触控时 |
| InputDispatcher | 窗口命中判定、策略拦截(如 Slogan) | 窗口树遍历、IPC 跨进程发送 |
Dispatcher 有一个内部线程「InputDispatcherThread」。它从 InputReader 拿到的队列里取出事件,做 window handle 查找。
调优小技巧: 我建议在你自己的 WMS 进程监控 /proc/self/stat 里的 utime 和 stime。如果 stime 占比高说明内核态开销大,可能是驱动或 InputReader 的循环数太高。
4.6 从 Dispatcher 到应用层
Dispatcher 确定目标窗口后,通过 InputChannel(基于 socketpair)把事件发送给应用所在的进程。应用侧由 ViewRootImpl 的 InputEventReceiver 接收,然后通过 MessageQueue 抛到主线程。
这里有一个很容易被忽略的点:应用主线程如果正在做布局动画或绘制,MessageQueue 可能不会立即处理 input 消息。我见过一个 app 在 Activity 启动时做大量反射,导致点击事件延迟了 200ms。
// 应用端接收示意(简化)
InputEventReceiver receiver = new InputEventReceiver(inputChannel, looper);
receiver.setInputEventHandled(true);
// 在 dispatchInputEvent 里区分 KeyEvent 和 MotionEvent
4.7 总延迟模型与优化方向
把上面每段路的延迟加起来:
- 硬件中断响应: ~0.5ms - 2ms(取决于 IRQ 处理函数长度)
- 驱动上报: ~1ms - 5ms(包括读写 I²C/SPI 总线)
- EventHub + InputReader: ~2ms - 4ms(队列拷贝和手势合成)
- InputDispatcher: ~1ms - 3ms(窗口命中+IPC 传递)
- 应用接收: ~0.5ms - 10ms(取决于主线程是否空闲)
整条链路如果在 16ms 以内就是合格。我自己的经验:当超过 20ms 时,用户能明显感知到「卡」。优先优化驱动 I²C 速率(从 400kHz 提到 1MHz 通常有明显收益)和减少 InputReader 里没必要的合成算法。
一句话总结: 一条事件从你手指按下去到界面响应,经历了 7 道关卡。每条关卡都可以用 perf 或 systrace 打点,找到最慢的那段开刀。别相信玄学调优,数据说话。