文章内容
- 1.1 事件先落在哪里
- 1.2 两个用户态进程(外加内核态输入子系统)与五个对象
- 1.3 读取链路与分发链路
- 2.1 SystemServer 里的创建、注册与启动
- 2.2 从 nativeInit 到 NativeInputManager
- 2.3 两个对象与两个线程
- 3.1 EventHub 的准备工作
- 3.2 getEvents:从 inputevent 到 RawEvent
- 3.3 loopOnce:一次循环读取一批事件
- 3.4 processEventsLocked:设备事件与输入事件分流
- 3.5 设备与 Mapper
- 3.6 从 RawEvent 到 NotifyMotionArgs
- 4.1 dispatchOnce:等待与唤醒
- 4.2 notifyMotion:MotionEntry 入队
- 4.3 dispatchOnceInnerLocked:取出并分类
- 4.4 findTouchedWindowTargetsLocked:选择窗口
- 4.5 从 dispatchEventLocked 到 publishMotionEvent
- 5.1 事件在应用进程的落点
- 5.2 View.dispatchTouchEvent 中的处理顺序
- 6.1 全屏悬浮窗方案
- 6.2 FLAGNOTTOUCHABLE 与触摸区域
- 6.3 应用层没有归还入口
Q:一次手指触摸屏幕,产生的 MotionEvent 怎样从内核输入设备文件到达 View.dispatchTouchEvent?应用在原窗口上覆盖一个全屏窗口,能否由上层窗口接收触摸,再把事件交给下面的窗口?
A:输入链路是一条由系统侧控制的单向流水线。EventHub 从 /dev/input 读取原始事件,InputReader 将其解释成设备无关的 NotifyMotionArgs,InputDispatcher 根据窗口信息确定事件归属,再经 InputChannel 的 socket 投递到应用进程。
事件交给上层窗口后,应用层既拿不到下层窗口的 handle,也没有返回 InputDispatcher 分发入口的接口,因此无法完成这种透传。
一、一次触摸事件的来路
1.1 事件先落在哪里
手指触碰屏幕后,触摸硬件产生电信号,驱动把它转换成 Linux input 子系统定义的事件。Android 用户空间并不直接操作硬件,而是读取 /dev/input/eventX 设备节点。设备节点具有文件描述符,因而可以使用 read() 读取,也可以交给 epoll 等待可读状态。这就是输入链路从“读文件”开始的原因。
内核提供的 struct input_event 仍是较低层的数据:type 表示事件类别,code 表示轴或按键编号,value 保存对应值,time 记录时间。一次触摸动作通常由多条这样的记录组成,直到 EV_SYN/SYN_REPORT 才形成一个可同步处理的状态批次。Java 层见到的 MotionEvent 尚未在这里产生。
1.2 两个用户态进程(外加内核态输入子系统)与五个对象
内核态输入子系统先生成设备事件,用户态工作则分布在 system_server 与应用进程之间。Android 6.0 的 inputflinger 运行在 system_server 内,EventHub、InputReader、InputDispatcher 负责读取、解释和投递;同在 system_server 内的 WindowManagerService(WMS)向分发器提供窗口层级、可触摸区域等信息。应用进程一侧由 ViewRootImpl 接住输入通道中的事件,再送入 View 树。
五个对象各有职责边界:EventHub 面向内核设备文件,InputReader 解释设备语义,InputDispatcher 管理窗口目标和连接,WMS 管理窗口状态,ViewRootImpl 则连接应用窗口与 View 层。系统侧选择的是窗口;事件进入应用进程后,应用才在 View 层继续分发。
1.3 读取链路与分发链路
读取链路从内核开始,到 InputReader 产出规范化参数为止:/dev/input/eventX → EventHub → RawEvent → 设备对应的 InputMapper → NotifyMotionArgs。分发链路接过这些参数:InputDispatcher → MotionEntry → 窗口目标 → InputChannel → ViewRootImpl → View.dispatchTouchEvent。
这两段由监听器接口衔接。InputReader 构造时获得的 listener 实际指向 InputDispatcher;TouchInputMapper 调用 notifyMotion() 后,事件才从“设备解释”进入“窗口分发”。

