snowCrane

← 返回文章列表

2026-08-03 · WordPress 归档

多指地图缩放中的 Input Bad Stream 分析与 InputVerifier 演进

多指地图缩放中的 Input Bad Stream 分析与 InputVerifier 演进 一、问题现象 在 […]

多指地图缩放中的 Input Bad Stream 分析与 InputVerifier 演进

一、问题现象

在地图页面进行双指缩小时,偶发出现如下错误:

Bad stream: Received ACTION_UP but no pointers are currently down

表现为地图缩放中断,后续触摸事件无法正常继续分发。问题发生在真实双指操作过程中,并非单指操作。

压测过程中还出现过另一类日志:

There are currently 2 touching pointers, but the incoming POINTER_DOWN event has 2

这表示当前输入序列重复发送了 POINTER_DOWN。它与“收到没有对应按下状态的 ACTION_UP”属于不同问题,不能混为一谈。

二、事件处理链路

驱动
  ↓
InputReader:读取并组装原始事件
  ↓
InputDispatcher:选择窗口并发送事件
  ↓
OutboundQueue / WaitQueue
  ↓
InputChannel socket
  ↓
应用主线程 Looper
  ↓
应用消费事件并返回处理结果

InputVerifier 位于 framework 向应用发布事件的路径上,用于检查触摸流的状态一致性,例如 pointer 是否重复按下、抬起是否有对应按下、连续事件中的 pointer 集合是否一致等。

三、日志分析

3.1 原始问题中的异常事件

脱敏后的典型日志如下:

XXX InputTransport: Bad stream: XXX
    Received ACTION_UP but no pointers are currently down for device DeviceId(XXX)

这条日志的含义是:InputTransport 在校验发送给目标连接的事件流时,发现当前 verifier 中没有处于按下状态的 pointer,但收到了 ACTION_UP。它描述的是“发送流的状态不一致”,并不直接说明事件一定由驱动错误生成。

分析这条日志时,需要同时查看异常前后的事件序列:

... ACTION_DOWN
... POINTER_DOWN
... MOVE
... WOULD_BLOCK / outbound retry
... ACTION_UP

如果 ACTION_UP 之前的 DOWNPOINTER_DOWN 没有成功发送,或者 verifier 已经因失败重试提前更新状态,就可能出现 framework 侧认为“当前没有 pointer 按下”的情况。

3.2 UnwantedInteractionBlocker 日志

脱敏后的相关日志形式为:

XXX UnwantedInteractionBlocker: enqueueOutboundMotionLocked: tag=XXX

该日志说明运动事件进入了 outbound 处理路径,通常需要和以下信息一起看:

  • 事件的 action 和 pointer 数量;
  • 事件序号或 eventId;
  • 目标窗口/连接是否仍然有效;
  • sendMessage() 的返回值;
  • 是否随后出现 outbound retry、WOULD_BLOCKACTION_CANCEL

单独出现 enqueueOutboundMotionLocked 不能判断事件异常,也不能证明驱动重复上报。只有当它和发送失败、队列重试以及 verifier 状态异常在同一时间窗口内出现时,才支持“应用消费不及时导致发送路径积压”的判断。

3.3 WOULD_BLOCK 日志

脱敏后可按如下形式整理:

XXX InputTransport: sendMessage failed, status=-2147483639 (WOULD_BLOCK)
XXX InputDispatcher: retry outbound event, seq=XXX, eventId=XXX

WOULD_BLOCK 表示非阻塞 socket 当前暂时不可写。它通常与应用主线程未及时读取 InputChannel、发送缓冲区积压有关。该错误是暂时性的,框架会保留事件并在后续重新尝试发送。

重点不是单独统计 WOULD_BLOCK,而是确认:

  1. 发送失败时 verifier 是否已经更新;
  2. 同一事件重试成功时 verifier 是否再次更新;
  3. 重试期间是否插入了取消、抬起或新的多指事件。

3.4 驱动、Reader 与 Framework 时间点

日志中的时间戳可能来自不同层级,不能直接按打印顺序判断事件产生顺序。建议对同一事件建立如下记录:

driver_time=XXX
reader_time=XXX
dispatcher_enqueue_time=XXX
send_time=XXX
app_receive_time=XXX
seq=XXX eventId=XXX action=XXX pointerCount=XXX

