ARTICLE · 1344374
Android源码第八章:InputReader 与 InputDispatcher——跟着一次触摸走到 View,再追回执
如果手指已经抬起,屏幕上的蓝色圆点却迟迟没动,先对照 getevent 和应用日志。驱动有没有报告这次触摸?窗口里的代码有没有收到它?
手动触摸先经过驱动和 evdev,再由 InputReader 整理。adb shell input tap 经系统接口向 InputDispatcher 注入,因此可以在应用日志里看到 DOWN 和 UP,却没有对应的 evdev 记录。 两条路径从分发器开始汇合。即便 View 已经执行回调,应用仍要经 InputChannel 送回 FINISHED,界面何时更新则由后续绘制链路决定。

用同一个小应用做对照。先在蓝色区域点击一次,沿驱动、分发器和应用主线程追到 View,再顺着回执找到系统端的等待队列。遇到“有底层记录却没回调”或“回调到了但画面还没变”时,可以沿这几道交接逐段排查。
文章目录
• 8.1 先把设备、系统与应用放进同一张架构图
• 8.2 准备环境并识别真实输入后端
• 8.3 运行一个可以观察事件消费的最小示例
• 8.4 Linux 怎样把设备报告变成可读的 event 节点
• 8.5 EventHub 与 InputReader 怎样生成 Android 事件
• 8.6 窗口出现时怎样建立 InputChannel
• 8.7 InputDispatcher 怎样选窗口并交付事件
• 8.8 应用 Looper 怎样从 Native 回到 Java
• 8.9 Activity、ViewGroup 与 View 怎样分配触摸序列
• 8.10 FINISHED、超时与资源释放怎样闭环
• 8.11 区分 Host 输入、evdev 写入与 adb 注入
• 8.12 用对照和 Trace 判断事件卡在哪里
• 8.13 常见误区与按层排查入口
• 8.14 回顾、自测、速查与源码基线
8.1 先把设备、系统与应用放进同一张架构图
输入系统分成采集、分发与应用处理
InputReader 整理设备报告,InputDispatcher 选择目标窗口,应用的 View 树再决定由谁处理。 EventHub、InputReader 和 InputDispatcher 在本章的 Android 系统中由 system_server 承载。Reader 与 Dispatcher 各有线程,文件所在的 inputflinger 目录不代表独立进程。

先从底部沿事件方向看。真实设备由驱动上报;本章的 SDK Emulator 按匹配的 VirtIO input 分支读源码,仍需用具体 eventX 的驱动绑定确认运行分支。普通触摸的采集接口是 Linux evdev,主链直接到 EventHub。MotionClassifier 是用户空间的可选处理,前提与位置见 8.5。
窗口管理服务(WMS)和输入管理服务(IMS)负责窗口加入、InputChannel 建立等控制工作。通道就绪后,Dispatcher 通过 Unix socket 把事件交给应用,回执从同一对端点反向送回。图上分别标出 Android 的软件分层和进程归属。应用进程里也有 Framework Java 与 Native 代码。窗口能否参与分发,取决于系统掌握的区域、token 和状态。参考 AOSP 系统架构 和 官方 Input pipeline。
触摸包含一个“手势序列”:单指常见是 DOWN → 多个 MOVE → UP,打断时可能以 CANCEL 结束。多指有 POINTER_DOWN/POINTER_UP、pointerId 和 actionIndex;pointerId 标识手指,数组下标只说明当前对象中的位置。MOVE 可批处理,历史样本保存在同一 MotionEvent 里,因此不能要求日志行数等于硬件报告行数。
先判断事件停在哪里
沿流程图走一次普通单指触摸。没有有效目标时,先检查窗口与输入策略。发送成功后,Dispatcher 把待确认的投递放进 waitQueue,应用 Looper 可以独立读取输入。应用可能按帧消费 MOVE,也可能处理完后暂时发不出回执。系统读到 FINISHED 后,按通道投递的序号(传输 seq)清掉对应的等待。焦点等待、通道断开和回执超时各有入口,排查时先区分是哪一类。

用一张时序图连起窗口、触摸和回执
图里这次触摸发生在窗口就绪之后。应用已经拿到 InputChannel,Dispatcher 也收到了窗口信息。触摸到来时,EventHub 从 evdev 取记录,Reader 把整理后的通知送到 Dispatcher 入队,Dispatcher 再通过通道将事件交给应用。最后顺着反向箭头找 FINISHED,看系统端何时把匹配的条目移出 waitQueue。手机上可以先看阶段标题和参与者,沿箭头追到需要的步骤后再放大看接口名。