二、输入服务的启动:对象与线程的创建
2.1 SystemServer 里的创建、注册与启动
输入服务由 SystemServer.startOtherServices() 建立。这里先构造 InputManagerService,随后把它注册为 Context.INPUT_SERVICE,为其设置 WMS 回调并调用 start()。
这段代码对应输入服务在 SystemServer 中的创建、注册和启动环节:
// 代码位置 frameworks/base/services/java/com/android/server/SystemServer.java 第 497—509 行 // 创建 Java 层输入管理服务 inputManager = new InputManagerService(context); // 此处省略:输出 Window Manager 启动日志(第 499 行) // 创建 WMS,并把输入服务引用传入 wm = WindowManagerService.main(context, inputManager, // 传入工厂测试模式配置 mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL, // 传入启动状态与核心模式配置 !mFirstBoot, mOnlyCore); // 注册窗口服务 ServiceManager.addService(Context.WINDOW_SERVICE, wm); // 将输入服务注册到 ServiceManager ServiceManager.addService(Context.INPUT_SERVICE, inputManager); // 此处省略:把 WMS 设置给 ActivityManagerService(第 506 行) // 把 WMS 的输入监控回调交给输入服务 inputManager.setWindowManagerCallbacks(wm.getInputMonitor()); // 启动输入服务的 native 线程 inputManager.start();输入服务的引用传入 WMS,WMS 的输入监控回调随后又交给输入服务,两者在同一段启动代码中建立关联。读取与分发线程则要等到 start() 被调用后才会启动。
2.2 从 nativeInit 到 NativeInputManager
InputManagerService 构造函数调用 nativeInit(),返回值保存在 mPtr。start() 再把这个值传给 nativeStart()。这个 long 保存 native 对象地址,使 Java 对象可以在后续生命周期中定位对应的 NativeInputManager。
这段代码对应 Java 层保存 native 指针并发起启动的环节:
// 代码位置 frameworks/base/services/core/java/com/android/server/input/InputManagerService.java 第 261—284 行 // 定义输入管理服务构造函数 public InputManagerService(Context context) { // 保存系统上下文 this.mContext = context; // 此处省略:创建 Handler、读取音频插孔配置并记录初始化日志(第 263—268 行) // 调用 JNI 初始化并保存 native 对象指针 mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue()); // 此处省略:注册 InputManagerInternal 本地服务(第 271 行) // 构造函数已建立 Java 服务与 native 对象的关联 } // 此处省略:设置 WMS 与有线配件回调的方法(第 274—280 行) // 定义输入服务启动方法 public void start() { // 使用已保存的指针启动 native 输入管理器 nativeStart(mPtr);mPtr 关联 Java 服务与 native 对象。构造函数负责保存这个关联,start() 再通过它启动 native 输入管理器。
这段代码对应 JNI 取得消息队列、创建 NativeInputManager 并回传地址的环节:
// 代码位置 frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp 第 1035—1047 行// 定义 JNI 初始化入口static jlong nativeInit(JNIEnv* env, jclass /* clazz */, // 接收 Java 服务、Context 和消息队列 jobject serviceObj, jobject contextObj, jobject messageQueueObj) { // 从 Java 消息队列取得 native MessageQueue sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); // 检查消息队列是否初始化成功 if (messageQueue == NULL) { // 向 Java 层抛出运行时异常 jniThrowRuntimeException(env, "MessageQueue is not initialized."); // 初始化失败时返回空指针值 return 0; // 判空失败路径已经返回,正常路径继续创建 native 对象 } // 创建 NativeInputManager NativeInputManager* im = new NativeInputManager(contextObj, serviceObj, // 使用 Java 消息队列对应的 Looper messageQueue->getLooper()); // 增加 native 对象的强引用计数 im->incStrong(0); // 把 native 指针转换成 jlong 返回 return reinterpret_cast<jlong>(im);// 返回后保留由 mPtr 持有的 native 对象}判空分支保证 NativeInputManager 构造时使用的是有效 MessageQueue,mPtr 保存的地址来自其后的 reinterpret_cast。
这段代码对应 nativeStart() 还原指针并启动 InputManager 的环节:
// 代码位置 frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp 第 1049—1056 行// 定义 JNI 启动入口static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) { // 从 jlong 恢复 NativeInputManager 指针 NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); // 启动其持有的 InputManager status_t result = im->getInputManager()->start(); // 检查 native 输入管理器是否启动成功 if (result) { // 启动失败时向 Java 层抛出异常 jniThrowRuntimeException(env, "Input manager could not be started."); // 仅在启动失败时抛出异常 }// nativeStart 通过异常而非返回值报告失败}JNI 代码确认了上述指针关系;nativeStart() 通过该指针调用 InputManager::start(),失败时将异常抛回 Java 层。
NativeInputManager 构造时创建 EventHub,再用它和两个 policy 接口构造 InputManager:
// 代码位置 frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp 第 288—306 行// 定义 NativeInputManager 构造函数NativeInputManager::NativeInputManager(jobject contextObj, // 接收 Java 服务对象与 Looper jobject serviceObj, const sp<Looper>& looper) : // 保存 Looper 并初始化交互状态 mLooper(looper), mInteractive(true) { // 此处省略:保存 Java 全局引用并初始化受锁保护的配置(第 291—304 行) // 创建负责访问输入设备的 EventHub sp<EventHub> eventHub = new EventHub(); // 创建 InputManager,并把自身作为 reader 与 dispatcher 的 policy mInputManager = new InputManager(eventHub, this, this);// 构造完成后,mInputManager 持有 EventHub、reader policy 与 dispatcher policy}Java 服务没有直接读取设备文件,设备访问和输入处理由 native 侧的 EventHub、InputManager 负责。
2.3 两个对象与两个线程
InputManager 创建 InputDispatcher 和 InputReader,并把 InputDispatcher 作为 listener 传给 InputReader。initialize() 创建两个线程;start() 的调用顺序是 InputDispatcherThread 在前,InputReaderThread 在后。读取线程开始产生事件时,分发线程已经可以处理入站队列。
这段代码对应 native 输入组件及其线程的创建、启动环节:
// 代码位置 frameworks/native/services/inputflinger/InputManager.cpp 第 27—51 行// 定义 InputManager 构造函数InputManager::InputManager( // 接收 EventHub 接口 const sp<EventHubInterface>& eventHub, // 接收读取策略与分发策略 const sp<InputReaderPolicyInterface>& readerPolicy, // 接收分发策略 const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) { // 创建 InputDispatcher mDispatcher = new InputDispatcher(dispatcherPolicy); // 创建 InputReader,并把 InputDispatcher 作为监听器 mReader = new InputReader(eventHub, readerPolicy, mDispatcher); // 创建配套线程 initialize();// 构造函数完成对象创建并进入线程初始化}// 此处省略:另一组构造参数的重载与析构函数(第 36—46 行)// 定义线程初始化函数void InputManager::initialize() { // 创建输入读取线程 mReaderThread = new InputReaderThread(mReader); // 创建输入分发线程 mDispatcherThread = new InputDispatcherThread(mDispatcher);// 两个线程对象均已创建,等待 start 启动}这一段保留了已核对的对象构造关系,也明确标出了构造函数与 initialize() 之间省略的重载和析构函数。
这段代码对应 InputManager::start() 依次启动两个线程并处理失败结果的环节:
// 代码位置 frameworks/native/services/inputflinger/InputManager.cpp 第 53—69 行// 定义 InputManager 启动函数status_t InputManager::start() { // 先启动输入分发线程 status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY); // 检查分发线程启动结果 if (result) { // 记录分发线程启动错误 ALOGE("Could not start InputDispatcher thread due to error %d.", result); // 直接返回错误码,不再启动读取线程 return result; // 失败路径已经返回,成功路径继续启动读取线程 } // 分发线程成功后再启动输入读取线程 result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY); // 检查读取线程启动结果 if (result) { // 记录读取线程启动错误 ALOGE("Could not start InputReader thread due to error %d.", result); // 请求已经启动的分发线程退出 mDispatcherThread->requestExit(); // 返回读取线程的启动错误码 return result; // 读取线程启动成功时继续返回 OK } // 两个线程均已启动 return OK;// start 用 OK 或错误码报告两个线程的整体启动结果}对象依赖、启动次序和失败清理都写在这两段实现中。InputReader 持有的 listener 指向 InputDispatcher,所以能够直接发出回调;读取线程启动失败时,代码还会请求已启动的分发线程退出。