如果 driver_time 中存在对应 UP,说明 framework 至少不是该 UP 的唯一来源;如果驱动没有该事件,而 Reader 或 Dispatcher 出现,则应进一步检查 Reader 转换、取消补发和重试逻辑。

四、原因分析

4.1 应用主线程卡顿是重要触发条件

地图缩放期间,应用主线程可能同时执行地图引擎计算、布局、绘制或其他耗时任务,导致应用不能及时从 InputChannel 读取事件。

主线程卡顿不会凭空生成一个 ACTION_UP,但会导致:

  • 事件消费延迟,应用侧 pointer 状态更新滞后;
  • framework 侧发送队列和 socket 缓冲区积压;
  • 非阻塞发送返回暂时不可写,后续进入重试;
  • framework、驱动和应用日志中的时间点出现偏移;
  • 应用恢复处理时,看到的事件状态可能已经与 framework 当前状态不同。

因此,主线程阻塞更适合被定义为异常输入流的触发和放大条件,而不是唯一事件来源。

4.2 WOULD_BLOCK 与重试

错误码 -2147483639 对应 WOULD_BLOCK,表示非阻塞发送暂时无法继续写入,通常意味着 socket 发送缓冲区当前不可写。它不等于事件已经丢失,也不能单独证明驱动产生了错误事件。

如果发送失败后从 OutboundQueue 重试,而 verifier 在发送成功前就更新状态,则同一个逻辑事件可能在失败尝试和成功重试中重复推进 verifier 状态,导致 verifier 看到的状态与真实发送流不一致。

4.3 UnwantedInteractionBlocker 的含义

UnwantedInteractionBlocker: enqueueOutboundMotionLocked 表明运动事件进入了 outbound 相关处理路径。该日志本身不能证明事件异常,也不能直接证明事件来自驱动重复上报。需要结合事件序号、action、pointer 数量和发送返回值进行判断。

4.4 是否为驱动多报了一个 UP

目前不能仅凭 framework 的 Bad stream 日志认定驱动多上报了 UP。应按同一事件序号或时间窗口对齐:

层级 关键数据
驱动 原始 type/code/value、硬件时间戳
InputReader 读取时间、转换后的 action、pointer 状态
Dispatcher 入队、发送、重试、取消时间
InputChannel sendMessage() 返回值及错误码
应用 收到事件时间、主线程开始/结束处理时间

如果原始驱动事件中不存在对应 UP,但 Reader 或 Dispatcher 之后出现,应继续检查 framework 的取消、补发和重试逻辑;如果驱动原始记录已经包含该事件,则不能归因于 framework 重复生成。

五、修复方案

5.1 Google InputVerifier 源码问题分析(截至 2024-09-23)

在 Google upstream 截至 2024-09-23 的 InputTransport/InputPublisher 实现中,motion event 的发布路径存在一个时序问题:事件在调用 sendMessage() 之前就执行了 InputVerifier 校验并推进内部 pointer 状态。

简化后的旧流程如下:

InputPublisher::publishMotionEvent()
  → InputVerifier::processMovement()
  → InputChannel::sendMessage()

正常发送时该顺序没有明显问题。但当应用主线程处理不及时、socket 缓冲区暂时不可写时,sendMessage() 可能返回 WOULD_BLOCK。此时事件并没有真正发送到应用侧,verifier 却已经记录了该事件的状态。

随后 Dispatcher 从 outbound queue 重试同一个事件,又会重新经过发布路径。如果 verifier 再次处理该事件,就可能出现以下不一致:

  • DOWNPOINTER_DOWN 被重复计入;
  • verifier 中记录的 active pointer 数量与实际已发送事件不一致;
  • 后续 UP 被判断为“没有对应按下状态”;
  • 合法的重试被误报为 Bad stream

因此,截至该时间点的源码实现存在“发送结果”和“verifier 状态更新”不同步的问题。它不意味着每次 WOULD_BLOCK 都会触发 Bad stream,也不意味着必然导致 system_server 重启,但在多指事件密集、应用主线程卡顿和发送重试同时出现时,会显著放大异常概率。

该问题的关键是:InputVerifier 应验证最终成功发布给应用的事件,而不是验证每一次可能失败的发送尝试。

5.2 调整 verifier 更新时机

将处理流程调整为:

构造事件
  → 调用 sendMessage()
  → 发送成功
  → 更新 InputVerifier 状态

