触摸事件传递机制:从屏幕到应用的完整链路

做触摸优化,第一件事就是搞清楚——你点下去的那一下,到底经历了什么才跑到 App 里?

我刚开始接触这块时,以为就是屏幕检测到手指,然后直接告诉应用。后来被一个 200ms 的延迟问题折磨了两周,才老老实实把整个链路啃了一遍。说白了,这中间经过的每一层,都可能成为延迟的帮凶。

硬件到应用的完整链路

一次触摸,从物理世界进入数字世界,大致要经过 5 个关卡:

  1. 屏幕(硬件层)——电容屏检测到手指触碰,产生电信号
  2. 驱动(内核态)——驱动将电信号转换成标准化的触摸数据包
  3. 内核(Linux Input 子系统)——将数据包上报到 input 事件队列
  4. 应用框架(Framework)——InputReader 读取事件,InputDispatcher 分发
  5. 应用层——View 树的事件分发与消费

关键认知:每一层都有缓冲区、队列、调度策略。任何一个环节出现拥塞,触摸响应就会变慢。我见过最离谱的案例,是驱动层上报频率被降到了 60Hz,结果触摸采样直接砍半。

下面这张图,是我每次培训必放的。它把整个链路画成了一目了然的流水线:

触摸事件完整链路图 触摸事件从硬件到应用的完整链路 屏幕 电容检测 → 电信号 驱动 信号 → 数据包 内核 Input 子系统 应用框架 InputReader/Dispatcher 应用 ⚠ 每一层都可能引入延迟:驱动采样率、内核调度、Framework 队列、主线程阻塞 各层关键延迟因素 • 屏幕:扫描频率(60Hz/120Hz/144Hz)、触控 IC 处理时间 • 驱动:上报速率(Report Rate)、中断处理优先级 • 内核:input 事件队列长度、CPU 调度延迟 • Framework:InputReader 轮询间隔、Dispatcher 分发策略 • 应用:View 树遍历、事件处理回调耗时

事件分发流程:InputReader → InputDispatcher → 应用

Framework 层是整个链路中最复杂、也最容易被忽视的部分。我把它拆成三个角色来讲:

角色 职责 运行线程 常见延迟原因
InputReader 从 /dev/input/ 读取原始事件,解析为 InputEvent InputReader 线程 读取频率低、事件队列积压
InputDispatcher 将事件分发给目标窗口,处理策略(如触摸消抖) InputDispatcher 线程 窗口查找耗时、ANR 超时机制
应用主线程 接收事件并执行 View.dispatchTouchEvent() 主线程(UI 线程) 主线程阻塞、布局复杂

InputReader 的工作细节

InputReader 本质上是一个轮询器。它不断从内核的 input 设备节点读取事件,然后做两件事:

  • 事件解析:把原始坐标、压力、时间戳转成 MotionEvent
  • 策略过滤:比如虚拟按键映射、边缘防误触

我的经验:有一次排查延迟,发现 InputReader 的轮询间隔被改成了 16ms(约 60Hz),但屏幕是 120Hz 的。这意味着每两次触摸采样之间,Reader 只读一次。后来把轮询间隔降到 4ms,延迟直接降了 8ms。这个参数在 Android 源码的 InputReader.cpp 里,叫 mEventHub->getNextEvent() 的超时时间。

InputDispatcher 的分发策略

InputDispatcher 拿到事件后,不会直接丢给应用。它会先做几件事:

  1. 查找目标窗口——根据触摸坐标找到对应的 Window
  2. 检查窗口状态——是否可接收事件、是否处于 ANR 倒计时
  3. 放入窗口的事件队列——每个窗口有自己的 InputEventQueue
  4. 通知应用取事件——通过管道或共享内存机制

这里有个坑:如果目标窗口正在处理上一个事件,Dispatcher 会等待。我曾经遇到一个案例,某个页面在 onTouchEvent 里做了大量计算,导致后续事件排队,触摸滑动明显卡顿。解决方案很简单——把计算移到子线程。

注意:InputDispatcher 有一个「超时机制」。如果应用在 5 秒内没有消费事件,系统会认为应用 ANR。但反过来,如果应用处理得太快,Dispatcher 也可能因为窗口切换而丢事件。嗯,这里要平衡好。

应用层的事件分发

事件到达应用层后,走的是 View 树的分发流程:

Activity.dispatchTouchEvent()
  └─ ViewGroup.dispatchTouchEvent()
       ├─ onInterceptTouchEvent()  // 是否拦截
       └─ child.dispatchTouchEvent()
            └─ View.onTouchEvent()  // 最终消费

这个流程看似简单,但每个方法都可能成为延迟点:

  • onInterceptTouchEvent 如果写得复杂,每次触摸都要遍历子 View
  • ViewGroup 的触摸目标查找 如果层级深,遍历耗时线性增长
  • onTouchEvent 回调 如果做了 I/O 或网络,直接卡死主线程

一句话总结:触摸延迟不是单一环节的问题。屏幕快了,驱动跟不上;驱动快了,Framework 分发不过来;Framework 优化了,应用主线程又卡住了。排查时一定要从链路视角去看,而不是只盯着应用层。

我个人习惯在排查延迟时,先抓一份 systrace / perfetto 看全局。从 InputReader 的读取时间戳,到应用层的 dispatchTouchEvent,每个环节的耗时一目了然。后面几章我会详细讲怎么用这些工具定位具体瓶颈。