三、读取:EventHub 与 InputReader
3.1 EventHub 的准备工作
EventHub 同时处理两类变化:输入设备产生数据,以及 /dev/input 目录中的设备节点增删。构造函数创建 epoll 实例,用 inotify 监视 IN_CREATE | IN_DELETE,还建立一对非阻塞管道用于主动唤醒等待中的线程。设备 fd、mINotifyFd 和唤醒管道的读端都可以纳入同一个等待集合。
这段代码对应 EventHub 建立 epoll、目录监视和唤醒管道的环节:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 200—223 行 // 创建并清零复用的 epoll 事件描述 struct epoll_event eventItem; // 清除 eventItem 中的旧数据 memset(&eventItem, 0, sizeof(eventItem)); // 监听文件描述符的可读事件 eventItem.events = EPOLLIN; // 用编号标识 inotify 事件来源 eventItem.data.u32 = EPOLL_ID_INOTIFY; // 把 inotify 文件描述符注册进 epoll result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem); // 此处省略:检查 inotify 注册结果(第 205 行) // 创建存放唤醒管道两端的数组 int wakeFds[2]; // 创建唤醒管道 result = pipe(wakeFds); // 此处省略:检查管道创建结果(第 209 行) // 保存唤醒管道读端 mWakeReadPipeFd = wakeFds[0]; // 保存唤醒管道写端 mWakeWritePipeFd = wakeFds[1]; // 此处省略:把管道两端设置为非阻塞模式(第 214—220 行) // 复用 eventItem,并改用唤醒来源编号 eventItem.data.u32 = EPOLL_ID_WAKE; // 把唤醒管道读端注册进同一个 epoll result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);同一个 eventItem 通过 data.u32 区分 inotify 与唤醒管道。设备热插拔和输入数据共用这套等待机制,任一 fd 就绪都能唤醒读取线程,无需轮询所有设备。
getEvents() 处理完当前 epoll 事件后,会在 mPendingINotify 为真时调用 readNotifyLocked()。这段代码对应读取 inotify 数据并处理读取失败的环节:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 1577—1593 行// 定义 inotify 通知处理函数status_t EventHub::readNotifyLocked() { // 保存 read 的返回值 int res; // 准备完整设备路径缓冲区 char devname[PATH_MAX]; // 保存设备文件名写入位置 char *filename; // 准备 inotify 事件缓冲区 char event_buf[512]; // 声明后续循环用于推进偏移的事件长度 int event_size; // 记录缓冲区中的事件偏移 int event_pos = 0; // 保存当前 inotify_event 指针 struct inotify_event *event; // 此处省略:输出 inotify 文件描述符调试日志(第 1586 行) // 从 inotify 文件描述符读取事件 res = read(mINotifyFd, event_buf, sizeof(event_buf)); // 此处省略:读取不足一条事件时的 EINTR 与错误返回分支(第 1588—1593 行)这里先保证缓冲区中至少包含一条完整的 inotify_event;成功读取后,函数继续拼出设备路径并遍历通知。
这段代码对应 IN_CREATE 打开设备、其他目录事件关闭设备的分支:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 1596—1611 行 // 复制输入设备目录作为路径前缀 strcpy(devname, DEVICE_PATH); // 把 filename 指向路径末尾 filename = devname + strlen(devname); // 在目录与文件名之间补上斜杠 *filename++ = '/'; // 逐条解析缓冲区中的 inotify 事件 while(res >= (int)sizeof(*event)) { // 取得当前位置的事件结构 event = (struct inotify_event *)(event_buf + event_pos); // 此处省略:输出当前 inotify_event 的调试语句(第 1602 行) // 只处理带设备文件名的事件 if(event->len) { // 把文件名拼到设备目录之后 strcpy(filename, event->name); // 节点创建时打开新设备 if(event->mask & IN_CREATE) { // 将新设备加入 EventHub openDeviceLocked(devname); // 非创建事件进入设备关闭分支 } else { // 此处省略:记录设备移除日志(第 1608 行) // 按完整路径关闭并移除设备 closeDeviceByPathLocked(devname); // 有文件名的事件已完成设备打开或关闭 } // event->len 为零时不执行设备路径操作 }getEvents() 在第 946—949 行调用该函数。设备打开和关闭随后分别形成 DEVICE_ADDED、DEVICE_REMOVED 类型的 RawEvent,再由 InputReader 处理。
3.2 getEvents:从 input_event 到 RawEvent
getEvents() 在没有现成事件时通过 epoll_wait() 阻塞。设备 fd 可读后,代码调用 read() 获取一批 struct input_event,再逐条填入 RawEvent 缓冲区。RawEvent 保留设备 ID、事件时间、类型、编码和值,为后续按设备解释提供统一输入。
这段代码对应 getEvents() 准备读取缓冲区和 RawEvent 输出位置的环节:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 707—718 行// 定义批量读取事件的入口size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) { // 要求输出缓冲区至少能容纳一条事件 ALOG_ASSERT(bufferSize >= 1); // 读取和更新 EventHub 状态时持有互斥锁 AutoMutex _l(mLock); // 准备内核 input_event 读取缓冲区 struct input_event readBuffer[bufferSize]; // 让 event 指向调用方提供的输出缓冲区 RawEvent* event = buffer; // 记录输出缓冲区剩余容量 size_t capacity = bufferSize; // 记录本轮是否由 wake 管道唤醒 bool awoken = false; // 持续处理待办事件,直到需要向调用方返回 for (;;) { // 取得当前单调时钟时间 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC); // 此处省略:重新打开设备、上报设备增删、扫描设备(第 720—777 行)这部分初始化同时保留内核事件缓冲区和 RawEvent 输出指针;第 720—777 行的设备重开、增删上报与扫描逻辑已明确标出。
没有可立即返回的事件时,getEvents() 释放锁和 wake lock,再进入 epoll_wait():
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 973—981 行 // 从待处理 epoll 事件的首项重新开始 mPendingEventIndex = 0; // 在阻塞等待前释放 EventHub 锁 mLock.unlock(); // release lock before poll, must be before release_wake_lock // 释放等待期间不需要持有的 wake lock release_wake_lock(WAKE_LOCK_ID); // 等待设备、inotify 或唤醒管道就绪 int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis); // 等待返回后重新取得 wake lock acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID); // 重新锁定 EventHub 状态 mLock.lock(); // reacquire lock after poll, must be after acquire_wake_lock锁在阻塞调用前释放,epoll_wait() 返回后再恢复,避免等待期间占住 EventHub 的互斥锁。
epoll 结果进入下一轮处理时,代码先把来源编号映射成 deviceIndex,判空后才能读取设备:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 779—818 行 // 记录本轮是否发生设备变化 bool deviceChanged = false; // 遍历 epoll 返回的待处理事件 while (mPendingEventIndex < mPendingEventCount) { // 取出当前 epoll_event 并推进索引 const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++]; // 此处省略:处理 inotify 与唤醒管道来源(第 783—806 行) // 用 epoll 事件中的设备编号查找设备索引 ssize_t deviceIndex = mDevices.indexOfKey(eventItem.data.u32); // 未找到设备时进入异常来源分支 if (deviceIndex < 0) { // 此处省略:记录未知设备编号日志(第 810—811 行) // 跳过无法映射到设备的 epoll 事件 continue; // 只有有效索引才会继续访问 mDevices } // 根据有效索引取得设备对象 Device* device = mDevices.valueAt(deviceIndex); // 只在设备文件描述符可读时读取 if (eventItem.events & EPOLLIN) { // 从设备节点批量读取 input_event int32_t readSize = read(device->fd, readBuffer, // 按输出缓冲区容量限制读取字节数 sizeof(struct input_event) * capacity);deviceIndex 的声明与判空位于访问 mDevices 之前,因此未知来源不会被当成有效设备解引用。
读取成功后,input_event 被逐条复制到 RawEvent;两段之间省略的是时间戳校正逻辑:
// 代码位置 frameworks/native/services/inputflinger/EventHub.cpp 第 833—837、918—921 行 // 把内建设备编号转换为对外设备编号 int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id; // 根据读取字节数计算 input_event 数量 size_t count = size_t(readSize) / sizeof(struct input_event); // 遍历本次读取的所有内核事件 for (size_t i = 0; i < count; i++) { // 取得当前 input_event 引用 struct input_event& iev = readBuffer[i]; // 此处省略:日志、Android 时间覆盖与时间戳校正(第 838—917 行) // 写入 RawEvent 的设备编号 event->deviceId = deviceId; // 写入 RawEvent 的类型 event->type = iev.type; // 写入 RawEvent 的编码 event->code = iev.code; // 写入 RawEvent 的值 event->value = iev.value;EventHub 的产出是按批次读取并填充的底层记录。它没有在这里决定目标窗口,也没有调用 View 层 API。
3.3 loopOnce:一次循环读取一批事件
InputReaderThread::threadLoop() 会反复调用 InputReader::loopOnce()。一次循环先计算超时时间,随后在不持有 InputReader 锁的情况下调用 getEvents();拿到事件后重新加锁并处理。这样,等待设备输入不会长期占用 reader 内部锁。
这段代码对应 InputReader 单次循环中的读取与处理环节:
// 代码位置 frameworks/native/services/inputflinger/InputReader.cpp 第 272—301 行// 定义 InputReader 的单次循环void InputReader::loopOnce() { // 此处省略:保存旧设备代次(第 273 行) // 初始化等待超时时间 int32_t timeoutMillis; // 此处省略:准备设备变化标志、设备列表并计算等待超时(第 275—292 行) // 从 EventHub 批量取得 RawEvent size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE); // 进入加锁区域 { // acquire lock // 锁定 InputReader 状态 AutoMutex _l(mLock); // 有事件时进入批处理 if (count) { // 处理本批 RawEvent processEventsLocked(mEventBuffer, count); // 此处省略:超时、设备代次变化、通知策略与 flush(第 302—333 行)读取线程每轮等待并处理一批事件,处理结束后继续循环。
3.4 processEventsLocked:设备事件与输入事件分流
processEventsLocked() 根据 type 分流。普通输入事件的类型值小于 FIRST_SYNTHETIC_EVENT,同一 deviceId 的连续记录会组成一个批次,并交给 processEventsForDeviceLocked()。合成事件分别处理设备增加、设备移除和扫描完成。
这段代码对应按设备批处理输入记录,并分流三类设备事件的环节:
// 代码位置 frameworks/native/services/inputflinger/InputReader.cpp 第 336—363 行// 定义 RawEvent 批处理函数void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) { // 遍历当前批次 for (const RawEvent* rawEvent = rawEvents; count;) { // 取得当前事件类型 int32_t type = rawEvent->type; // 默认批次包含当前一条事件 size_t batchSize = 1; // 普通输入事件按设备进入 Mapper if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) { // 取得当前设备编号 int32_t deviceId = rawEvent->deviceId; // 在剩余记录中寻找同设备事件 while (batchSize < count) { // 合成事件或设备编号变化时结束当前批次 if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT // 同一批次只包含同一设备的数据 || rawEvent[batchSize].deviceId != deviceId) { // 停止扩展当前批次 break; // 遇到批次边界后停止增加 batchSize } // 把下一条同设备记录纳入批次 batchSize += 1; // 当前 deviceId 的连续记录已经全部纳入 batchSize } // 此处省略:DEBUG_RAW_EVENTS 调试日志(第 349—351 行) // 将同设备的一批事件交给 InputDevice processEventsForDeviceLocked(deviceId, rawEvent, batchSize);这部分实现确认了普通输入事件按照 deviceId 成批交给设备对象,避免把不同设备的记录混入同一次处理。
这段代码对应 processEventsLocked() 对三类设备状态事件的分支处理:
// 代码位置 frameworks/native/services/inputflinger/InputReader.cpp 第 353—363 行 // 合成事件进入设备状态分支 } else { // 根据合成事件类型分派 switch (rawEvent->type) { // 处理设备增加 case EventHubInterface::DEVICE_ADDED: // 创建并注册新设备 addDeviceLocked(rawEvent->when, rawEvent->deviceId); // DEVICE_ADDED 不再进入其他 switch 分支 break; // 处理设备移除 case EventHubInterface::DEVICE_REMOVED: // 移除已经注册的设备 removeDeviceLocked(rawEvent->when, rawEvent->deviceId); // DEVICE_REMOVED 不再进入其他 switch 分支 break; // 处理设备扫描完成 case EventHubInterface::FINISHED_DEVICE_SCAN: // 通知配置已经发生变化 handleConfigurationChangedLocked(rawEvent->when); // FINISHED_DEVICE_SCAN 不再进入其他 switch 分支 break;设备拓扑变化和设备上报的数据共享 RawEvent 通道,但 type 判断会把它们送入不同路径。
3.5 设备与 Mapper
addDeviceLocked() 最终调用 createDeviceLocked()。设备的 classes 决定要挂载哪些 InputMapper:键盘使用 KeyboardInputMapper,光标设备使用 CursorInputMapper,多点触摸和单点触摸分别使用 MultiTouchInputMapper、SingleTouchInputMapper,摇杆使用 JoystickInputMapper。一个 InputDevice 可以持有多个 mapper,InputDevice::process() 会把原始事件交给它们。
这段代码对应依据设备类别安装触摸 Mapper 的环节:
// 代码位置 frameworks/native/services/inputflinger/InputReader.cpp 第 433—500 行// 定义输入设备创建函数InputDevice* InputReader::createDeviceLocked(int32_t deviceId, int32_t controllerNumber, // 接收设备标识与类别位 const InputDeviceIdentifier& identifier, uint32_t classes) { // 创建 InputDevice InputDevice* device = new InputDevice(&mContext, deviceId, bumpGenerationLocked(), // 传入控制器编号、标识和类别 controllerNumber, identifier, classes); // 此处省略:外部设备、麦克风、开关、振动器、键盘与光标 Mapper 配置(第 438—481 行) // 多点触摸设备使用 MultiTouchInputMapper if (classes & INPUT_DEVICE_CLASS_TOUCH_MT) { // 添加多点触摸 Mapper device->addMapper(new MultiTouchInputMapper(device)); // 单点触摸设备使用 SingleTouchInputMapper } else if (classes & INPUT_DEVICE_CLASS_TOUCH) { // 添加单点触摸 Mapper device->addMapper(new SingleTouchInputMapper(device)); // 触摸设备只会进入多点或单点 Mapper 分支之一 } // 此处省略:摇杆、外接触控笔 Mapper 与函数返回(第 490—500 行)EventHub 无需理解触摸坐标的含义,设备分类对应的 Mapper 负责完成解释。
3.6 从 RawEvent 到 NotifyMotionArgs
触摸 Mapper 接收 EV_ABS、EV_KEY、EV_SYN 等记录。遇到 SYN_REPORT 时,TouchInputMapper::sync() 生成新的 RawState 快照,syncTouch() 填充 pointer 的 id、x、y、pressure 等字段,assignPointerIds() 维持指针身份,cookAndDispatch() 再根据 STYLUS、MOUSE、GESTURES 等 pointer usage 选择处理路径。进入 dispatchMotion() 后,坐标、属性、action、时间等参数被装进 NotifyMotionArgs。
这段代码对应把触摸参数封装成 NotifyMotionArgs 并通知监听器的环节:
// 代码位置 frameworks/native/services/inputflinger/InputReader.cpp 第 6072—6077 行 // 用标准化后的触摸字段构造通知参数 NotifyMotionArgs args(when, getDeviceId(), source, policyFlags, // 写入动作、状态和边缘标志 action, actionButton, flags, metaState, buttonState, edgeFlags, // 写入显示、指针属性与坐标 mViewport.displayId, pointerCount, pointerProperties, pointerCoords, // 写入精度与按下时间 xPrecision, yPrecision, downTime); // 把 Motion 通知交给监听器 getListener()->notifyMotion(&args);// dispatchMotion 完成参数封装并把通知交给监听器}监听器就是构造 InputReader 时传入的 InputDispatcher。执行该调用时,原始设备数据已经转换为与具体触摸控制器无关的运动事件参数。