图里的 mapper 是 Reader 按设备能力选用的输入映射器。它把同一报告中的原始记录组合成 Android 输入语义。这里跟踪一个单独的事件,MOVE 批处理和多指手势放到后面解释。退出单独成段,因为应用关闭本地接收器与系统移除窗口可能交错。
8.2 准备环境并识别真实输入后端
终端、源码与设备分别是什么
沿用第一章的 Linux Host 和已有 SDK Emulator。以下命令在 Host 的 Bash 终端执行;源码不在 $HOME/aosp 时修改第一行。按本文给出的完整文件搭建示例时,把 chapter8_notes 改成保存示例与附录脚本的目录。每个新终端都要初始化这些变量,设备 serial 从 devices -l 的实际结果选择:
chapter8_top="$HOME/aosp"chapter8_adb="$chapter8_top/out/host/linux-x86/bin/adb"chapter8_notes="$chapter8_top/Android源码学习笔记/章节学习/第八章-Input"cd "$chapter8_top""$chapter8_adb" devices -lread -r -p '输入本次设备 serial:' chapter8_serial"$chapter8_adb" -s "$chapter8_serial" shell getprop sys.boot_completed"$chapter8_adb" -s "$chapter8_serial" shell getprop ro.build.fingerprint"$chapter8_adb" -s "$chapter8_serial" shell uname -a
预期设备在线且 sys.boot_completed=1。没有在线设备时先回到第一章的运行步骤;本章不会要求重新完整编译系统。记录 fingerprint、Kernel、产品和采集时间,不能将 Host 工作区 HEAD 当成正在运行镜像的身份。
先找 eventX,再反查驱动
下面是只读查询;getevent 和部分 sysfs/proc 文件可能要求设备已经有 root shell 权限。权限不足时记录失败,不把无输出当成没有输入设备。不要为查笔记自动重启 adbd;如果自己执行了 adb root,须重新连接并重开 logcat/getevent 监听。
"$chapter8_adb" -s "$chapter8_serial" shell getevent -lp"$chapter8_adb" -s "$chapter8_serial" shell cat /proc/bus/input/devices"$chapter8_adb" -s "$chapter8_serial" shell dumpsys input"$chapter8_adb" -s "$chapter8_serial" shell ls /sys/class/input
根据设备名及 ABS/KEY 能力选择触摸屏或键盘,而不是固定使用 event0。记录 /proc/bus/input/devices 的 Name、Phys、Sysfs、Handlers;将 Sysfs 的设备路径与父设备 driver 链接对应起来:
read -r -p '输入上一步确认的 event 节点名,例如 event4:' chapter8_eventif [[ "$chapter8_event" =~ ^event[0-9]+$ ]]; then "$chapter8_adb" -s "$chapter8_serial" shell readlink -e "/sys/class/input/$chapter8_event/device" "$chapter8_adb" -s "$chapter8_serial" shell readlink -e "/sys/class/input/$chapter8_event/device/device/driver" "$chapter8_adb" -s "$chapter8_serial" shell cat /proc/moduleselse printf '%s\n' '请输入已确认的 event 节点名。' >&2fi
不同总线的 driver 不一定恰好在 device/device/driver。若第二条未返回已存在的路径,沿第一条返回路径的父级找 /driver 和 /driver/module,直到能对应 PCI/VirtIO、I2C、USB 等实际后端;不要仅凭应用 source=TOUCHSCREEN 推断物理设备。
第一章的 2026-10-19 历史模块记录确实有 virtio_input 已加载。当前 Host 预编译 goldfish_modules/virtio_input.ko 的内嵌 scmversion 是 gccafb60de224,而 GKI 的历史 config 中 CONFIG_VIRTIO_INPUT 未设置。两者并不矛盾:匹配的 virtual-device 源码配置片段 virtual_device_core.fragment 单独设置 CONFIG_VIRTIO_INPUT=m。GKI 配置、虚拟设备模块配置、模块已加载、某个 eventX 已绑定这四项必须分别确认。
本章据此选择 ACK 的 virtio_input.c 作为底层阅读分支;没有把历史“模块已加载”扩写为本轮完整输入设备绑定验证。Host Emulator 是预编译程序,本工程并没有它的完整精确构建源码,因此 Host UI → 虚拟队列前半段只交代协议边界与确认方法,Guest 之后按固定源码读通。真实手机可从自己的触摸控制器 Driver 汇入同一 Linux input 主链。
源码目录先认这几处
rg -n 'EventHub::getEvents|InputReader::loopOnce' frameworks/native/services/inputflinger/readerrg -n 'InputDispatcher::notifyMotion|InputDispatcher::dispatchOnce' frameworks/native/services/inputflinger/dispatcherrg -n 'socketpair|sendFinishedSignal' frameworks/native/libs/inputrg -n 'WindowInputEventReceiver|finishInputEvent' frameworks/base/core/java/android/view/ViewRootImpl.java
8.3 运行一个可以观察事件消费的最小示例
示例使用普通 Activity 窗口,由 Framework 管理输入通道。它记录 Activity、父容器和子 View 的事件与返回值。两个开关分别控制父容器是否拦截 MOVE、子 View 是否消费 DOWN。另有一个按钮,把下一次符合条件的区域 DOWN 延迟 600 ms。退出按钮调用 finish()。
延迟按钮只设置 delayNextDown=true;只有子 View 消费 DOWN 时,才会执行并清除这次延迟。 若“子 View 消费 DOWN”为“否”,蓝色区域的 DOWN 会先返回 false,延迟标记仍可能留给后面的手势。各组对照应按 8.12 的初态与恢复步骤独立设置,不能只按按钮就认定延迟已发生。
知识库里可直接使用 Chapter8InputDemo 或 源码包。如果手里只有这篇文章,选一个本地目录作为 chapter8_notes,按下列路径保存完整文件。先准备应用文件和附录 A;到 8.12 采集 Trace 时再准备附录 B。
• 示例/Chapter8InputDemo/ 放下文的 AndroidManifest.xml,Java 文件放在这个目录下的 src/com/example/inputdemo/MainActivity.java。采用 Soong 备选路线时,再保存构建声明。
• 示例/构建示例APK.py 保存附录 A 的 Host 构建脚本。
• 示例/追踪配置/ 保存附录 B 的 第八章Input.pbtxt 和 提取输入区间.sql。
构建声明与 Manifest
工程中的 Android.bp.example 是声明模板,知识库保留这个名字以免被 Soong 重复扫描。源码包解压到 packages/apps 后使用真正的 Android.bp。
android_app { name: "Chapter8InputDemo", manifest: "AndroidManifest.xml", srcs: ["src/**/*.java"], sdk_version: "current", min_sdk_version: "28", certificate: "testkey", optimize: { enabled: false },}
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.inputdemo"> <uses-sdk android:minSdkVersion="28" /> <application android:label="Input Demo" android:debuggable="true" android:theme="@android:style/Theme.Material.Light.NoActionBar"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application></manifest>
完整 Java 示例
保存为 src/com/example/inputdemo/MainActivity.java。先读 TouchSurface.onTouchEvent 看子 View 怎样消费,再读 TouchParent.onInterceptTouchEvent 和 Activity 的记录位置;Trace 区间供 8.12 采集时使用。
本示例记录的是每层可见的 Java 事件,不会直接打印 Driver 的事务或原始设备数据。界面刷新通过 invalidate() 请求,输入处理返回与显示出新画面不是同一个完成时刻。
package com.example.inputdemo;import android.app.Activity;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.os.Bundle;import android.os.Process;import android.os.SystemClock;import android.os.Trace;import android.util.Log;import android.view.KeyEvent;import android.view.MotionEvent;import android.view.View;import android.widget.Button;import android.widget.FrameLayout;import android.widget.LinearLayout;import android.widget.TextView;import java.util.Locale;/** 教学示例:只记录事件和消费关系,不自行创建隐藏的 InputEventReceiver。 */public final class MainActivity extends Activity { private static final String TAG = "Chapter8Input"; private boolean interceptMove; private boolean consumeDown = true; private boolean delayNextDown; private static void log(String message) { Log.i(TAG, "pid=" + Process.myPid() + " tid=" + Process.myTid() + " " + message); } private static String describe(MotionEvent e) { return String.format(Locale.US, "%s device=%d source=0x%x pointers=%d id0=%d x=%.1f y=%.1f history=%d ageMs=%d", MotionEvent.actionToString(e.getAction()), e.getDeviceId(), e.getSource(), e.getPointerCount(), e.getPointerId(0), e.getX(), e.getY(), e.getHistorySize(), SystemClock.uptimeMillis() - e.getEventTime()); } @Override public void onCreate(Bundle state) { super.onCreate(state); LinearLayout page = new LinearLayout(this); page.setOrientation(LinearLayout.VERTICAL); TextView info = new TextView(this); info.setText("在下方蓝色区域按下、移动和抬起;观察 Chapter8Input 日志。\n" + "三个开关用于对比事件消费、父容器拦截和主线程延迟。"); page.addView(info); Button intercept = new Button(this); intercept.setText("父容器拦截 MOVE:关"); intercept.setOnClickListener(v -> { interceptMove = !interceptMove; intercept.setText("父容器拦截 MOVE:" + (interceptMove ? "开" : "关")); log("interceptMove=" + interceptMove); }); page.addView(intercept); Button consume = new Button(this); consume.setText("子 View 消费 DOWN:是"); consume.setOnClickListener(v -> { consumeDown = !consumeDown; consume.setText("子 View 消费 DOWN:" + (consumeDown ? "是" : "否")); log("consumeDown=" + consumeDown); }); page.addView(consume); Button delay = new Button(this); delay.setText("下一次区域 DOWN 延迟 600ms"); delay.setOnClickListener(v -> { delayNextDown = true; log("armed one DOWN delay=600ms"); }); page.addView(delay); Button close = new Button(this); close.setText("关闭此窗口"); close.setOnClickListener(v -> finish()); page.addView(close); TouchParent parent = new TouchParent(this); parent.addView(new TouchSurface(this), new FrameLayout.LayoutParams(-1, -1)); page.addView(parent, new LinearLayout.LayoutParams(-1, 0, 1)); setContentView(page); log("onCreate"); } @Override public boolean dispatchTouchEvent(MotionEvent e) { Trace.beginSection("Chapter8.Activity.dispatch"); log("Activity enter " + describe(e)); try { boolean handled = super.dispatchTouchEvent(e); log("Activity return handled=" + handled); return handled; } finally { Trace.endSection(); } } @Override public boolean dispatchKeyEvent(KeyEvent e) { String action = e.getAction() == KeyEvent.ACTION_DOWN ? "DOWN" : e.getAction() == KeyEvent.ACTION_UP ? "UP" : "MULTIPLE"; log("Activity key " + action + " code=" + KeyEvent.keyCodeToString(e.getKeyCode()) + " device=" + e.getDeviceId()); return super.dispatchKeyEvent(e); } @Override public void onWindowFocusChanged(boolean focused) { super.onWindowFocusChanged(focused); log("windowFocus=" + focused); } @Override protected void onDestroy() { log("onDestroy"); super.onDestroy(); } private final class TouchParent extends FrameLayout { private float downX; private float downY; TouchParent(Context context) { super(context); } @Override public boolean dispatchTouchEvent(MotionEvent e) { log("Parent dispatch " + describe(e)); boolean handled = super.dispatchTouchEvent(e); log("Parent return handled=" + handled); return handled; } @Override public boolean onInterceptTouchEvent(MotionEvent e) { if (e.getActionMasked() == MotionEvent.ACTION_DOWN) { downX = e.getX(); downY = e.getY(); } float distance = Math.abs(e.getX() - downX) + Math.abs(e.getY() - downY); boolean intercept = interceptMove && e.getActionMasked() == MotionEvent.ACTION_MOVE && distance > 72 * getResources().getDisplayMetrics().density; log("Parent intercept=" + intercept + " " + MotionEvent.actionToString(e.getAction())); return intercept; } @Override public boolean onTouchEvent(MotionEvent e) { log("Parent onTouch " + describe(e)); if (e.getActionMasked() == MotionEvent.ACTION_UP) performClick(); return true; } @Override public boolean performClick() { return super.performClick(); } } private final class TouchSurface extends View { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private float x = 80; private float y = 100; private String lastAction = "等待输入"; TouchSurface(Context context) { super(context); setFocusableInTouchMode(true); } @Override protected void onDraw(Canvas canvas) { canvas.drawColor(Color.rgb(225, 239, 255)); paint.setColor(Color.rgb(25, 75, 135)); paint.setTextSize(18 * getResources().getDisplayMetrics().scaledDensity); canvas.drawText("触摸实验区:" + lastAction, 24, 48, paint); canvas.drawCircle(x, y, 26, paint); } @Override public boolean onTouchEvent(MotionEvent e) { Trace.beginSection("Chapter8.Child.onTouch"); try { log("Child onTouch " + describe(e)); if (e.getActionMasked() == MotionEvent.ACTION_DOWN) { if (!consumeDown) return false; if (delayNextDown) { delayNextDown = false; Trace.beginSection("Chapter8.DelayDown600ms"); try { SystemClock.sleep(600); } finally { Trace.endSection(); } } } x = e.getX(); y = e.getY(); lastAction = MotionEvent.actionToString(e.getAction()); invalidate(); if (e.getActionMasked() == MotionEvent.ACTION_UP) performClick(); return true; } finally { Trace.endSection(); } } @Override public boolean performClick() { super.performClick(); log("Child performClick"); return true; } }}
先构建 APK,再安装到选定设备
主路径使用工程已有的 Host 工具;附录 A 给出完整脚本,按文件名保存即可,无需下载整个知识库。构建脚本将 Java 编译、D8、aapt2 与 signapk 命令记录到输出目录。脚本会检查缺少的工具,不会修改系统镜像。它适用于本例没有独立资源文件的程序,不能直接作为任意 Android 工程的通用构建器。
if chapter8_run=$(mktemp -d "$HOME/aosp-chapter8.XXXXXX") && python3 "$chapter8_notes/示例/构建示例APK.py" \ --aosp-top "$chapter8_top" \ --module "$chapter8_notes/示例/Chapter8InputDemo" \ --name Chapter8InputDemo --out "$chapter8_run/apk" && test -s "$chapter8_run/apk/Chapter8InputDemo.apk" && "$chapter8_adb" -s "$chapter8_serial" install -r "$chapter8_run/apk/Chapter8InputDemo.apk"; then "$chapter8_adb" -s "$chapter8_serial" shell am start -S -W -n com.example.inputdemo/.MainActivityelse printf '%s\n' '构建或安装失败。检查错误后重试,先别启动设备上的旧包。' >&2fi
成功判据依次是构建命令退出码 0 且输出新 APK、安装显示 Success、启动后出现实验界面。参数 -S 先停止本例包名的旧进程,以便观察新 Activity 的 onCreate。缺工具时回到第一章准备 Host 工具。签名冲突时先查设备上同包名应用来源;卸载会删除该示例数据,需明确接受该影响再执行。
熟悉 Soong 的读者可把源码包解压到一个尚不存在的 packages/apps/Chapter8InputDemo,沿第一章初始化已选产品后执行 m Chapter8InputDemo。这是备选路线;本章没有本轮 Soong 成功构建记录,不能只看到解析日志就说构建通过。
从日志预测默认手势
在第二个 Host 终端先执行 8.2 的变量初始化,再用单调时间格式监听。8.12 会复用这条命令。
"$chapter8_adb" -s "$chapter8_serial" logcat -T 1 -v threadtime -v monotonic -s Chapter8Input
用 Emulator 窗口里的鼠标/触摸操作蓝色区域,默认应看到 Activity → Parent → Child 的 DOWN/MOVE/UP,以及相应的 handled 返回;同一次回调的 PID/TID 应属于应用主线程。日志的 ageMs 是记录这一行时的 uptimeMillis 与事件 eventTime 之差,不是当前回调自身的耗时,也不是触摸到画面呈现的总延迟;父子层的该数值不能相加。移动时可能合并历史样本,记录的 history 可以大于 0。不要要求每一次 Host 鼠标移动都产生一行独立 MOVE。
结束实验可点“关闭此窗口”。需要移除本例时,可执行 adb -s 实际serial uninstall com.example.inputdemo,这个命令也会删除示例数据。应用日志已经给出了触摸进入 View 树的观察点。沿这条路往前找,最早能读到怎样的设备报告?

