一、触摸系统概述:AOSP输入系统架构全景

各位同学,欢迎来到《AOSP触摸与输入系统移植实战课程》。我是你们的老朋友,一个在Android底层摸爬滚打了多年的工程师。

今天咱们聊点硬核的——触摸系统。说实话,我刚接触AOSP输入系统那会儿,也被它绕得头晕。EventHub、InputReader、InputDispatcher……这些名字听起来就像三个互不相干的黑盒子。但等你真正搞懂了它们之间的协作关系,你会发现,嗯,其实没那么复杂。

先看一张全景图。整个输入系统,说白了就是一条流水线:

  1. 硬件层:触摸屏、按键、鼠标等物理设备产生原始信号
  2. 内核层:Linux Input子系统将信号封装成标准事件(struct input_event)
  3. EventHub:从设备节点读取原始事件
  4. InputReader:解析原始事件,转换成Android内部事件
  5. InputDispatcher:把事件分发给目标窗口
  6. 应用层:View.onTouchEvent() 等回调处理

你想想看,从你手指碰到屏幕,到App收到MotionEvent,中间经历了这么多环节。任何一个环节出问题,触摸就卡顿、断触或者干脆没反应。我在项目中遇到过好几次,明明硬件没问题,就是EventHub读不到事件,最后发现是selinux权限没配好——这种坑,踩一次就记住了。

核心要点:AOSP输入系统是一个典型的生产者-消费者模型。EventHub是生产者,InputReader是加工者,InputDispatcher是分发者。三者通过管道和共享内存通信。

二、EventHub与InputReader工作原理

2.1 EventHub:最底层的“搬运工”

EventHub,名字起得很直白——事件集线器。它的工作就是盯着 /dev/input/ 目录下的所有设备节点。

我习惯把EventHub比作一个“守门人”。它用epoll机制同时监听多个设备文件。一旦有数据可读,立刻通知上层。

// EventHub的核心循环(简化版)
while (true) {
    int ret = epoll_wait(mEpollFd, events, EPOLL_MAX_EVENTS, -1);
    for (int i = 0; i < ret; i++) {
        if (events[i].data.fd == mWakeReadPipeFd) {
            // 唤醒事件,处理命令
        } else {
            // 读取设备节点数据
            struct input_event buffer[INPUT_EVENT_BUFFER_SIZE];
            int count = read(events[i].data.fd, buffer, sizeof(buffer));
            // 将原始事件放入队列
            mEventBuffer.push_back(buffer, count);
        }
    }
}

这里有个细节要注意:EventHub读取的是Linux内核的input_event结构体,包含type、code、value三个字段。比如触摸屏按下,type是EV_ABS,code是ABS_MT_POSITION_X,value是x坐标值。

避坑指南:我曾经在移植一款触控屏时,发现EventHub能读到事件,但坐标值始终不对。查了两天才发现,是设备树的input上报格式配置错了——ABS_MT_POSITION_X的最大值设成了4095,但屏幕分辨率是1920。EventHub读到的是原始值,不会帮你做归一化。这个坑,大家移植时一定要检查。

2.2 InputReader:从“原始数据”到“内部事件”

EventHub把原始事件丢给InputReader,InputReader就开始干活了。它的核心任务就两个:

  • 解析:把input_event转换成Android的RawEvent
  • 合成:把多个原始事件合成一个完整的MotionEvent或KeyEvent

举个例子,触摸屏上报一个手指按下,内核会连续发送多个input_event:

// 内核上报的原始事件序列
EV_ABS / ABS_MT_SLOT / 0          // 当前操作的是第0个触点
EV_ABS / ABS_MT_TRACKING_ID / 123 // 触点的唯一ID
EV_ABS / ABS_MT_POSITION_X / 500  // X坐标
EV_ABS / ABS_MT_POSITION_Y / 300  // Y坐标
EV_SYN / SYN_REPORT / 0           // 同步信号,表示一次完整上报结束

InputReader收到这些事件后,会做两件事:

  1. 根据ABS_MT_SLOT和ABS_MT_TRACKING_ID判断是新增触点还是移动
  2. 等到SYN_REPORT信号,才把这一批事件打包成一个MotionEvent