发送因 WOULD_BLOCK 失败时,不推进 verifier 状态;从 outbound 队列重试并真正发送成功后,再更新状态。这样可以避免同一逻辑事件因发送重试被 verifier 重复处理。

5.3 增强诊断信息

建议记录以下信息:

  • 事件序号、eventId、action、pointer 数量;
  • 驱动、Reader、Dispatcher 和应用侧时间点;
  • sendMessage() 返回值及 WOULD_BLOCK 次数;
  • OutboundQueue、WaitQueue 的入队、出队和重试;
  • verifier 失败时的 pointer 状态摘要。

日志应避免记录设备序列号、用户坐标、应用包名和内部路径等敏感信息。

六、InputVerifier 为什么会被引入

InputVerifier 的核心目的不是提升发送性能,而是在事件发给应用之前尽早发现不一致的触摸流,避免错误状态继续传播,典型检查包括:

  • 重复 DOWN
  • 没有对应 DOWNUP
  • MOVE 时 pointer 集合发生非法变化;
  • POINTER_DOWN / POINTER_UP 状态错误;
  • 多指触摸状态不一致。

早期实现发现异常后会直接报告严重错误。随着 Android 版本演进,校验范围、灰度开关、可观测性和异常恢复能力逐步增强,以降低异常输入流对系统稳定性的影响。

七、Google upstream 演进概览

Android 阶段 提交 主要变化
Android 14 开发阶段,2023-02-22 92c8fd5ad1 / f06b672b3e 首次引入 touch stream verifier,检查发往 InputChannel 的触摸流一致性
Android 14,2023-06-14 5c02a71995 将 InputVerifier 迁移到 Rust
Android 14,2023-06-28 1160ecdfa2 增加 hover enter/move/exit 校验
Android 14,2023-07-18 227a7f8fd9 将 flags 纳入事件一致性检查
Android 14,2023-09-19 2d151ac3e2 限制 verifier 只检查 pointer 类型输入
Android 15 前后,2023-08-23 96818960a7 增加事件校验 feature flag
Android 15,2024-10-16 72090cbd1c 将 publish 阶段的 verifier 更新移动到 sendMessage() 成功之后,避免 WOULD_BLOCK 重试导致重复更新
Android 15/16,2025-01-17 fb477b13991e37d1e8bd26640c9988 增加 button press/release 状态校验,期间经历测试问题和后续调整
Android 16,2025-04-04 2042f272e8 增加 injection verifier 的 dumpsys input 输出,提升问题定位能力
Android 17,2025-10-02 a2548f859d 增加 downTime 合法性校验
Android 17,2025-10-16 bf29210a32 将 downTime 校验拆为独立 feature flag,便于灰度启用
Android 17,2025-12-10 36fda36aea 支持 absolute mode captured touchpad 事件流校验
Android 17,2026-02-03 f1179754c5 优化 display 移除/折叠场景下 verifier 的生命周期处理
Android 17,2026-03-04 ffe2eae576 使用 unique_ptr 管理 InputPublisher 中的 InputChannel,明确对象所有权

其中,72090cbd1c 与本问题关系最直接:它修正了发送失败重试路径中的 verifier 更新时机。

八、验证方法

建议对原始版本和修复版本执行相同次数的双指缩放,并分别测试正常负载、应用主线程人为阻塞两种场景,统计:

  • Bad stream 次数;
  • WOULD_BLOCK 次数;
  • outbound 重试次数;
  • 重复 POINTER_DOWN 和异常 ACTION_UP 次数;
  • ACTION_CANCEL 次数;
  • 应用主线程 dispatch 延迟和丢帧情况。

最终需要确认三点:

  1. 驱动原始事件与 framework 事件可以按序号或时间准确对应;
  2. WOULD_BLOCK 重试不会重复推进 verifier 状态;
  3. 修复后 Bad stream 下降,同时压测脚本自身不再生成非法 pointer 序列。

九、结论

该问题更可能是“多指缩放期间应用主线程处理不及时 + 非阻塞发送重试 + verifier 更新时机不合理”共同作用的结果。现有证据不能直接证明驱动多报了 UP,需要通过驱动原始事件与 framework 事件的跨层时序比对完成最终归因。

修复的关键是保证 verifier 只对真正成功发送给应用的事件更新状态,并通过完整的事件时序日志和正确的压测脚本验证修复效果。