四、分发:事件的归属与投递
4.1 dispatchOnce:等待与唤醒
InputDispatcherThread 的循环入口是 dispatchOnce()。它在锁内调用 dispatchOnceInnerLocked() 和待执行命令,离开锁后根据下一次唤醒时间计算超时,调用 mLooper->pollOnce()。队列为空时,线程在这里等待;新事件入队时,wake() 会让它立即继续。
这段代码对应分发线程执行一轮并进入等待的环节:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 230—252 行// 定义分发线程的单次循环void InputDispatcher::dispatchOnce() { // 默认不设置有限唤醒时间 nsecs_t nextWakeupTime = LONG_LONG_MAX; // 此处省略:加锁、广播线程存活状态与派发循环说明(第 232—237 行) // 没有待执行命令时处理入站事件 if (!haveCommandsLocked()) { // 从入站队列推进一次分发 dispatchOnceInnerLocked(&nextWakeupTime); // 没有待执行命令时,本轮已完成一次队列推进 } // 此处省略:执行待处理命令、释放锁并取得当前时间(第 242—250 行) // 计算 Looper 等待时长 int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime); // 等待超时、回调或主动唤醒 mLooper->pollOnce(timeoutMillis);// dispatchOnce 在 pollOnce 返回后结束本轮循环}这套等待与唤醒机制让分发线程在无事件时休眠,并在新事件到达后恢复执行,避免持续空转。
4.2 notifyMotion:MotionEntry 入队
InputReader 回调 notifyMotion() 后,分发器先验证参数并执行策略过滤,再创建 MotionEntry。enqueueInboundEventLocked() 将它放到 mInboundQueue 尾部;如果队列此前为空,返回的 needWake 为真,代码随即唤醒 Looper。
这段代码对应运动事件转换为 MotionEntry、进入队列并唤醒线程的环节:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 2454、2512—2518 行// 定义运动事件通知入口void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) { // 此处省略:参数校验、策略过滤与加锁(第 2455—2510 行) // 创建供分发器持有的 MotionEntry MotionEntry* newEntry = new MotionEntry(args->eventTime, // 写入设备、来源和策略信息 args->deviceId, args->source, policyFlags, // 写入动作、按钮和事件标志 args->action, args->actionButton, args->flags, // 写入元状态与按钮状态 args->metaState, args->buttonState, // 写入边缘标志、精度与按下时间 args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime, // 写入显示编号 args->displayId, // 写入指针数据 args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);MotionEntry 构造实参完整保留了时间、设备、动作、状态、显示和指针数据,没有在同一条调用中截去中间参数。
紧接着,分发器把完整的 MotionEntry 入队,并在需要时唤醒 Looper:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 2520—2527 行 // 将事件加入入站队列并判断是否需要唤醒 needWake = enqueueInboundEventLocked(newEntry); // 入队完成后释放分发器锁 mLock.unlock(); // 离开由 notifyMotion 手动管理的锁区间 } // release lock // 队列从空变为非空时唤醒分发线程 if (needWake) { // 唤醒正在 pollOnce 的 Looper mLooper->wake(); // 仅在 needWake 为真时执行主动唤醒 }// notifyMotion 不选择窗口,入队后的事件由分发线程继续处理}入队发生在锁内,主动唤醒发生在释放锁之后。读取线程只负责提交事件与唤醒,窗口选择仍在分发线程中完成。
4.3 dispatchOnceInnerLocked:取出并分类
dispatchOnceInnerLocked() 在没有 mPendingEvent 时检查 mInboundQueue。队列非空便取出队首,随后按 EventEntry 类型处理。TYPE_MOTION 会转成 MotionEntry 并调用 dispatchMotionLocked();触摸来源属于 pointer event,因此接着进入触摸窗口目标查找。
分发器在这里还处理策略丢弃、分发冻结、按键重复和超时等状态。在这条触摸链路中,mInboundQueue 是生产者与消费者的边界,mPendingEvent 表示当前正在分发的事件。
4.4 findTouchedWindowTargetsLocked:选择窗口
dispatchMotionLocked() 对触摸类 pointer event 调用 findTouchedWindowTargetsLocked()。该函数从前到后遍历窗口 handle,跳过其他 display 和不可见窗口。可见窗口若未设置 FLAG_NOT_TOUCHABLE,并且既没有设置 FLAG_NOT_FOCUSABLE、也没有设置 FLAG_NOT_TOUCH_MODAL(即处于 touch-modal 状态),或者触点落在 touchableRegion 内,就会被选为 newTouchedWindowHandle,遍历随即停止。
这段代码对应按层级、窗口标志和触摸区域确定目标窗口的环节:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1200—1210 行 // 初始化本轮找到的窗口 handle sp<InputWindowHandle> newTouchedWindowHandle; // 默认还没有计算出 touch-modal 状态 bool isTouchModal = false; // 从前向后遍历窗口寻找触摸目标 size_t numWindows = mWindowHandles.size(); // 按窗口层级访问 handle for (size_t i = 0; i < numWindows; i++) { // 取得当前窗口 handle sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i); // 读取当前窗口的输入属性 const InputWindowInfo* windowInfo = windowHandle->getInfo(); // 跳过不属于当前 display 的窗口 if (windowInfo->displayId != displayId) { // 继续检查下一个窗口 continue; // wrong display // 当前窗口属于目标 display 时继续判断 }窗口遍历按层级进行,displayId 不匹配时立即 continue,所以后面的标志与区域判断只针对当前 display。
这段代码对应从窗口 flags 计算 isTouchModal,再结合 touchableRegion 选出目标:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1212—1221 行 // 读取窗口布局标志 int32_t flags = windowInfo->layoutParamsFlags; // 只检查可见窗口 if (windowInfo->visible) { // 跳过 FLAG_NOT_TOUCHABLE 窗口 if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) { // 两个相关标志均未设置时处于 touch-modal 状态 isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE // 同时检查 FLAG_NOT_TOUCH_MODAL | InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0; // touch-modal 窗口或命中触摸区域时成为候选目标 if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) { // 记录当前顶层匹配窗口 newTouchedWindowHandle = windowHandle; // 已找到目标,停止遍历 break; // found touched window, exit window loopisTouchModal 是由 FLAG_NOT_FOCUSABLE 与 FLAG_NOT_TOUCH_MODAL 共同计算出的布尔值,并非窗口预先携带的静态属性。分发器使用 WMS 同步过来的窗口 handle,不会在这里遍历应用的 View。
4.5 从 dispatchEventLocked 到 publishMotionEvent
目标确定后,dispatchEventLocked() 根据 InputTarget.inputChannel 找到 Connection,准备 dispatch cycle。enqueueDispatchEntriesLocked() 按目标的派发模式建立 DispatchEntry,放进连接的 outbound queue;队列此前为空时,立即调用 startDispatchCycleLocked()。
这段代码对应从窗口目标找到连接,并启动连接级派发周期的环节:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 921—946 行// 定义面向目标集合的事件分发函数void InputDispatcher::dispatchEventLocked(nsecs_t currentTime, // 接收事件及已经选出的目标 EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) { // 此处省略:调试日志、事件状态断言与用户活动通知(第 923—929 行) // 遍历所有输入目标 for (size_t i = 0; i < inputTargets.size(); i++) { // 取得当前输入目标 const InputTarget& inputTarget = inputTargets.itemAt(i); // 按 InputChannel 查找连接 ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel); // 只向仍然注册的连接派发 if (connectionIndex >= 0) { // 从连接表中取得目标连接 sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex); // 为该连接准备派发周期 prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget); // 此处省略:连接不存在时记录丢弃日志(第 938—943 行) // 有效连接已进入 prepareDispatchCycleLocked,无效连接只记录丢弃日志 } // 循环继续处理剩余 InputTarget,直到目标列表耗尽 }// 该函数只准备连接级派发,不直接向 InputChannel 写入事件}inputTarget、connectionIndex 与 connection 的声明都保留在使用位置之前;无效连接只记录日志,不会进入派发周期。
这段代码对应为指定连接建立各类 DispatchEntry,并在出站队列从空变为非空时启动发送:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1821—1843 行// 定义向连接队列写入 DispatchEntry 的函数void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime, // 接收连接、事件与目标 const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) { // 记录出站队列原先是否为空 bool wasEmpty = connection->outboundQueue.isEmpty(); // 此处省略:HOVER_EXIT、OUTSIDE、HOVER_ENTER 三种派发模式(第 1825—1831 行) // 按原样派发模式创建 DispatchEntry enqueueDispatchEntryLocked(connection, eventEntry, inputTarget, // 使用未转换的事件派发标志 InputTarget::FLAG_DISPATCH_AS_IS); // 此处省略:SLIPPERY_EXIT、SLIPPERY_ENTER 两种派发模式(第 1834—1837 行) // 只在本次入队使队列变为非空时启动派发周期 if (wasEmpty && !connection->outboundQueue.isEmpty()) { // 启动当前连接的派发周期 startDispatchCycleLocked(currentTime, connection); // 非空队列已有派发周期时不会重复启动 }// 各派发模式对应的 DispatchEntry 已进入 connection->outboundQueue}窗口目标最终对应到分发器中已注册的连接,DispatchEntry 则进入该连接的出站队列。跨进程传递的是输入消息,并非应用可见的窗口对象。
startDispatchCycleLocked() 根据事件类型调用 InputPublisher。运动事件走 publishMotionEvent(),底层通过 InputChannel 的 socket 把序号、动作、坐标、指针属性和时间等数据发送到应用进程。
这段代码先补足 startDispatchCycleLocked() 的连接队列、当前事件与类型分支上下文:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1932—1962 行// 定义连接级派发周期void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime, // 接收要发送事件的连接 const sp<Connection>& connection) { // 此处省略:派发周期调试日志(第 1934—1937 行) // 连接正常且出站队列非空时持续派发 while (connection->status == Connection::STATUS_NORMAL // 队列中仍有待发送条目 && !connection->outboundQueue.isEmpty()) { // 取得队首派发条目 DispatchEntry* dispatchEntry = connection->outboundQueue.head; // 记录本条事件的交付时间 dispatchEntry->deliveryTime = currentTime; // 此处省略:说明即将发布事件的源码注释(第 1944 行) // 保存发布调用的返回状态 status_t status; // 取得派发条目关联的原始事件 EventEntry* eventEntry = dispatchEntry->eventEntry; // 按事件类型选择发布函数 switch (eventEntry->type) { // 此处省略:按键事件发布分支(第 1948—1959 行) // 进入运动事件发布分支 case EventEntry::TYPE_MOTION: { // 把通用事件转换为 MotionEntry MotionEntry* motionEntry = static_cast<MotionEntry*>(eventEntry);case EventEntry::TYPE_MOTION 现在位于完整的连接循环与事件类型 switch 中,当前 dispatchEntry 和 eventEntry 都有明确来源。
对于普通 pointer 坐标,分发器根据目标连接的缩放因子计算偏移,并在需要缩放时改用 scaledCoords:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1964—1980 行 // 准备缩放后的坐标缓冲区 PointerCoords scaledCoords[MAX_POINTERS]; // 默认直接使用 MotionEntry 中的坐标 const PointerCoords* usingCoords = motionEntry->pointerCoords; // 此处省略:说明偏移取决于输入来源的源码注释(第 1967 行) // 保存 X、Y 偏移与缩放因子 float xOffset, yOffset, scaleFactor; // pointer 事件且目标不要求清零坐标时计算变换 if ((motionEntry->source & AINPUT_SOURCE_CLASS_POINTER) // 排除要求零坐标的目标 && !(dispatchEntry->targetFlags & InputTarget::FLAG_ZERO_COORDS)) { // 取得目标连接的缩放因子 scaleFactor = dispatchEntry->scaleFactor; // 按缩放因子计算 X 偏移 xOffset = dispatchEntry->xOffset * scaleFactor; // 按缩放因子计算 Y 偏移 yOffset = dispatchEntry->yOffset * scaleFactor; // 非单位缩放时处理各指针坐标 if (scaleFactor != 1.0f) { // 此处省略:逐个复制并缩放指针坐标(第 1975—1978 行) // 发布时改用缩放后的坐标数组 usingCoords = scaledCoords; // 单位缩放时继续使用原始坐标 }这段分支保留了 scaleFactor、xOffset、yOffset 的来源,并明确说明坐标循环省略的位置。
若不满足前述条件,代码把偏移清零;带有 FLAG_ZERO_COORDS 的目标还会得到清零后的坐标数组:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1981—1993 行 // 非 pointer 或要求零坐标时进入替代分支 } else { // 清除 X 偏移 xOffset = 0.0f; // 清除 Y 偏移 yOffset = 0.0f; // 不再缩放坐标 scaleFactor = 1.0f; // 此处省略:说明零坐标不会暴露目标位置的源码注释(第 1986 行) // 目标要求隐藏实际坐标时执行清零 if (dispatchEntry->targetFlags & InputTarget::FLAG_ZERO_COORDS) { // 此处省略:逐个清空 scaledCoords(第 1988—1990 行) // 发布时改用已经清零的坐标数组 usingCoords = scaledCoords; // usingCoords 此时指向已清零的 scaledCoords } // 坐标偏移与发布数组已经确定 }两个坐标分支最终都会给 publishMotionEvent() 提供确定的偏移、精度和 usingCoords。
这段代码对应把完整的运动事件参数写入目标 InputChannel:
// 代码位置 frameworks/native/services/inputflinger/InputDispatcher.cpp 第 1995—2006 行 // 以下源码注释标明运动事件发布点 // Publish the motion event. // 调用 InputPublisher 发布运动事件 status = connection->inputPublisher.publishMotionEvent(dispatchEntry->seq, // 写入设备编号与来源 motionEntry->deviceId, motionEntry->source, // 写入解析后的动作与按钮 dispatchEntry->resolvedAction, motionEntry->actionButton, // 写入分发标志与边缘标志 dispatchEntry->resolvedFlags, motionEntry->edgeFlags, // 写入元状态与按钮状态 motionEntry->metaState, motionEntry->buttonState, // 写入坐标偏移与精度 xOffset, yOffset, motionEntry->xPrecision, motionEntry->yPrecision, // 写入按下时间与事件时间 motionEntry->downTime, motionEntry->eventTime, // 写入指针数量和属性 motionEntry->pointerCount, motionEntry->pointerProperties, // 写入选定的坐标数组 usingCoords); // 运动事件发布分支执行完毕 break; // 该 case 已把 MotionEntry 交给 InputPublisher }这段调用构成 system_server 到应用进程的投递边界。InputChannel 传输序列化后的输入消息,应用侧从配对的通道端点读取。