嗯,这里要注意。如果触控屏驱动没有正确发送SYN_REPORT,InputReader就会一直等,导致触摸事件延迟甚至丢失。我在调试一款国产触控IC时就遇到过,驱动工程师忘了加SYN_REPORT,结果触摸响应慢了200ms——用户体验极差。

个人经验:调试触摸系统时,我建议先用getevent命令看原始事件流。如果getevent能正常显示坐标,但App收不到,问题大概率出在InputReader或InputDispatcher。如果getevent都看不到数据,那就要查EventHub和内核驱动了。

三、InputDispatcher事件分发流程

InputReader把事件准备好后,通过InputChannel发送给InputDispatcher。InputDispatcher是整个输入系统的“交通警察”,它决定事件该发给谁。

3.1 分发策略

InputDispatcher维护了一个窗口列表,每个窗口都有一个InputChannel。分发时,它根据以下规则选择目标:

  • 触摸事件:发给当前触摸点下方的窗口(通过hit test计算)
  • 按键事件:发给当前获得焦点的窗口
  • 轨迹球事件:发给当前获得焦点的窗口

分发流程大致如下:

// InputDispatcher分发核心逻辑(伪代码)
void InputDispatcher::dispatchOnce() {
    // 1. 从队列取出一个事件
    EventEntry* entry = mInboundQueue.dequeue();
    
    // 2. 找到目标窗口
    Vector<InputTarget> targets;
    findTargets(entry, &targets);
    
    // 3. 遍历所有目标,发送事件
    for (auto& target : targets) {
        // 通过InputChannel发送
        status_t status = target.inputChannel->sendMessage(entry);
        if (status != OK) {
            // 发送失败,可能是窗口已销毁
            handleTargetDead(target);
        }
    }
    
    // 4. 等待应用处理完成(同步事件需要)
    if (entry->needWait) {
        waitForAppProcess(entry);
    }
}

3.2 同步与异步

你可能会问:为什么触摸事件有时候会丢?其实跟同步机制有关。

InputDispatcher支持两种模式:

模式 说明 典型场景
同步 等待应用处理完上一个事件,才发下一个 按键事件(防止乱序)
异步 不等待,直接发送 触摸事件(追求低延迟)

但异步模式有个隐患:如果应用处理速度跟不上,事件会堆积在InputChannel的缓冲区里。缓冲区满了,新事件就会被丢弃。这就是为什么有些手机在快速滑动时会断触——不是硬件问题,是软件处理不过来。

关键点:InputDispatcher的ANR机制也是基于这个同步等待。如果应用超过5秒没有处理完上一个事件,InputDispatcher就会认为应用ANR,弹出“应用无响应”对话框。我在项目中遇到过,某个第三方输入法在处理按键事件时卡住了,导致整个系统触摸都卡死——因为InputDispatcher被阻塞了。

3.3 事件注入与拦截

InputDispatcher还支持事件注入,比如通过adb shell input tap x y 模拟点击。这个功能在自动化测试中很常用。

注入事件的流程是:

  1. EventHub收到注入命令(通过mWakeReadPipeFd唤醒)
  2. InputReader解析注入事件
  3. InputDispatcher正常分发

但要注意,注入事件和真实事件在InputDispatcher内部是区分处理的。注入事件会带上一个INJECTION标志,系统可以根据这个标志做权限校验。

避坑指南:我曾经在移植一个车载系统时,需要支持方向盘按键控制中控屏。但方向盘按键是通过CAN总线转GPIO模拟的,上报的事件类型和标准按键不一样。我花了整整一周,才在InputReader里加了一个映射层,把非标事件转成标准KeyEvent。如果你也遇到类似需求,建议优先修改内核驱动,让上报的事件符合Linux Input协议规范——这样EventHub和InputReader就不用改了。

小结

好了,这一章的内容就到这里。我们梳理了AOSP输入系统的三大核心组件:

  • EventHub:从内核读取原始事件,是系统的“耳朵”
  • InputReader:解析原始事件,是系统的“大脑”
  • InputDispatcher:分发事件给应用,是系统的“手”

下一章,我们会深入EventHub的源码,看看它到底是怎么管理设备节点的。到时候我会分享一些我在调试多点触控时的实战经验,保证干货满满。

记住一句话:搞懂输入系统,就是搞懂Android的“感知”能力。触摸不灵,别急着换屏,先查查EventHub和InputReader——很多时候,问题出在软件层。

我们下章见。