8.4 Linux 怎样把设备报告变成可读的 event 节点
从这一节开始,代码块摘自固定提交中的对应函数,链接指向每段摘录的第一行。要读分支和参数全貌,可从那一行回到函数开头。各项目提交见 8.14。
Driver 注册 input_dev,而不是直接创建 Java 事件
VirtIO 是 Guest 与 Host 之间的虚拟设备协议。virtqueue 是它交换缓冲区的队列,不是 Java 消息队列。Guest 的 virtinput_probe 初始化 struct virtio_input 和 input_dev,查询支持的能力,再向 Linux input 注册。下列摘录只展示最终注册阶段;分配失败和各清理标签保留在完整函数中。
virtio_input.c · 第 320 行 中的 static int virtinput_probe(
err = input_register_device(vi->idev);if (err) goto err_input_register;virtinput_fill_evt(vi);
注册的是“这个输入设备支持什么、通过哪个 input_dev 上报”,不是某个应用窗口。 evdev 作为 Linux input handler 匹配设备并建立 event 节点;它的连接入口是 evdev_connect。普通应用不用自己打开这些受权限保护的节点,system_server 的 EventHub 负责采集。
当虚拟事件队列收到可用数据时,virtinput_recv_events 取出 Host 提供的事件记录:
virtio_input.c · 第 42 行 中的 static void virtinput_recv_events(
while ((event = virtqueue_get_buf(vi->evt, &len)) != NULL) { spin_unlock_irqrestore(&vi->lock, flags); input_event(vi->idev, le16_to_cpu(event->type), le16_to_cpu(event->code), le32_to_cpu(event->value)); spin_lock_irqsave(&vi->lock, flags); virtinput_queue_evtbuf(vi, event);
input_event 的 type、code、value 来自 VirtIO 协议并完成小端转换;随后缓冲区重新放回虚拟队列复用。 Host 指针不是 Guest 的 Java 对象地址。这里没有绘制或 View 回调,下一站是内核公共输入层。
input_event、SYN_REPORT 与 evdev 队列
input_event 检查设备支持的事件类型,在设备锁内处理状态:
input.c · 第 395 行 中的 void input_event(
if (is_event_supported(type, dev->evbit, EV_MAX)) { spin_lock_irqsave(&dev->event_lock, flags); input_handle_event(dev, type, code, value); spin_unlock_irqrestore(&dev->event_lock, flags);
再经 input_handle_event 的状态与 flush 规则,交给 input_pass_values;后者调用已打开 input handle 的 handle_events,evdev 对应 evdev_events,再分发到它的各个 client。所谓 client 在这里指“打开这个 event 节点的读者”,与 Binder 的客户端角色不是同一套实例。
为什么 getevent 与 Android 能同时观察同一设备?evdev 为每次打开分配一个 evdev_client 及其环形缓冲区。 普通读取不会从 EventHub 的队列偷走事件;排他 grab 和过滤等特殊情况另论。分配位置就在 evdev_open:
evdev.c · 第 468 行 中的 static int evdev_open(
client = kvzalloc(struct_size(client, buffer, bufsize), GFP_KERNEL);if (!client) return -ENOMEM;init_waitqueue_head(&client->wait);client->bufsize = bufsize;spin_lock_init(&client->buffer_lock);client->evdev = evdev;evdev_attach_client(evdev, client);
设备报告通过 evdev_pass_values 写到这些 client 的队列。type/code/value 表示状态变化,EV_SYN/SYN_REPORT 标记一批相关变化完成,evdev 在有完整包时让等待者可读。
evdev.c · 第 283 行 中的 static void evdev_pass_values(
if (wakeup) wake_up_interruptible_poll(&client->wait, EPOLLIN | EPOLLOUT | EPOLLRDNORM | EPOLLWRNORM);
唤醒改变的是等待条件,CPU 什么时候真正运行 EventHub 还由调度器决定。evdev 的 poll 注册同一个等待队列;read 则把已完成包中的记录复制到用户空间。下面是 evdev_read 的取出与复制节点:
evdev.c · 第 583 行 中的 static ssize_t evdev_read(
while (read + input_event_size() <= count && evdev_fetch_next_event(client, &event)) { if (input_event_to_user(buffer + read, &event)) return -EFAULT; read += input_event_size();

这张图区分“同一次设备报告的多读者队列”和“数据复制”,没有把两进程画成共享一个 Java MotionEvent。读取过程中节点消失会返回 ENODEV,非阻塞且无数据可读会返回 EAGAIN;EventHub 要分别处理,而不是一直忙等。
一组底层记录怎样变成一帧触摸状态
以多点触摸 slot 协议为例,常见的相关记录有 ABS_MT_SLOT、ABS_MT_TRACKING_ID、ABS_MT_POSITION_X、ABS_MT_POSITION_Y,最后有 SYN_REPORT。不是每一帧都重发全部字段;接收方保留未改变的状态。下面是格式示意,非本轮 getevent 实测:
EV_ABS ABS_MT_SLOT 0EV_ABS ABS_MT_TRACKING_ID 42EV_ABS ABS_MT_POSITION_X 420EV_ABS ABS_MT_POSITION_Y 860EV_SYN SYN_REPORT 0
TRACKING_ID 结束的编码、轴范围与 type-A/type-B 差异要按实际设备协议读;不能把这个片段直接写回任意设备。Linux 原始轴坐标也不保证就是 View 局部像素。接下来由 mapper 维护状态、校准与映射,再生成 Android 动作。
8.5 EventHub 与 InputReader 怎样生成 Android 事件
EventHub 打开并等待设备
执行者是 system_server 的 InputReader 线程。EventHub 扫描 /dev/input 并用 inotify 关注变化,openDeviceLocked 为设备打开非阻塞 fd(文件描述符,即本进程打开资源后使用的编号),使用 ioctl 查询名称和能力,分类并加载匹配配置,然后把 fd 加入 epoll,等待这些描述符何时就绪,而不是逐个忙问。
EventHub.cpp · 第 2460 行 中的 void EventHub::openDeviceLocked(
int fd = open(devicePath.c_str(), O_RDWR | O_CLOEXEC | O_NONBLOCK);if (fd < 0) { ALOGE("could not open %s, %s\n", devicePath.c_str(), strerror(errno)); return;
文件路径给出设备入口,fd 绑定的是这次打开的内核资源。 ioctl 在这里查询 evdev 能力,不是 Binder 的 BINDER_WRITE_READ。设备状态存在时 EventHub 可复用已打开对象,设备移除则关闭并通知 Reader。
getEvents 在没有已收集事件时释放内部锁,再进入 epoll 等待:
EventHub.cpp · 第 2200 行 中的 std::vector<RawEvent> EventHub::getEvents(
mLock.unlock(); // release lock before pollint pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);mLock.lock(); // reacquire lock after poll
epoll 等待发生在 InputReader 线程,不占用应用主线程。 有设备 fd 就绪时,同一个 getEvents 循环读取 input_event,形成 RawEvent:
EventHub.cpp · 第 2126 行 中的 std::vector<RawEvent> EventHub::getEvents(
RawEvent& ev = events.emplace_back(RawEvent{ .when = processEventTimestamp(iev), .readTime = systemTime(SYSTEM_TIME_MONOTONIC), .deviceId = deviceId, .type = iev.type, .code = iev.code, .value = iev.value,});
when 是处理后的设备事件时间,readTime 是用户空间读取时刻;二者之差有观察意义,但不是 View 回调耗时。deviceId 也不等于 eventX 的 X,Android 设备重连后编号可以变化。
Reader 选择 mapper,并在同步点组合状态
InputReader::loopOnce 取得批数据,在 Reader 锁内按 device 分组并调用对应 InputDevice 和 mapper:
InputReader.cpp · 第 196 行 中的 void InputReader::loopOnce()
std::vector<RawEvent> events = mEventHub->getEvents(timeoutMillis);{ // acquire lock std::scoped_lock _l(mLock); mReaderIsAliveCondition.notify_all(); if (!events.empty()) { mPendingArgs += processEventsLocked(events.data(), events.size());
Touch mapper 消费 ABS、KEY 等记录的累积状态,在 SYN_REPORT 触发同步:
TouchInputMapper.cpp · 第 1309 行 中的 std::list<NotifyArgs> TouchInputMapper::process(
std::list<NotifyArgs> out;if (rawEvent.type == EV_SYN && rawEvent.code == SYN_REPORT) { out += sync(rawEvent.when, rawEvent.readTime);}return out;
MultiTouchInputMapper 与其 motion accumulator 保存 slot 和 tracking 信息,syncTouch 整理触点,再经 TouchInputMapper 的 raw/cooked 状态、坐标变换与 dispatchTouches 生成动作,dispatchMotion 构造 NotifyMotionArgs。键盘则由 KeyboardInputMapper 将 Linux key/scanCode 结合 .kl/.kcm 等映射为 Android KeyEvent 的语义。
这里需要分清四点:多个 Linux 记录可组合成一个 Android 事件;原始坐标会转换;Linux key code 与 Android key code 不能当同一编号;某些设备事件会被策略消费或重新组织。 .idc、设备类型、显示关联、旋转与输入 viewport 都影响坐标。不能看到 X/Y 数值不同就断言数据被损坏。
键盘支线怎样汇入同一分发器
键盘不是把触摸坐标改成一个 keyCode。KeyboardInputMapper 在 EV_KEY 分支按 scanCode 与 HID usage 判断按下/抬起,processKey 查询配置映射,保存按下状态、修饰键状态和 downTime,再形成 NotifyKeyArgs。下面是实际映射节点:
KeyboardInputMapper.cpp · 第 271 行 中的 std::list<NotifyArgs> KeyboardInputMapper::processKey(
std::optional<MappedKey> mappedKey = getDeviceContext().mapKey(scanCode, usageCode, mMetaState);if (mappedKey) { keyCode = mappedKey->keyCode; keyMetaState = mappedKey->metaState; policyFlags = mappedKey->flags;
.kl 描述物理按键等映射,.kcm 描述字符和修饰键等语义;键盘布局、显示方向、系统策略会影响最终含义。Linux scanCode、Android keyCode 与输入到文本框的字符分别是不同层次。 键盘通过监听链进入 Dispatcher.notifyKey,再由 KeyEntry 走自己的路由分支;按键重复和系统快捷键还涉及 Android 的状态与策略,不应把设备 value=2 直接当作完整的应用重复事件解释。
监听调用到队列,再到另一个线程
Reader 在锁内准备通知列表,出锁后才调用后续监听链:
InputReader.cpp · 第 241 行 中的 void InputReader::loopOnce()
for (const NotifyArgs& args : notifyArgs) { mNextListener.notify(args);}
这里是同进程函数调用,调用 notify 本身不必然换线程。 顺着构造函数中每个对象持有的下一个监听器看,本版依次经过 UnwantedInteractionBlocker、PointerChoreographer、InputProcessor、可选统计节点、InputFilter、可选 InteractionReporter,最终才到 Dispatcher。监听器可以过滤、改写或产生新的通知。Native InputFilter 对 motion 调用下一个监听器,它的 Rust 过滤分支处理的是键盘事件。
InputProcessor 有可选的 MotionClassifier。只有分类器已启用且输入符合触摸条件才会进入分类,evdev 到 EventHub 的采集路径不以它为前提。见 InputManager 的构造函数、InputFilter 的 motion 和 key 分支、InputProcessor 的触摸分类分支。
当合法通知最终进入 InputDispatcher::notifyMotion,它创建 MotionEntry、加到 inbound 队列,出锁后必要时唤醒 Dispatcher 的 Looper。下面只摘最后的入队/唤醒段;输入校验、权限标志和策略过滤在此前完成。
InputDispatcher.cpp · 第 4813 行 中的 void InputDispatcher::notifyMotion(
needWake = enqueueInboundEventLocked(std::move(newEntry)); mLock.unlock();} // release lockif (needWake) { mLooper->wake();
inbound 队列内部真的保存了事件,不是只发送一个无数据的唤醒信号:
InputDispatcher.cpp · 第 1427 行 中的 bool InputDispatcher::enqueueInboundEventLocked(
bool needWake = mInboundQueue.empty();mInboundQueue.push_back(std::move(newEntry));const EventEntry& entry = *(mInboundQueue.back());traceInboundQueueLengthLocked();
Dispatcher 线程来自自己独立的 InputThread,循环执行 dispatchOnce:
InputDispatcher.cpp · 第 968 行 中的 status_t InputDispatcher::start()
mThread = std::make_unique<InputThread>( "InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); }, /*isInCriticalPath=*/true, mVm);return OK;
先看 MotionEntry 何时入队。调用 notify 的线程仍是 Reader。wake 让 Dispatcher 有机会结束等待,它要等获得调度并取出条目才开始处理。图展示这次交接的因果关系,具体调度先后和耗时要看 Trace。

Reader 可以继续采集下一批设备输入,Dispatcher 独立处理投递和确认。队列、锁与唤醒完成交接;JNI、函数名带 notify 或 callback 都不是线程切换的证明。
8.6 窗口出现时怎样建立 InputChannel
先有窗口连接,再交付手势
窗口控制通信可对照 Binder;应用接收器的 fd 回调可对照 Looper。窗口与显示的背景见 第七章。
还没有触摸前,应用的 ViewRootImpl 添加窗口时向 IWindowSession 请求窗口加入显示。当前 setView 为需要接收输入的窗口准备 InputChannel 输出对象,作为 Binder 调用的返回承载之一:
ViewRootImpl.java · 第 1784 行 中的 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId, mInsetsController.getRequestedVisibleTypes(), inputChannel, addResult);
调用经 Session 到 WMS。WMS 在成功添加且允许输入通道的条件下,调用 WindowState.openInputChannel,再复制到返回容器并释放临时包装。Binder 在这里传递窗口控制和 fd,不是在每次 MOVE 时复制整个 View 或让窗口重新打开一对 socket。
WindowState 的创建动作由 IMS 转到 Native Dispatcher,并把 token 关联到输入窗口描述:
WindowState.java · 第 2685 行 中的 InputChannel openInputChannel()
final InputChannel channel = mWmService.mInputManager.createInputChannel(name);mInputChannelToken = channel.getToken();mInputWindowHandle.setToken(mInputChannelToken);mWmService.mInputToWindowMap.put(mInputChannelToken, this);return channel;
窗口请求到达 IMS 后,继续沿 JNI 查看通道资源在哪里建立。IMS 的 createInputChannel 调用 NativeInputManagerService.NativeImpl 声明的 Native 方法,JNI 注册为 nativeCreateInputChannel,从 Java 请求名建立 Native 通道,再把结果包装回 Java。JNI 随后调用 NativeInputManager,把创建请求继续转给 Dispatcher:
com_android_server_input_InputManagerService.cpp · 第 752 行 中的 base::Result<std::unique_ptr<InputChannel>> NativeInputManager::createInputChannel(
return mInputManager->getDispatcher().createInputChannel(name);
继续读 Dispatcher 的 createInputChannel,它向 InputChannel 请求成对端点,将 server 端放进连接管理器,返回 client 端:
InputDispatcher.cpp · 第 6436 行 中的 Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(
status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);
真正分配内核 socket 的位置在 InputTransport.cpp:
InputTransport.cpp · 第 362 行 中的 status_t InputChannel::openInputChannelPair(
int sockets[2];if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) { status_t result = -errno; ALOGE("channel '%s' ~ Could not create socket pair. errno=%s(%d)", name.c_str(), strerror(errno), errno); outServerChannel.reset(); outClientChannel.reset(); return result;
成功后,函数用两个 fd 构建不同 InputChannel 对象,并分配用于识别连接的 Binder token:
InputTransport.cpp · 第 378 行 中的 status_t InputChannel::openInputChannelPair(
sp<IBinder> token = sp<BBinder>::make();android::base::unique_fd serverFd(sockets[0]);outServerChannel = InputChannel::create(name, std::move(serverFd), token);android::base::unique_fd clientFd(sockets[1]);outClientChannel = InputChannel::create(name, std::move(clientFd), token);return OK;
两个端点各有自己的 fd 与用户空间包装;token 用于连接身份关联,事件数据通过 socket 消息传递。 同一个数字 fd 在不同进程不能当作同一个已打开文件,Binder 的 fd 传递为接收方建立有效描述符。当前版本的通道包含 InputChannelCore/Parcel 包装,临时对象的 copy/转移也不能误读为“新建一条手势通道”。
WMS 的临时通道 copyTo 会复制 fd 持有,并保留相同连接 token;随后释放临时包装,不会因关闭一个副本就立即关闭其余持有者:
InputTransport.cpp · 第 522 行 中的 void InputChannel::copyTo(
outChannel.name = getName();outChannel.fd.reset(dupChannelFd(fd.get()));outChannel.token = getConnectionToken();
应用收到返回的 channel 后,ViewRootImpl 在本线程创建 WindowInputEventReceiver:
ViewRootImpl.java · 第 1898 行 中的 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
mInputEventReceiver = new WindowInputEventReceiver(inputChannel, Looper.myLooper(), mAttachInfo.mThreadedRenderer);
看图时先找 Kernel 建立的 socket 端点,再找两边的 fd/token 包装。Binder 把客户端端点的描述符和关联信息交给应用。应用随后创建接收器,它只在自己的进程内持有对应的 Native 对象。

token、窗口区域与焦点怎样汇合
通道只提供接收能力。窗口属性和焦点请求由两条控制路径到达 Dispatcher。InputWindowHandleWrapper 用 SurfaceControl.Transaction.setInputWindowInfo 提交 token、触摸区域、可见性与变换等信息。SurfaceFlinger 发布窗口信息后,Dispatcher 的 onWindowInfosChanged 按 display 更新窗口列表并唤醒分发循环。焦点请求则由 InputMonitor 调用 Transaction.setFocusedWindow,SurfaceFlinger 再经 IInputFlinger.setFocusedWindow 交给 Dispatcher。两种信息都参与目标选择,入口各自独立。见 InputMonitor.requestFocus 和 SurfaceFlinger 的焦点请求转发。
触摸起点通常按 display、坐标、触摸区域、遮挡和窗口属性选目标;键盘等需要焦点的事件则查焦点窗口。焦点应用、焦点窗口、Java View 内部焦点分别属于不同层次:一个 Activity 前台可见,也可能尚未有可接收输入的窗口;窗口获得焦点,不等于某个 EditText 已获焦点。
8.7 InputDispatcher 怎样选窗口并交付事件
按事件类型选择目标
dispatchOnce 在自己的线程里运行主分发逻辑、待执行命令和 ANR 检查,再进入 Looper.pollOnce 等待下一轮。触摸经过 dispatchMotionLocked,在 pointer 分支进入当前版本的 TouchStates 窗口目标算法:
InputDispatcher.cpp · 第 2209 行 中的 bool InputDispatcher::dispatchMotionLocked(
Result<std::vector<InputTarget>, InputEventInjectionResult> result = mTouchStates .findTouchedWindowTargets(currentTime, *entry, mDragState ? mDragState->dragWindow : nullptr, std::bind_front(&InputDispatcher:: addDragEventLocked, this), std::bind_front(&InputDispatcher:: logDispatchStateLocked, this, /*delay=*/0ms));
目标选择同时依赖事件状态与窗口状态。 新手势 DOWN 会建立相应 touched-window 状态;后续事件通常延续这个状态,而不是每个 MOVE 都从最上层窗口重新开始。分屏、多指拆分、spy、手势转移、遮挡安全策略和显示移除会影响结果。窗口 token 再映射到 Connection,连接持有 InputPublisher 和输出/等待队列。
非 pointer 的分支调用 findFocusedWindowTargetLocked;等待焦点与窗口的失败不能简单归因为“应用 onTouch 太慢”。这也是为什么“触摸一律投给焦点窗口”无法解释普通按坐标命中的窗口行为。
键盘的目标选择也有单独的源码节点。dispatchKeyLocked 在策略处理之后查询焦点窗口;若当前还在等窗口,允许 PENDING 返回等待下一轮,不会把它当作已经交付成功:
InputDispatcher.cpp · 第 2029 行 中的 bool InputDispatcher::dispatchKeyLocked(
Result<sp<WindowInfoHandle>, InputEventInjectionResult> result = findFocusedWindowTargetLocked(currentTime, *entry, nextWakeupTime);if (!result.ok()) { if (result.error().code() == InputEventInjectionResult::PENDING) { return false;
三类队列与发送边界
同一输入可发给多个目标,每个目标有不同的派发状态和 seq。事件 id、连接上的传输 seq、Java InputEvent.getSequenceNumber 也不是同一编号。
startDispatchCycleLocked 在正常连接上处理 outboundQueue,先记录 deliveryTime 和本连接的 dispatch timeout,再用 InputPublisher 发布。成功后进入等待确认队列:
InputDispatcher.cpp · 第 4026 行 中的 void InputDispatcher::startDispatchCycleLocked(
const nsecs_t timeoutTime = dispatchEntry->timeoutTime;connection->waitQueue.emplace_back(std::move(dispatchEntry));connection->outboundQueue.erase(connection->outboundQueue.begin());traceOutboundQueueLength(*connection);if (connection->responsive) { mAnrTracker.insert(timeoutTime, connection->getToken());}traceWaitQueueLength(*connection);
若 socket 发送返回 WOULD_BLOCK,且 waitQueue 有未完成事件,Dispatcher 保留待发项,等应用处理后再试。若 waitQueue 为空,当前实现把这次 WOULD_BLOCK 视为异常并中止连接分发,其他发送错误也进入 abortBrokenDispatchCycleLocked。成功写入 socket 时,应用还需要由自己的 Looper 读取和处理。
InputChannel 用消息保留边界
InputPublisher 把事件编码成 InputMessage,带 seq、事件标识、时间、source、display 与触点数据。InputChannel.sendMessage 通过非阻塞 Unix socket 发送:
InputTransport.cpp · 第 393 行 中的 status_t InputChannel::sendMessage(
const size_t msgLength = msg->size();InputMessage cleanMsg;msg->getSanitizedCopy(&cleanMsg);ssize_t nWrite;do { nWrite = ::send(getFd(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);} while (nWrite == -1 && errno == EINTR);
输入事件经 socket 送出,SOCK_SEQPACKET 保留消息边界。窗口和权限控制走另外的 Binder 路径。 应用进程从自己的 fd 读取并解码消息,再把数据交给 Java。接下来先看应用在哪条线程接手,再看处理完要送回什么。

8.8 应用 Looper 怎样从 Native 回到 Java
mReceiverPtr 是什么时候创建的
在 Java 接收器构造函数里,选定 Looper 决定以后回调在哪条线程执行。正常 ViewRootImpl 传的是应用主线程 Looper:
InputEventReceiver.java · 第 80 行 中的 public InputEventReceiver(InputChannel inputChannel, Looper looper)
mLooper = looper;mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this), inputChannel, mLooper.getQueue());
nativeInit 不是只读一个凭空存在的指针。JNI 先提取并消费 Java channel 包装,取得对应 Native MessageQueue,再创建 NativeInputEventReceiver,注册 fd,保持 Native 强引用并把地址返回给 Java。核心分配与初始化节点如下:
android_view_InputEventReceiver.cpp · 第 761 行 中的 static jlong nativeInit(
sp<NativeInputEventReceiver> receiver = sp<NativeInputEventReceiver>::make(env, receiverWeak, std::move(inputChannel), messageQueue);status_t status = receiver->initialize();
android_view_InputEventReceiver.cpp · 第 773 行 中的 static jlong nativeInit(
receiver->incStrong(gInputEventReceiverClassInfo.clazz); // retain a reference for the objectreturn reinterpret_cast<jlong>(receiver.get());
mReceiverPtr 保存的是本应用进程中的 Native 接收器地址。 接收器拥有 InputConsumer,后者持有通道;它不是 system_server 的 Connection 地址,也不是 socket 中事件数据的地址。initialize → setFdEvents(ALOOPER_EVENT_INPUT) 将 socket fd 与本线程 Native Looper 关联:
android_view_InputEventReceiver.cpp · 第 329 行 中的 void NativeInputEventReceiver::setFdEvents(
const int fd = mInputConsumer->getChannel()->getFd();int res = 0;if (events) { res = mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);} else { res = mMessageQueue->getLooper()->removeFd(fd);
fd 就绪时 Native Looper 调用 handleEvent,然后 consumeEvents 消费消息。它运行在 Looper 所属线程,不是在应用 Binder 线程池中。MessageQueue 的 nativePollOnce 与 Native Looper 的细节可回看第三章。
从 InputMessage 到 Native MotionEvent,再到 Java 包装
consumeEvents 调用 InputConsumer.consume。读取新消息时,mChannel->receiveMessage() 的结果存入 mMsg。下面先从工厂取得 Native MotionEvent,再用这条非批处理的消息填入事件数据。参见 InputConsumer.cpp 的读取分支。
InputConsumer.cpp · 第 354 行 中的 InputConsumer::ConsumeResult InputConsumer::consume(
MotionEvent* motionEvent = factory->createMotionEvent();if (!motionEvent) return ConsumeResult{android::base::Error(NO_MEMORY), unfinishedInputMessages};updateTouchState(mMsg);initializeMotionEvent(*motionEvent, mMsg);*outSeq = mMsg.header.seq;*outEvent = motionEvent;
Native 事件由应用本地工厂取得,可能复用;参数从消息初始化,而不是跨进程传来一个可解引用的 Java 指针。 MOVE 的批处理分支可合并历史样本,必要时重采样,所以不能把每次 socket 消息与每次 Java 回调做无条件一一对应。
JNI 接着为 Java 构造/复用对应事件包装:
android_view_InputEventReceiver.cpp · 第 568 行 中的 status_t NativeInputEventReceiver::consumeEvents(
inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
本次对象转换完成后,JNI 将传输 seq 和 Java 事件一起交给 Java dispatchInputEvent:
android_view_InputEventReceiver.cpp · 第 633 行 中的 status_t NativeInputEventReceiver::consumeEvents(
env->CallVoidMethod(receiverObj.get(), gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj.get());
Java 用映射表保存“Java 事件 sequenceNumber → 传输 seq”,随后调用 onInputEvent:
InputEventReceiver.java · 第 280 行 中的 private void dispatchInputEvent(int seq, InputEvent event)
mSeqMap.put(event.getSequenceNumber(), seq);onInputEvent(event);
WindowInputEventReceiver.onInputEvent 进入当前版本的 processRawInputEvent,先考虑输入兼容处理,再 enqueueInputEvent;旧文中的直接 enqueue 写法不能代替当前入口。正常未变换分支保留接收器和原始事件的关联。
ViewRootImpl.java · 第 11682 行 中的 public void processRawInputEvent(InputEvent event)
enqueueInputEvent(event, mInputEventReceiver, 0, true);
ViewRootImpl 的输入阶段与按帧消费
ViewRootImpl 将事件交给 InputStage 链。键盘事件可能先经过 Pre-IME、IME、Post-IME 等阶段,触摸通常跳向适当的 Post-IME 阶段;系统策略、无障碍或手写识别也可能消费。之后才可能进入 View 树。
Batched MOVE 就绪时,WindowInputEventReceiver.onBatchedInputEventPending 可以用 Choreographer 的 INPUT 阶段安排消费。当前 scheduleConsumeBatchedInput 使用 postVsyncCallback:
ViewRootImpl.java · 第 11409 行 中的 void scheduleConsumeBatchedInput()
mConsumeBatchedInputScheduled = true;mChoreographer.postVsyncCallback(Choreographer.CALLBACK_INPUT, mConsumedBatchedInputCallback);
延迟到合适的帧时刻消费,与应用一直阻塞主线程是两种情况。 请求 unbuffered 输入等分支又会改变消费时机;不能把某一次看到批量 MOVE 推广成 DOWN/UP 总要等待下一帧。
8.9 Activity、ViewGroup 与 View 怎样分配触摸序列
从窗口根进入 Activity,再回到内容树
Post-IME 的 pointer 处理在应用主线程调用 mView.dispatchPointerEvent。默认 Activity 窗口的根是 DecorView:
ViewRootImpl.java · 第 8800 行 中的 private int processPointerEvent(QueuedInputEvent q) {
handled = handled || mView.dispatchPointerEvent(event);
对触摸,View.dispatchPointerEvent 会调用 dispatchTouchEvent。这里的窗口根是 DecorView,实际执行的是它重写的分发方法。参见 View.java 的触摸分支。
DecorView.java · 第 426 行 中的 public boolean dispatchTouchEvent(MotionEvent ev)
final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
DecorView 的 Window.Callback 通常是 Activity,所以示例重写的 Activity.dispatchTouchEvent 能先观察它。Activity 默认继续委托 Window 的 superDispatchTouchEvent 进入 DecorView 的 ViewGroup 处理;未处理则回退 Activity.onTouchEvent:
Activity.java · 第 4674 行 中的 public boolean dispatchTouchEvent(MotionEvent ev)
if (getWindow().superDispatchTouchEvent(ev)) { return true;}return onTouchEvent(ev);
这里看起来“又回到 DecorView”,实际上分别进入窗口回调入口和 super 分发入口,并不是无穷递归。
DOWN 建目标,拦截时取消旧目标
ViewGroup.dispatchTouchEvent 在新 DOWN 时清除前一个手势的 touch targets,查 onInterceptTouchEvent,再按可触摸子 View、坐标和绘制顺序分派。子 View 消费 DOWN 后,会形成 TouchTarget;正常 MOVE/UP 延续到这个目标。
一个父容器开始拦截已经交给子 View 的手势时,框架对旧 child 执行取消处理并从目标链摘除。下面是现有 TouchTarget 遍历中的连续节点:
ViewGroup.java · 第 2933 行 中的 public boolean dispatchTouchEvent(MotionEvent ev)
final boolean cancelChild = (target.child != null && resetCancelNextUpFlag(target.child)) || intercepted;if (target.child != null && dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true;
CANCEL 表示旧目标要结束手势状态,并不表示用户一定抬起了手指。 父容器拦截、系统抢占、窗口变化都可能影响序列。子 View 必须同时处理 UP 与 CANCEL 的收尾;不能只在 UP 清除拖动状态。
示例中默认 Child 消费 DOWN,开启父容器拦截后,手指移动超过阈值,Child 预期收到 CANCEL;后续事件由父容器处理。触发拦截的那个 MOVE 对旧 child 会变成取消交付,不能要求同一个原始 MOVE 先完整给 child,再完整给 parent。
关闭“子 View 消费 DOWN”后,Child 的 DOWN 返回 false,Parent.onTouchEvent 返回 true,Parent 成为这次处理者。之后不能期待该 child 继续收到完整 MOVE/UP。用这个条件对照,比背诵“返回 true 向上/向下传递”更可靠。

图以本例的单指、父容器可以消费为前提。一般 ViewGroup 自己也返回 false 时,还会继续回退。多指 split、安全过滤和嵌套滚动各有规则。requestDisallowInterceptTouchEvent 只影响父容器的拦截判断,嵌套滚动则处理 View 之间的滚动协作。窗口之间的分发仍由 InputDispatcher 决定。
onTouch 返回值与界面更新
View 的 OnTouchListener 有机会先消费,未消费时再走 onTouchEvent。示例没有自行注册 OnTouchListener,直接覆盖 onTouchEvent;UP 时调用 performClick,保留点击语义入口。MotionEvent 中 getX/getY 是当前 View 本地坐标,父子变换会改变它们,而 raw 坐标又属于另一坐标空间。
示例在处理触摸时更新状态并调用 invalidate,随后退出回调。应用回调、FINISHED、渲染、合成和画面呈现各有时间点。 如果日志证明 View 已接到事件,画面却没变化,就沿第七章的绘制链继续查。
8.10 FINISHED、超时与资源释放怎样闭环
Java 事件为什么还要 finish
View 返回 handled 只是应用内部函数结果。ViewRootImpl 的 InputStage 完成本次流水线后,finishInputEvent 把结果交回 q.mReceiver;兼容处理分支还需要对应其处理后的事件。本章跟踪普通未修改分支:
ViewRootImpl.java · 第 11367 行 中的 private void finishInputEvent(QueuedInputEvent q)
q.mReceiver.finishInputEvent(q.mEvent, handled);
InputEventReceiver 查出传输 seq,删除 Java 侧映射并通知 Native:
InputEventReceiver.java · 第 232 行 中的 public final void finishInputEvent(InputEvent event, boolean handled)
int seq = mSeqMap.valueAt(index);mSeqMap.removeAt(index);nativeFinishInputEvent(mReceiverPtr, seq, handled);
这里不是把 Java sequenceNumber 原封不动当作 Dispatcher 的 seq。 方法后续还 recycleIfNeededAfterDispatch,ViewRootImpl 回收自己的 QueuedInputEvent。应用业务若要跨回调保留 MotionEvent,应使用 MotionEvent.obtain 创建自己的副本,并在用完后 recycle;示例只复制数值,不保留传入事件,不能手动 recycle 框架借给回调的对象。
Java 的 Native 声明经 JNI 注册进入 nativeFinishInputEvent;它从本应用 mReceiverPtr 对应的地址恢复接收器,再交给 C++ 方法。这里继续在绑定的 Looper 线程运行:
android_view_InputEventReceiver.cpp · 第 786 行 中的 static void nativeFinishInputEvent(
sp<NativeInputEventReceiver> receiver = reinterpret_cast<NativeInputEventReceiver*>(receiverPtr);status_t status = receiver->finishInputEvent(seq, handled);
NativeInputEventReceiver.finishInputEvent 将 seq/handled 放进 outbound 队列并尝试处理:
android_view_InputEventReceiver.cpp · 第 279 行 中的 status_t NativeInputEventReceiver::finishInputEvent(
Finish finish{ .seq = seq, .handled = handled,};mOutboundQueue.emplace(finish);return processOutboundEvents();
processOutboundEvents 调用 InputConsumer.sendFinishedSignal。若一次 MOVE 合并了传输 seq 7、8、9,Native 消费器把最后一个 seq 交给 Java,同时保留它与前序 seq 的链。Java 完成后,Native 按链先回 7、8,再回 9。单事件的回执只需要自己的 seq。同一 InputChannel 还可传 TIMELINE,供系统接收 GPU 完成与呈现时间。TIMELINE 与 FINISHED 使用不同消息类型。参见 批处理建立 seq chain 和 发送完成确认。
写 socket 暂不可用时,Native 接收器保留待发项并监听 OUTPUT。Java 调完 finish 时,系统端可能尚未收到回执。
android_view_InputEventReceiver.cpp · 第 394 行 中的 status_t NativeInputEventReceiver::processOutboundEvents()
mOutboundQueue.emplace(*outbound);setFdEvents(ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT);return WOULD_BLOCK; // try again later
Dispatcher 怎样结束等待
同一连接的 server 端 fd 收到响应,由 Dispatcher Looper 的 handleReceiveCallback 读取并识别 FINISHED:
InputDispatcher.cpp · 第 4167 行 中的 int InputDispatcher::handleReceiveCallback(
const InputPublisher::Finished& finish = std::get<InputPublisher::Finished>(*result);finishDispatchCycleLocked(currentTime, connection, finish.seq, finish.handled, finish.consumeTime);
finishDispatchCycleLocked 安排命令,再由 doDispatchCycleFinishedCommand 根据 seq 查找并移出 waitQueue,删除该投递的 ANR 跟踪:
InputDispatcher.cpp · 第 6737 行 中的 void InputDispatcher::doDispatchCycleFinishedCommand(
auto entryIt = std::find_if(connection->waitQueue.begin(), connection->waitQueue.end(), [seq](auto& e) { return e->seq == seq; });if (entryIt != connection->waitQueue.end()) { std::unique_ptr<DispatchEntry> dispatchEntry = std::move(*entryIt); connection->waitQueue.erase(entryIt); const sp<IBinder>& connectionToken = connection->getToken(); mAnrTracker.erase(dispatchEntry->timeoutTime, connectionToken);
看图时先找应用的待发队列,再找系统端的 waitQueue。写阻塞时,应用保留待发回执,系统也还保留原条目。写入成功与系统读到消息是两个时间点。

动图展示一次未批处理投递的可能顺序。先看传输 seq 7 从待发队列进入 waitQueue,再看 Java 事件编号 101 怎样映射回 7。应用即使返回未处理,也要送回 FINISHED。最后由 Dispatcher 读到消息并移除等待项。

“未消费”仍然是一种已完成结果:handled=false 的确认不能省略,某些按键还有系统 fallback 处理。Java 事件对象、socket 里的 InputMessage、Dispatcher 的 DispatchEntry 各有生命周期。MotionEvent 回收时,系统端可能还在等回执。
超时是等待条件,不是慢函数名称
startDispatchCycleLocked 为每个 DispatchEntry 记录 timeoutTime,Dispatcher 的 processAnrsLocked 检查期限,并通过策略向 Java 系统层报告不响应。超时取值来自窗口信息/默认值及构建配置,不能把所有设备、所有输入分支永久固定为 5 秒:
InputDispatcher.cpp · 第 1200 行 中的 std::chrono::nanoseconds InputDispatcher::getDispatchingTimeoutLocked(
const sp<WindowInfoHandle> window = mWindowInfos.findWindowHandle(connection->getToken());if (window != nullptr) { return window->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);}return DEFAULT_INPUT_DISPATCHING_TIMEOUT;
当前 onAnrLocked 会先检查 waitQueue 是否已恢复,再记录等待原因并调用 processConnectionUnresponsiveLocked,后者通知策略;事件取消及应用处理还依赖当前状态。没有焦点窗口的等待又是另一分支。
示例的单次 600ms sleep 便于放大输入区间和观察随后事件的延迟,它不能保证触发 Input ANR。sleep 时线程通常处于睡眠,不是占用 CPU 600ms;Dispatcher 不应因为一个应用慢,就阻塞整个线程等 Java 方法返回。
正常窗口退出与进程异常退出
点“关闭此窗口”后,应用侧 ViewRootImpl 释放接收器。系统侧 WindowState.disposeInputChannel 负责移除 Dispatcher 中的连接登记,再清理窗口 token 的关联。接收器的 Native dispose 尝试处理待发送确认,取消 fd 监听,清掉 consumer 并移除相关 Looper 消息:
android_view_InputEventReceiver.cpp · 第 259 行 中的 void NativeInputEventReceiver::dispose()
const status_t result = processOutboundEvents();if (result != OK) { LOG(WARNING) << "channel '" << mName << "' ~ Could not send " << mOutboundQueue.size() << " outbound event(s), status:" << statusToString(result);}setFdEvents(0);// Do not process any more events after the receiver has been disposed.mInputConsumer.reset();mMessageQueue->getLooper()->removeMessages(this);
Java dispose 的契约要求在所属 Looper 线程调用,业务不能跨线程随意关闭框架接收器。InputChannel 的 Native fd 用 unique_fd 管理;包装引用释放到适当条件后关闭描述符,不能把移除 epoll 登记当成 fd 已经关闭。
应用进程异常退出时,Kernel 关闭它持有的 fd。server 端可以收到 HANGUP/ERROR 或读取到 DEAD_OBJECT,Dispatcher 的 handleReceiveCallback 移除连接,abortBrokenDispatchCycleLocked 清理 outboundQueue、waitQueue 等状态;同时 WMS 还会走窗口/客户端死亡清理。没有 Java onDestroy 日志,不代表内核 fd 与系统连接永远泄漏。 两条清理路径可能交错,不能硬画成“先必然执行 onDestroy,再通知系统”。
设备本身拔除则是另一个层次:EventHub 发现 inotify、HUP 或 ENODEV,关闭对应 fd,Reader 清理设备与 mapper 状态,按场景产生取消/重置;evdev_release 在引用条件满足时回收该打开者的 client 队列。应用窗口关闭不等于把系统 InputReader 线程或所有物理设备关闭。
8.11 区分 Host 输入、evdev 写入与 adb 注入
三条入口验证的范围不同
本章默认不提供任意节点 sendevent 的硬编码脚本。协议字段、轴范围、设备拓扑和清理条件不同,盲写 event0 可能产生不完整状态;先按 8.2 识别能力,再研究匹配的 uinput/evdev 协议。上表说明实验边界,不把第三条说成模拟了第一条的全部实现。
input tap 是怎样注入的
当前命令入口在 system_server 的 InputShellCommand。sendTap 直接构造一个 DOWN 和一个 UP 注入:
InputShellCommand.java · 第 489 行 中的 private void sendTap(
final long now = SystemClock.uptimeMillis();injectMotionEvent(inputSource, MotionEvent.ACTION_DOWN, now, now, x, y, 1.0f, displayId);injectMotionEvent(inputSource, MotionEvent.ACTION_UP, now, now, x, y, 0.0f, displayId);
它通过 InputManagerGlobal 和 IMS 的注入接口,检查权限与目标后进入 NativeImpl 的 JNI。MotionEvent 分支直接调用 Dispatcher.injectInputEvent:
com_android_server_input_InputManagerService.cpp · 第 2735 行 中的 static jint nativeInjectInputEvent(
im->getInputManager()->getDispatcher().injectInputEvent(motionEvent, targetUid, mode, std::chrono::milliseconds( timeoutMillis), uint32_t(policyFlags));
这里没有回到 EventHub.getEvents,也没有先写 /dev/input/eventX。 注入的 source 可以声明 TOUCHSCREEN,它表示 Android 事件分类。当前 InputShellCommand 的实现对 DOWN 和 UP 分别以 WAIT_FOR_FINISH 注入。正常成功时会等各自关联的前台投递完成。handled 可以是 false,后续绘制和画面呈现也各有自己的时间点。Shell 包装层没有检查注入 API 的布尔返回值,命令退出码不能替代应用日志。其他调用还可以选择异步或只等待分发结果。
下面以默认屏为例。在已打开实验应用的 Host 终端执行,把坐标选在蓝色区域内。多屏时先查实际 display ID,再用 input touchscreen -d DISPLAY_ID tap 指定目标。
"$chapter8_adb" -s "$chapter8_serial" shell wm sizeread -r -p '输入实验区域内屏幕 X:' chapter8_xread -r -p '输入实验区域内屏幕 Y:' chapter8_y"$chapter8_adb" -s "$chapter8_serial" shell input touchscreen tap "$chapter8_x" "$chapter8_y""$chapter8_adb" -s "$chapter8_serial" shell input keyevent KEYCODE_A
预期触摸到达正确窗口时有 Java DOWN/UP;键盘输出依当前焦点和策略。若坐标点到按钮或系统栏,日志与消费者会不同。手势导航边缘、遮挡、锁屏和多显示也会影响结果,先查窗口/焦点再怀疑 Dispatcher。
8.12 用对照和 Trace 判断事件卡在哪里
对照一 手动输入与 adb 注入
终端 A 按 8.2 初始化设备变量,并用已确认的节点开始监听。手动触摸前先确认观察器没有因权限或节点错误退出。
read -r -p '输入已确认的触摸设备节点名,例如 event4:' chapter8_eventif [[ "$chapter8_event" =~ ^event[0-9]+$ ]]; then "$chapter8_adb" -s "$chapter8_serial" shell getevent -lt "/dev/input/$chapter8_event"else printf '%s\n' '请输入已确认的 event 节点名。' >&2fi
getevent 会持续运行,用 Ctrl+C 结束。终端 B 独立初始化 8.2 的变量,运行 8.3 的 logcat 命令;终端 C 同样初始化后,用来执行 8.11 的 input 命令。各终端的变量不共享。
先用 Emulator GUI 手动点击蓝色区域一次。只有确认选定节点出现对应触摸报告、应用也记录了 DOWN/UP,才用相同区域做一次 adb 注入。两次操作间隔几秒,期间避免其他输入。预期注入仍会到达应用,但选定节点上没有与本次注入对应的 evdev 触摸报告。若手动对照就没有报告,先回 8.2 检查节点、权限和实际输入来源。
getevent 会为自己这次打开的 evdev fd 请求 CLOCK_MONOTONIC。logcat 的 -v monotonic 则把显示时间换算为单调时间。先用动作顺序和两端的大致时间核对同一次点击。若 getevent 提示无法启用单调时钟,或两边时间无法对上,就分开做单次操作并记录各自可见结果,将跨日志时间关联记为未确认。这组对照也不用于计算驱动到 View 的时延。参考 getevent 请求单调时钟 和 liblog 换算显示时间。
对照二 消费与拦截
每一行都是独立对照,先结束上一手势,再按该行设置开关。无额外条件的基线是“父容器拦截 MOVE:关”“子 View 消费 DOWN:是”,且没有待消费的延迟标记;下文说明怎样确认和恢复这个状态。
这些是根据示例和源码给出的预期,不是本轮已执行结果。MOVE 批处理、事件兼容和设备行为可影响日志细节,以动作、角色和消费关系为判据,不以固定行数验收。
延迟 DOWN 的初态、触发与恢复
先完成上一手势的 UP 或 CANCEL,再准备下一轮。恢复消费开关不等于清除了旧的延迟标记。 如果前一轮按过延迟按钮却没有确认执行,或者已不清楚标记状态,先点“关闭此窗口”,再用 8.3 的启动命令重新打开,确认出现新的 onCreate 日志。示例的新 Activity 将 consumeDown 初始化为 true,另外两个布尔字段为 false;无需重启设备或清除应用数据。
1. 先设初态。 将“父容器拦截 MOVE”设为“关”,“子 View 消费 DOWN”设为“是”。从不消费对照恢复时,应看到切换日志 consumeDown=true;从拦截对照恢复时,应看到 interceptMove=false。本来已处于该状态时可依据按钮文字和新实例默认值确认,不必为了产生日志反复切换。
2. 采集内先做基线。 按下面的 Trace 步骤启动采集,在蓝色区域完成一次普通 DOWN/UP;预期有 Child onTouch ACTION_DOWN,没有 Chapter8.DelayDown600ms 区间。不要把点在控制按钮上的 DOWN 当作区域基线。
3. 只设置一次延迟,再触发。 点一次“下一次区域 DOWN 延迟 600ms”,确认日志 armed one DOWN delay=600ms。这条日志只表示标记已设置;随后在蓝色区域按下并抬起,才应在该次 Chapter8.Child.onTouch 内出现 Chapter8.DelayDown600ms,持续约 600ms 或因调度更长。该分支会在 sleep 之前把 delayNextDown 清为 false。
4. 再验证恢复。 等上一手势结束,不再点延迟按钮,在同一区域再完成一次 DOWN/UP。应再次有 Child DOWN,但不再有 DelayDown600ms 区间。判据是这次没有进入延迟分支,不要求所有回调都低于某个固定毫秒数。
若日志中已有 armed one DOWN delay=600ms却没有目标延迟区间,先核对消费按钮是否为“是”、Child 是否实际收到 DOWN、采集是否覆盖了该时刻,以及应用 Trace 是否有效;这轮不能算延迟实验成功。未确认消费的标记可能跨轮残留,按本段开头关闭并重新打开示例后重做,不把之后突然出现的延迟记成正常基线或 Framework 随机卡顿。
对照三 用 Trace 区分睡眠、运行与等待
先把同篇附录 B 的配置与查询保存到 $chapter8_notes/示例/追踪配置/,再执行下列命令。
下面在 8.3 的 Host 终端执行,沿用设备变量和输出目录。先看 atrace 支持的分类。不支持某项时记下来。没有 slice 也可能是采集范围不包含它。
"$chapter8_adb" -s "$chapter8_serial" shell atrace --list_categorieschapter8_trace=if test -d "$chapter8_run" && chapter8_capture=$(mktemp -d "$chapter8_run/trace.XXXXXX"); then chapter8_trace_name="$(basename "$chapter8_run")-$(basename "$chapter8_capture")" chapter8_remote="/data/misc/perfetto-traces/chapter8-$chapter8_trace_name.perfetto-trace" chapter8_local="$chapter8_capture/input.perfetto-trace" if "$chapter8_adb" -s "$chapter8_serial" shell -T perfetto --txt -c - -o "$chapter8_remote" \ < "$chapter8_notes/示例/追踪配置/第八章Input.pbtxt" && "$chapter8_adb" -s "$chapter8_serial" pull "$chapter8_remote" "$chapter8_local" && test -s "$chapter8_local"; then chapter8_trace="$chapter8_local" printf '本轮 Trace:%s\n' "$chapter8_trace" else printf '%s\n' '采集或拉取失败,当前文件不能作为本轮证据。' >&2 fielse printf '%s\n' '请先核对示例运行目录和目录写入权限。' >&2fi
采集前先完成上面的初态准备。Perfetto 前台运行约 14 秒,从标准输入读取本地配置。在这段时间内用另一个终端或 GUI 按 延迟 DOWN 步骤完成基线、目标 DOWN 和恢复 DOWN。正常采集和拉取后终端会打印本轮 Trace 路径。三组动作没有全部被覆盖时,重新采集并保留各自输出。
先把本地 Trace 打开
在 Host 浏览器打开 Perfetto UI:
先在执行 pull 的同一个终端打印实际路径,再通过 Open trace file 选择该文件,或把文件拖进浏览器;文件选择框不会展开 Bash 变量。
test -n "$chapter8_trace" && printf '%s\n' "$chapter8_trace"
载入的是这个文件里的记录,不是自动连接设备的实时画面。用侧栏 Query (SQL) 粘贴附录 B 的 SQL,按 Ctrl+Enter 执行(macOS 为 Cmd+Enter),会得到区间名称、开始/持续时间与进程/线程。时间来自 Trace 时间轴,不是墙上时钟。若没有匹配行,先检查应用 Trace、操作区域与采集是否覆盖目标,不能据此认定输入从未发生。
在顶部搜索 Chapter8.DelayDown600ms,按 Enter 跳到命中的事件,再点击区间看详情;选中区间后按 F 放大。W/S 缩放、A/D 平移,Ctrl+P 查轨道。按日志 PID/TID 对应应用主线程,展开其状态轨道:Running 表示正在 CPU 上运行,Runnable 表示可运行但仍在等调度,Sleeping 表示睡眠;仅凭状态不能推出具体等待原因,仍要与本例 sleep 代码和延迟区间对齐。当前步骤按官方文档核对,本轮没有本例实测 Trace 可供展示。
查看统计时可在 Query (SQL) 执行以下查询。非零不全是错误,需结合 name、severity 与该项含义判断;统计没有报错也不能证明完整覆盖了目标操作。
SELECT name, idx, severity, valueFROM statsWHERE value != 0ORDER BY severity, name, idx;
打开、查询与状态说明依据:Perfetto system tracing、Perfetto UI、PerfettoSQL、Buffers and dataflow、CPU Scheduling。本轮未执行这些设备采集和查询。
读取记录时先查看采集错误和数据源状态,再定位 InputReader、InputDispatcher 和应用主线程,打开 Chapter8.Activity.dispatch、Chapter8.Child.onTouch、Chapter8.DelayDown600ms 等区间。需要结构化查询时用 提取输入区间.sql,其输出只代表实际采到的 slice。
先确定目标回调区间内线程是 Running、Runnable 还是 Sleeping。默认采集可解释的是应用回调区间和线程状态。附录 SQL 只读线程轨,ViewRootImpl 的异步 deliverInputEvent 可能位于其他轨道。要把该事件与另一进程的 socket 或分发记录关联起来,须找到同一个 input event ID 或真实 Flow。仅凭时间相近,记为“跨轨未关联”。600ms 区间可以主要是睡眠;应用慢也不代表 InputReader 连续占用 CPU。 默认配置没有宣称采到完整 Kernel input_event,若要追 Driver 函数或真实 IRQ,须确认可用 tracepoint、权限与设备后端后单独设计采集。
可以用当前窗口/输入状态辅助定位:
"$chapter8_adb" -s "$chapter8_serial" shell dumpsys window > "$chapter8_run/window.txt""$chapter8_adb" -s "$chapter8_serial" shell dumpsys input > "$chapter8_run/input.txt"
先对照名称、display、token、focus、队列和连接状态。字段名随版本变化,可以按正文中的对象和符号找。dumpsys 是快照,waitQueue 中停留很短的条目可能赶不上采样。
本章实验记录应该留下什么
保存源码提交和本地改动、APK 新产物摘要、设备 fingerprint/Kernel、后端绑定信息、命令与退出码、操作时间、关键日志和 Trace 查询。示例日志用 PID/TID、事件动作和 history 说明哪个线程收到了怎样的回调。InputChannel 的传输 seq 需要能观测该字段的系统记录,默认示例日志不打印它。只有跨轨证据能对应到同一事件时,才把系统记录与应用回调连起来。
实际执行后可沿知识库 实验模板在本章新增记录;较大原始 trace 应按知识库证据规则存放。
8.13 常见误区与按层排查入口
两种丢失不是同一个问题
内核 evdev 环形缓冲区溢出可以产生 SYN_DROPPED。当前 InputDevice.process 在该标志后设置丢弃状态,到下一次 SYN_REPORT 重置 mapper;不能把残缺包直接拼成正常手势。应用主线程慢导致的是分发/消费/确认滞后,可能积压或批处理,不等于内核一定溢出了。
设备拔除、evdev 过载和 ANR 的行为来自固定源码。本轮没有触发这些分支,后续做对照时要单独记录权限、故障动作和恢复条件。
8.14 回顾、自测、速查与源码基线
用三个观察点回看一次触摸
设备 Driver 把状态变化报给 Linux input,evdev 将记录放到打开者的队列;InputReader 线程里的 EventHub 等待并读取,mapper 在同步点形成 Android 语义通知。经过策略与处理中间节点后,Dispatcher 入队并在自己的线程里选择窗口、创建派发项,经已建立的 InputChannel 发送消息。应用主线程的 Native Looper 消费事件,JNI 创建/复用 Java 事件,ViewRootImpl 输入阶段再交给 Activity 和 View 树。流水线结束将 handled 与传输 seq 通过 FINISHED 回送,Dispatcher 才移除该次等待与超时跟踪。
窗口通道在添加窗口时创建。正常移除窗口与进程死亡都有相应的回收入口。设备拔除则从 Linux input 一端开始,按设备自己的生命周期处理。
下次蓝色圆点迟迟没动,先在 getevent 里找匹配报告,再在应用日志里确认目标回调和处理结果。要查系统何时收到回执,继续看通道记录。应用已经请求更新但圆点还没画出来时,从 invalidate() 接到第七章的绘制链路。
自测题
1. 为什么触摸不需要每个 MOVE 都调一次 WMS Binder?
2. getevent 与 EventHub 同时读取会不会必然抢走对方事件?
3. 一个 ABS_MT_POSITION_X 记录是否足以构成 MotionEvent.ACTION_MOVE?
4. Reader 调 notifyMotion 的那一行是否已经切到 Dispatcher 线程?
5. InputChannel 的两端是谁创建的,应用在何时拿到自己的 fd?
6. mReceiverPtr 指向谁,在哪个进程创建?
7. 触摸与普通键盘事件为什么可能选择不同窗口?
8. 子 View 不消费 DOWN 后,为什么不保证它还能收到 UP?
9. CANCEL 与 UP 的不同含义是什么?
10. handled=false 是否仍需 FINISHED?传输 seq 与 Java sequenceNumber 相同吗?
11. 为什么一次 Java MOVE 回调可能包含多个历史样本?
12. input tap 能证明触摸 Driver 已执行吗?
13. 600ms onTouch 区间是否代表持续使用 CPU 600ms,是否必然 ANR?
14. 应用被异常终止但没有 onDestroy,哪些机制仍能清理输入连接?
参考答案
1. 窗口/权限与 fd 控制通过 Binder 等管理通路,高频事件走建立好的 Unix socket InputChannel;见 8.6~8.7。
2. 通常不会,evdev 每次打开有各自 client 队列;排他 grab 等分支需单独判断,见 8.4。
3. 不足,mapper 保存多个轴/触点状态,按同步报告与前后状态生成动作,见 8.4~8.5。
4. 不是,先是调用线程执行入队,Dispatcher 自己的 InputThread 等待结束后取出处理,见 8.5。
5. Dispatcher 请求 socketpair,保留 server 端,client 端经窗口添加 Binder 返回;见 8.6。
6. 指向本应用的 NativeInputEventReceiver,由 nativeInit 创建、初始化并保持引用;见 8.8。
7. pointer 通常按坐标/触摸区域等查命中目标,键盘等按焦点窗口与策略;见 8.6~8.7。
8. 未建立该 child 的 TouchTarget;父容器或其他目标可消费该手势,见 8.9。
9. UP 是抬起,CANCEL 是停止当前手势处理,可能来自拦截/抢占/系统状态变化,见 8.9。
10. 需要,它表示完成而非一定消费;Java 映射表把事件编号转换成传输 seq,见 8.10。
11. InputConsumer 批处理、历史样本与按帧消费会合并,见 8.8。
12. 不能,常规系统注入直接进入 Dispatcher,见 8.11。
13. 都不是,区间可主要睡眠;ANR 依据具体等待期限及当前系统状态,见 8.10、8.12。
14. Kernel 关闭进程 fd,server 端 HUP/错误促使移除连接,WMS 也处理窗口/客户端死亡;见 8.10。
按问题找源码
固定源码与实际运行材料分别记录
本章用户空间按本工程 android17-release 对应的固定项目提交阅读,Kernel 按第一章历史镜像线索对应的 ACK 提交阅读。具体标识及各自范围如下。
| 94b4c163b7dfe5ce3607f7bb8456f9573f7de57d | ||
| ae266dcb706d083868578cfedce381ef44488a07 | ||
| 545d2487e38192a2ce25040897ced877cf6b4f53 | ||
| 296e55aa0244e8929e393e00e34471fef2a5d662 | ||
| ccafb60de224d35a8bc14de33e1cd47caaaa786b | ||
| 544e91ec0222069e5477857d16614b453e137340 | ||
第一章 底层来源清单保留镜像、Kernel、配置、模块与 Host 后端证据。本章核对了 Host 预编译 virtio_input.ko 的内嵌版本字段;尚未逐设备关联 eventX、驱动与实际报告,也没有把 Host 预编译程序宣称为某份本地源码的精确构建产物。
公开固定源码:Kernel input、evdev、VirtIO input、虚拟设备配置。
进一步阅读:官方 Input pipeline、getevent、Touch devices。官方说明用于接口与概念背景,本文具体调用与对象关系以上述固定源码为准。
附录 A:Host 构建脚本全文
对应 8.3 的独立 APK 主路线,保存为 $chapter8_notes/示例/构建示例APK.py。Manifest、完整 Java 和可选 Soong 声明已在 8.3 给出;这里给出构建工具脚本,不能把它当成所有 Android 工程的通用构建器。
#!/usr/bin/env python3"""用已构建的 AOSP Host 工具打包第八章无资源示例,不修改系统镜像。"""import argparseimport jsonfrom pathlib import Pathimport subprocessimport zipfiledef main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--aosp-top", type=Path, required=True) parser.add_argument("--module", type=Path, required=True) parser.add_argument("--name", required=True) parser.add_argument("--out", type=Path, required=True) parser.add_argument("--target-sdk", default="36") args = parser.parse_args() top = args.aosp_top.resolve() module = args.module.resolve() output = args.out.resolve() if not (module / "AndroidManifest.xml").is_file(): parser.error("module 需要指向包含 AndroidManifest.xml 的示例目录") if output.exists() and any(output.iterdir()): parser.error("out 目录需为空;请为本轮构建使用新目录") output.mkdir(parents=True, exist_ok=True) host = top / "out/host/linux-x86" sdk = top / "prebuilts/sdk/current/public/android.jar" java = top / "prebuilts/jdk/jdk21/linux-x86/bin/java" javac = java.with_name("javac") jar = java.with_name("jar") required = [host / "bin/aapt2", host / "framework/d8.jar", host / "framework/signapk.jar", sdk, java, javac, jar, host / "lib64/libconscrypt_openjdk_jni.so", top / "build/make/target/product/security/testkey.x509.pem", top / "build/make/target/product/security/testkey.pk8"] missing = [str(path) for path in required if not path.is_file()] if missing: parser.error("缺少已构建的 Host 工具或材料:\n" + "\n".join(missing)) commands = [] def run(command): command = [str(value) for value in command] subprocess.run(command, check=True, cwd=top) commands.append(command) generated, classes, dex = [output / name for name in ("generated", "classes", "dex")] for directory in (generated, classes, dex): directory.mkdir() sources = sorted((module / "src").rglob("*.java")) + sorted(generated.rglob("*.java")) run([javac, "--release", "17", "-cp", sdk, "-d", classes, *sources]) run([jar, "cf", output / "classes.jar", "-C", classes, "."]) run([java, "-cp", host / "framework/d8.jar", "com.android.tools.r8.D8", "--lib", sdk, "--min-api", "28", "--output", dex, output / "classes.jar"]) run([host / "bin/aapt2", "link", "-I", sdk, "--manifest", module / "AndroidManifest.xml", "--min-sdk-version", "28", "--target-sdk-version", args.target_sdk, "-o", output / "unsigned.apk"]) with zipfile.ZipFile(output / "unsigned.apk", "a", zipfile.ZIP_DEFLATED) as archive: for path in sorted(dex.glob("*.dex")): archive.write(path, path.name) apk = output / (args.name + ".apk") run([java, "-Djava.library.path=" + str(host / "lib64"), "-jar", host / "framework/signapk.jar", top / "build/make/target/product/security/testkey.x509.pem", top / "build/make/target/product/security/testkey.pk8", output / "unsigned.apk", apk]) (output / "build-commands.json").write_text(json.dumps(commands, ensure_ascii=False, indent=2)) print(apk)if __name__ == "__main__": main()
附录 B:Trace 配置与查询全文
按文件名保存后回到 8.12,采集命令通过标准输入读取这份配置。若要观察 Driver 或 IRQ,先确认目标设备支持的 tracepoint。
示例/追踪配置/第八章Input.pbtxt
buffers { size_kb: 32768 fill_policy: RING_BUFFER }duration_ms: 14000data_sources { config { name: "linux.ftrace" ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "sched/sched_waking" atrace_categories: "input" atrace_categories: "view" atrace_categories: "wm" atrace_apps: "com.example.inputdemo" } }}data_sources { config { name: "linux.process_stats" process_stats_config { scan_all_processes_on_start: true } } }
示例/追踪配置/提取输入区间.sql
-- 在 Perfetto Trace Processor 中执行。结果仅代表本次 trace 实际采到的 slice。SELECT s.name, ROUND(s.ts / 1e6, 3) AS start_ms, ROUND(s.dur / 1e6, 3) AS duration_ms, p.name AS process_name, t.name AS thread_name, t.tidFROM slice sJOIN thread_track tt ON tt.id = s.track_idJOIN thread t ON t.utid = tt.utidLEFT JOIN process p ON p.upid = t.upidWHERE s.name GLOB 'Chapter8.*' OR s.name GLOB 'deliverInputEvent*' OR s.name GLOB 'dispatchInputEvent*'ORDER BY s.ts;
参考资料
• VirtIO input:virtio_input.c(固定提交)
• Kernel input:input.c(固定提交)
• evdev:evdev.c(固定提交)
• EventHub.cpp(固定提交)
• InputReader.cpp(固定提交)
• TouchInputMapper.cpp(固定提交)
• KeyboardInputMapper.cpp(固定提交)
• InputDispatcher.cpp(固定提交)
• ViewRootImpl.java(固定提交)
• WindowState.java(固定提交)
• InputManagerService JNI(固定提交)
• InputTransport.cpp(固定提交)
• InputEventReceiver.java(固定提交)
• InputEventReceiver JNI(固定提交)
• InputConsumer.cpp(固定提交)
• DecorView.java(固定提交)
• Activity.java(固定提交)
• ViewGroup.java(固定提交)
• InputShellCommand.java(固定提交)
• Perfetto UI
• Perfetto system tracing
• Perfetto UI 使用说明
• PerfettoSQL 入门
• Perfetto buffers and dataflow
• Perfetto CPU Scheduling
• 虚拟设备配置:virtual_device_core.fragment(固定提交)
• AOSP 系统架构
• Input 官方资料
• getevent 使用说明
• Touch devices 官方资料















































