五、回到应用进程:ViewRootImpl 与 View.dispatchTouchEvent
5.1 事件在应用进程的落点
应用进程中的 ViewRootImpl.WindowInputEventReceiver 接收输入通道消息,转换为框架可处理的输入事件,并把它送入窗口对应的输入处理链。触摸事件随后到达 View 树的 dispatchTouchEvent(),事件路径也就从 /dev/input/eventX 延伸到了应用窗口。
这个边界也区分了两种“分发”:InputDispatcher 选择窗口并完成跨进程投递,View.dispatchTouchEvent() 处理窗口内部的 View 事件。两者使用的目标信息、所处进程和职责均不同。
5.2 View.dispatchTouchEvent 中的处理顺序
Android 6.0 的 View.dispatchTouchEvent() 先处理无障碍焦点与一致性检查,再调用 onFilterTouchEventForSecurity()。安全过滤通过后,已启用的 View 会优先调用 mOnTouchListener.onTouch();监听器没有消费事件时,才调用 onTouchEvent()。这里的 result 表示当前 View 是否处理了事件,不会让系统重新选择另一个窗口。
这段代码对应触摸事件进入单个 View 后的安全过滤、监听器和默认处理环节:
// 代码位置 frameworks/base/core/java/android/view/View.java 第 9262—9297 行 // 定义 View 的触摸事件分发入口 public boolean dispatchTouchEvent(MotionEvent event) { // 此处省略:无障碍焦点处理(第 9263—9271 行) // 初始化消费结果 boolean result = false; // 此处省略:一致性校验与 ACTION_DOWN 清理(第 9275—9283 行) // 先执行触摸安全过滤 if (onFilterTouchEventForSecurity(event)) { // 取得监听器信息 ListenerInfo li = mListenerInfo; // 启用状态下优先调用 OnTouchListener if (li != null && li.mOnTouchListener != null // 监听器返回 true 时记录已消费 && (mViewFlags & ENABLED_MASK) == ENABLED // 调用触摸监听器 && li.mOnTouchListener.onTouch(this, event)) { // 标记事件已处理 result = true; // 监听器消费后不再调用当前 View 的 onTouchEvent } // 监听器未消费时调用 View 默认处理 if (!result && onTouchEvent(event)) { // 标记默认处理已消费事件 result = true; // 保存 onTouchEvent 的消费结果 } // 安全过滤未通过时保持 result 为 false }OnTouchListener 与 onTouchEvent() 只决定当前 View 是否消费事件,二者都没有向下层窗口分发的能力。
六、小结
| 对象 | 所在侧 | 输入 | 职责与关键函数 | 输出 |
|---|---|---|---|---|
EventHub |
system_server / inputflinger |
/dev/input/eventX |
用 epoll、inotify 和 getEvents() 读取设备数据 |
RawEvent |
InputReader |
system_server / inputflinger |
RawEvent |
loopOnce() 批处理,交给 InputDevice 与 Mapper 解释 |
NotifyMotionArgs |
InputDispatcher |
system_server / inputflinger |
NotifyMotionArgs |
notifyMotion() 入队,findTouchedWindowTargetsLocked() 选窗口,publishMotionEvent() 投递 |
InputChannel 消息 |
| WMS | system_server |
窗口状态 | 向输入系统提供窗口层级、标志、触摸区域与 handle | 窗口目标信息 |
ViewRootImpl |
应用进程 | InputChannel 消息 |
接收窗口事件并送入 View 输入处理链 | View.dispatchTouchEvent() |
一次触摸由内核设备记录开始,经 EventHub 读取、InputReader 解释、InputDispatcher 定位窗口与跨进程投递,最终进入应用的 View.dispatchTouchEvent()。系统侧在投递前完成窗口归属判定,应用侧拿到事件后只处理本窗口内的 View 分发;两者之间没有“把已接收事件退回并改投下层窗口”的公开通路。这正是覆盖窗口能够拦截或放行,却无法先拦截再透传的原因。
6.1 全屏悬浮窗方案
有人会想到用 WindowManager.addView() 添加一个全屏透明悬浮窗,为录制回放工具捕获触摸;用户跳转到第三方应用后,悬浮窗继续接收事件,再把事件转给悬浮窗下面的应用。这个设想需要同时满足两个条件:悬浮窗先成为触摸目标,并且应用随后能指定下层窗口为新的系统分发目标。
输入链路只提供了前一个条件的一半。悬浮窗是否接收事件,由系统侧窗口目标查找决定。按 InputChannel 的投递方式,应用侧从配对的通道端点读出的是本窗口的输入消息,并把它还原成 MotionEvent;应用不会同时获得窗口栈或其他窗口的 InputChannel。
6.2 FLAG_NOT_TOUCHABLE 与触摸区域
若悬浮窗设置 FLAG_NOT_TOUCHABLE,findTouchedWindowTargetsLocked() 会跳过它,下层符合条件的窗口可以成为目标,但悬浮窗也收不到触摸。若不设置该标志,该窗口既没有设置 FLAG_NOT_FOCUSABLE、也没有设置 FLAG_NOT_TOUCH_MODAL(即处于 touch-modal 状态),或者点击位置落在它的 touchableRegion 内,位于前面的悬浮窗就会被选中,下层窗口不会再被选作这次触摸的普通目标。
FLAG_NOT_TOUCH_MODAL 也不能解决这个矛盾。它允许窗口区域外的触摸寻找其他目标;全屏窗口的触摸区域覆盖点击位置时,事件仍归该窗口。FLAG_WATCH_OUTSIDE_TOUCH 产生的 outside 通知同样不等于拿到完整手势后再转交。
6.3 应用层没有归还入口
应用收到事件后,dispatchTouchEvent() 的返回值只描述当前 View 树是否消费。返回 false 不会沿 socket 把原始事件退回 InputDispatcher,也不会触发系统对下层窗口再做一次 hit test。应用还无法取得第三方窗口在分发器中的 InputWindowHandle 或对应连接,自然不能调用 dispatchEventLocked() 为它构造 InputTarget。
MotionEvent.obtain() 或重新注入一个事件也不等同于透传原手势。那会成为另一条受权限、目标选择和事件一致性约束的注入路径,并没有把当前事件的系统归属转移给下层窗口。应用公开 API 没有提供所需的下层窗口 handle 与分发入口。
因此,全屏覆盖窗口只有两种直接结果:设置 FLAG_NOT_TOUCHABLE 后让下层接收,但覆盖层无法捕获;覆盖层保持可触摸时自己成为目标,却无法把收到的事件交还系统并指定下层窗口。输入分发的单向所有权转移决定了这项限制。

本文由 AgentWorkFlow 生成