EN

Android Systrace 基础知识 - Input 解读

字数统计: 5.6k阅读时长: 24 min
2019/11/04
loading

本文是 Systrace 系列文章的第六篇,主要是对 Systrace 中的 Input 进行简单介绍,介绍其 Input 的流程; Systrace 中 Input 信息的体现 ,以及如何结合 Input 信息,分析与 Input 相关的问题

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

版本说明(2026-10-13 技术复核): 保留 2019 年截图和原始 dumpsys 数据,旧实现以 AOSP android-9.0.0_r1 为参照,并补充 Android 16 与 Perfetto 观测边界。原图中的队列/埋点不是所有系统固定接口;本次读取了两个公开附件中的 Trace 文本,没有重新运行设备实验。

本文目录

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

这篇 Input 文章可与 Perfetto 5:Choreographer 渲染流程、Perfetto 7:MainThread 和 RenderThread 一起读。排查时先定位事件进入系统、选择目标、写入 InputChannel、App 接收与处理、完成确认,再关联需要更新的帧和实际呈现。输入确认完成与画面显示完成是两条不同的边界。

Choreographer 篇已经区分了 Looper 消息分发与 native FD 回调。输入通道可读时也会通过 native Looper 进入 Java,因此不能把所有输入都理解成一个普通 Handler Message。本文主要讨论触摸 MotionEvent;按键 KeyEvent 的焦点选择、策略拦截与回退处理另有分支,Home/Back 也不能概括成必定交给当前 App 的普通按键。

典型触摸路径是:

  1. 触摸控制器按设备策略采样,经驱动形成内核输入事件;硬件采样、报告、InputReader 被调度的频率不必一一相同。
  2. EventHub 读取事件,InputReader 按设备/mapper 处理坐标、动作等,再通知后续分发链。
  3. InputDispatcher 根据事件类型、显示、窗口触摸区域、手势状态和策略选择目标连接;不是向所有“注册输入的 App”广播。
  4. App 接收后按适用路径立即或批量处理。UI 状态变化可能促成下一帧更新,请求会合并,不是每个输入都对应一个新帧。

时间轴从左到右,但比较的是已对时的事件/区间边界,跨线程的先后关系并不能单独证明因果。仍要对照 channel、序号、调用栈和调度信息。我希望大家在看基于 Systrace 的源码流程分析之后,脑子里有一个图形化的、立体的流程图,你跟的代码走到哪一步了,在图形里也能快速定位。

Input in Systrace

以下保留桌面单指滑动案例:通常从 DOWN 开始,经过若干 MOVE,以 UP 或 CANCEL 结束;多指还会有 POINTER_DOWN/POINTER_UP,不能把所有手势都固定为 Down-Move-Up。

历史桌面滑动概览:InputReader、Dispatcher、连接队列与 App 轨道

原图中的 InputReader 和 InputDispatcher 是 system_server 的 native 线程。其余标记需要分别理解:

  1. InputReader 处理 EventHub 读入的数据,触摸路径使用 Motion 通知,按键使用 Key 通知。
  2. InputDispatcher 选择目标并维护分发状态。队列和名称属于该版本实现,厂商及新系统埋点可能不同。
  3. oq: 是某个 Connection 的 outbound 队列计数,连接可能对应窗口或 monitor,一个 App 可以有多个窗口/连接。
  4. wq: 记录已成功发布、仍待 finished 协议处理的条目;不是“只有业务返回 true 才算成功”。
  5. aq:pending 是 ViewRootImpl 待取出的输入队列计数,不等于全部正在处理/等待确认的输入数。
  6. deliverInputEvent 是 App 分发处理的跟踪区间,不是内核唤醒时刻;需要另看 sched_wakeup/sched_switch。
  7. 旧查看器的 Input Response / Rendering Response 是交互分析分组,不是通用硬件触摸到屏幕的直接测量。
  8. 手指抬起后,满足手势/速度条件时可以继续 fling;并非每次 UP 必触发 fling。

下面先看 DOWN。MOVE 的批处理、历史采样和重采样差异会影响时序,不能当作可忽略的小差异。

InputDown 事件在 SystemServer 的工作流

原图展示 Reader 工作、Dispatcher 调度以及连接计数变化。颜色和计数提供线索,精确时延要选取对应事件并读时间值:

system_server 中 DOWN 事件相关的 Reader、Dispatcher 和队列计数

InputDown 事件在 App 的工作流

普通 DOWN 通常不走 MOVE 的批量等待路径,可以在输入通道可读后进入分发;这不取决于“系统有没有 VSync”。主线程忙、可运行但未获 CPU 或其他输入阶段等待时,仍可能延后。

Launcher 接收 DOWN 后进入 deliverInputEvent 的历史选区

App 的 Pending 队列

aq:pending 的一个历史采样值为 1

原图说明“处理完变 0”过于简化。Android 9 ViewRootImpl.doProcessInputEvents()在取出一个 QueuedInputEvent 后,先减小 pending 计数,再调用 deliverInputEvent()。所以计数归零时,该事件可能正在处理、等待异步阶段或尚未回传 finished;主线程阻塞在 native 通道读取之前时,这个 Java 队列也不一定先堆起来。

主线程处理 Input 事件

图中可见 native poll → InputEventReceiver → ViewRootImpl 输入阶段 → DecorView/Activity/View 分发。这是该事件的路径,不表示所有类型都经过同一组 stage,也不能从 MethodTrace 插桩时长直接推正常运行成本。

ViewRootImpl 输入阶段进入 DecorView、Activity 与 View 的调用栈

关键知识点和流程

正常路径可以概括为:Reader 通知 Dispatcher → 事件进入 inbound/pending → 选择目标并进入各连接 outbound → 发布成功后进入 wait → App 处理 → 通过同一输入通道返回 finished 序号和 handled 状态 → Dispatcher 处理确认并清理或按策略继续。

数据和确认主要走 InputChannel socket 协议,不是每个触摸都用 Binder 回调 Java InputManagerService。handled=false 也可以是一次合法处理结果,finished 不等于 UI 已经重绘。异常连接清理也可能让队列减少,因此要结合连接状态读计数。

InputReader

以 AOSP android-9.0.0_r1 为源码参照:EventHub 监听输入设备 FD 及设备变化;每轮读出的 RawEvent 由 mapper 处理。该版本缓冲数组容量为 256,指 RawEvent 容量,不是 256 个完整手势、采样率或所有版本固定常量。触摸形成 NotifyMotionArgs/MotionEntry 路径,Key 才是 NotifyKeyArgs/KeyEntry。

下面的历史图包括 interceptKeyBeforeQueueing 等按键分支,不能当作所有触摸都必经的函数链:

Gityuan 的历史 InputReader 示意图,包含按键策略分支

下面是 Android 9 的完整 loopOnce() 方法,可以看到阻塞读取和 listener flush 均在 Reader 锁外;它仍属于系统源码,不是独立应用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void InputReader::loopOnce() {
int32_t oldGeneration;
int32_t timeoutMillis;
bool inputDevicesChanged = false;
Vector<InputDeviceInfo> inputDevices;
{ // acquire lock
AutoMutex _l(mLock);

oldGeneration = mGeneration;
timeoutMillis = -1;

uint32_t changes = mConfigurationChangesToRefresh;
if (changes) {
mConfigurationChangesToRefresh = 0;
timeoutMillis = 0;
refreshConfigurationLocked(changes);
} else if (mNextTimeout != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
}
} // release lock

size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

{ // acquire lock
AutoMutex _l(mLock);
mReaderIsAliveCondition.broadcast();

if (count) {
processEventsLocked(mEventBuffer, count);
}

if (mNextTimeout != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
if (now >= mNextTimeout) {
#if DEBUG_RAW_EVENTS
ALOGD("Timeout expired, latency=%0.3fms", (now - mNextTimeout) * 0.000001f);
#endif
mNextTimeout = LLONG_MAX;
timeoutExpiredLocked(now);
}
}

if (oldGeneration != mGeneration) {
inputDevicesChanged = true;
getInputDevicesLocked(inputDevices);
}
} // release lock

// Send out a message that the describes the changed input devices.
if (inputDevicesChanged) {
mPolicy->notifyInputDevicesChanged(inputDevices);
}

// Flush queued events out to the listener.
// This must happen outside of the lock because the listener could potentially call
// back into the InputReader's methods, such as getScanCodeState, or become blocked
// on another thread similarly waiting to acquire the InputReader lock thereby
// resulting in a deadlock. This situation is actually quite plausible because the
// listener is actually the input dispatcher, which calls into the window manager,
// which occasionally calls into the input reader.
mQueuedListener->flush();
}

InputDispatcher

Reader 的 listener 通知可能使 Dispatcher 入队并唤醒,但策略过滤和其他唤醒来源也存在。下图的具体 Runnable 选区显示唤醒来自 TID 25669(图中 Reader),不能将此推广到每一次 Dispatcher 唤醒:

一次 InputDispatcher Runnable 选区记录 wakeup from tid 25669

Android 9 Dispatcher分别处理 Key 与 Motion。Key 常围绕焦点窗口和按键策略;触摸还要命中测试、维护手势目标、处理 split/cancel 等情况。下面旧图中的 dispatchKeyLocked、findFocusedWindowTargetsLocked、publishKeyEvent 明确是 Key 分支,不能直接替代本文的触摸路径。

Gityuan 的历史 InputDispatcher Key 分支示意图

下面用流程伪码概括队列边界:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatchOnce:
obtain or continue the pending EventEntry
select eligible targets for the event type and window/touch state
enqueue a DispatchEntry for each selected connection
publish through that connection's InputPublisher/InputChannel
if publication succeeds:
move the entry from outboundQueue to waitQueue
otherwise:
handle backpressure or connection errors

on consumer FINISHED(seq, handled):
locate that connection's matching dispatch entry
process completion / possible fallback policy
retire or requeue the entry as required
continue pending dispatch work

deliveryTime 在这版 startDispatchCycleLocked() 尝试发布条目时设置;不是硬件采样时刻,也不能不顾后续等待把最初进入 dispatchOnce 的时间当成所有事件统一的投递时刻。WOULD_BLOCK 等背压/错误需要单独处理,不能先假定已经进入 wait。策略命令还可能通过命令队列在合适的锁边界执行。

InboundQueue

notifyKey 与 notifyMotion 经相应检查后可封装并入队。取出条目成为 mPendingEvent 后,iq 计数可以减少,但该事件仍可能等待目标或继续分发,所以 iq=0 不代表系统输入工作全做完。

iq 记录 Dispatcher inbound 队列长度,取出不等于处理结束

OutboundQueue

这是每个 Connection 待发布的条目队列;同一输入可能按规则有多个目标,也可能被过滤。一个进程有多个窗口,monitor 又有独立用途,不能把 Connection 简化成“一 App 一个队列”。下图选中的是 StatusBar 连接,值为 1,不是 Launcher 的所有输入数。

选中的 StatusBar outbound 连接计数为 1

WaitQueue

发布成功后,Dispatcher 将条目从 outbound 移入 wait。App 的 finishInputEvent(event, handled) 经 native InputConsumer 发送确认;Dispatcher 按连接和序号处理后再移除相应条目,必要时还涉及回退策略。详见 InputTransport 和 InputEventReceiver。

wait 长时间增长是调查线索,原因可能是 App 未获调度、处理耗时、MOVE 批量等待、确认传送/处理或连接问题。长度不等于时长,也不是 ANR 判据。 下图来自 sample.tencent.mat... 的另一份列表案例,选区 32.276 ms,一个计数值为 8;不能把它当成桌面那份 Trace 或八个事件都尚未进入 App。

另一列表案例中的 wait 计数 8 与 32.276ms 选区

整体逻辑

原图来自 Gityuan 博客,包含 KeyboardInputMapper/notifyKey;它是历史阅读线索,不是 Android 16 所有触摸输入的精确调用链。现代源码仍需按自己版本跟踪。

Gityuan 的历史输入时序图,主体为 KeyboardInputMapper 与 notifyKey

Input 刷新与 Vsync

那么,是不是屏幕采样率越高越好?先区分触摸采样率、输入报告/调度频率、显示刷新率和 App 实际帧率。120Hz 的周期约 8.33ms,160Hz 为 6.25ms;原文的 120Hz“8ms”只是粗略近似,且这些不是今天所有设备的固定规格。

历史图标注约 160Hz 采样与 MOVE 批量期间的 wait 变化

我当时在图中把设备标为 160Hz,但 Reader 工作间隔还受报告批次和调度影响。WaitQueue 短暂积累也不等于 App 永远不消费。批量 MOVE 可在 Choreographer INPUT 阶段按帧时间消费,DOWN/UP 等不能用同一规则概括。

多个采样点不等于只留最新、其余全丢。 MotionEvent 的 batching 合同允许一个 ACTION_MOVE 携带多个历史样本;应用可先读 getHistoricalX/Y() 和历史时间,再读当前样本。Android 9 InputConsumer.consumeBatch/consumeSamples()会选取合适时间范围的样本、加入 MotionEvent history,并维护对应确认序号链;启用重采样时还会根据帧时间和样本估计位置。

因此不能从 60Hz 显示推出“120Hz 触摸足够”,也不能从 90Hz 推出“必须 180Hz”。更高采样可能减少部分输入等待或提供更细数据,也可能增加功耗/处理负担;2/3 个样本交替成批本身并不证明 UI 必抖。要结合事件时间、历史点/重采样、实际呈现与端到端延迟做对照,不要求采样率必须恰好为刷新率两倍。

Input 调试信息

1
adb shell dumpsys input

这个命令输出比较多,我截取了 Device、InputReader 和 InputDispatcher 三段来看。以下保留当时的设备快照。字段和格式以设备版本为准;它不是整段交互的连续记录。unknown、空字段与大整数哨兵应按其含义解释。

Device 信息

设备路径、Descriptor、vendor/product 和映射文件属于这份快照,event6 不是所有手机固定的触摸节点,也不能仅凭这些示例值判定厂商。

1
2
3
4
5
6
7
8
9
10
11
12
13
    3: main_touch
      Classes: 0x00000015
      Path: /dev/input/event6
      Enabled: true
      Descriptor: 4055b8a032ccf50ef66dbe2ff99f3b2474e9eab5
      Location: main_touch/input0
      ControllerNumber: 0
      UniqueId: 
      Identifier: bus=0x0000, vendor=0xbeef, product=0xdead, version=0x28bb
      KeyLayoutFile: /system/usr/keylayout/main_touch.kl
      KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
      ConfigurationFile: 
      HaveKeyboardLayoutOverlay: false

Input Reader 状态

Raw 与 Cooked 坐标分开:例中 raw (660, 1338) 转为 cooked (659.389, 1337.401),反映映射/校准后的结果。PhysicalWidth 这里明确是 px,不是毫米;unknown range 不等于零范围。External Stylus 的 9223372036854775807 是该旧状态中的无有效截止时间等哨兵,不能当成真实输入时刻。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  Device 3: main_touch
    Generation: 24
    IsExternal: false
    HasMic:     false
    Sources: 0x00005103
    KeyboardType: 1
    Motion Ranges:
      X: source=0x00005002, min=0.000, max=1079.000, flat=0.000, fuzz=0.000, resolution=0.000
      Y: source=0x00005002, min=0.000, max=2231.000, flat=0.000, fuzz=0.000, resolution=0.000
      PRESSURE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      SIZE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
    Keyboard Input Mapper:
      Parameters:
        HasAssociatedDisplay: false
        OrientationAware: false
        HandlesKeyRepeat: false
      KeyboardType: 1
      Orientation: 0
      KeyDowns: 0 keys currently down
      MetaState: 0x0
      DownTime: 521271703875000
    Touch Input Mapper (mode - direct):
      Parameters:
        GestureMode: multi-touch
        DeviceType: touchScreen
        AssociatedDisplay: hasAssociatedDisplay=true, isExternal=false, displayId=''
        OrientationAware: true
      Raw Touch Axes:
        X: min=0, max=1080, flat=0, fuzz=0, resolution=0
        Y: min=0, max=2232, flat=0, fuzz=0, resolution=0
        Pressure: min=0, max=127, flat=0, fuzz=0, resolution=0
        TouchMajor: min=0, max=512, flat=0, fuzz=0, resolution=0
        TouchMinor: unknown range
        ToolMajor: unknown range
        ToolMinor: unknown range
        Orientation: unknown range
        Distance: unknown range
        TiltX: unknown range
        TiltY: unknown range
        TrackingId: min=0, max=65535, flat=0, fuzz=0, resolution=0
        Slot: min=0, max=20, flat=0, fuzz=0, resolution=0
      Calibration:
        touch.size.calibration: geometric
        touch.pressure.calibration: physical
        touch.orientation.calibration: none
        touch.distance.calibration: none
        touch.coverage.calibration: none
      Affine Transformation:
        X scale: 1.000
        X ymix: 0.000
        X offset: 0.000
        Y xmix: 0.000
        Y scale: 1.000
        Y offset: 0.000
      Viewport: displayId=0, orientation=0, logicalFrame=[0, 0, 1080, 2232], physicalFrame=[0, 0, 1080, 2232], deviceSize=[1080, 2232]
      SurfaceWidth: 1080px
      SurfaceHeight: 2232px
      SurfaceLeft: 0
      SurfaceTop: 0
      PhysicalWidth: 1080px
      PhysicalHeight: 2232px
      PhysicalLeft: 0
      PhysicalTop: 0
      SurfaceOrientation: 0
      Translation and Scaling Factors:
        XTranslate: 0.000
        YTranslate: 0.000
        XScale: 0.999
        YScale: 1.000
        XPrecision: 1.001
        YPrecision: 1.000
        GeometricScale: 0.999
        PressureScale: 0.008
        SizeScale: 0.002
        OrientationScale: 0.000
        DistanceScale: 0.000
        HaveTilt: false
        TiltXCenter: 0.000
        TiltXScale: 0.000
        TiltYCenter: 0.000
        TiltYScale: 0.000
      Last Raw Button State: 0x00000000
      Last Raw Touch: pointerCount=1
        [0]: id=0, x=660, y=1338, pressure=44, touchMajor=44, touchMinor=44, toolMajor=0, toolMinor=0, orientation=0, tiltX=0, tiltY=0, distance=0, toolType=1, isHovering=false
      Last Cooked Button State: 0x00000000
      Last Cooked Touch: pointerCount=1
        [0]: id=0, x=659.389, y=1337.401, pressure=0.346, touchMajor=43.970, touchMinor=43.970, toolMajor=43.970, toolMinor=43.970, orientation=0.000, tilt=0.000, distance=0.000, toolType=1, isHovering=false
      Stylus Fusion:
        ExternalStylusConnected: false
        External Stylus ID: -1
        External Stylus Data Timeout: 9223372036854775807
      External Stylus State:
        When: 9223372036854775807
        Pressure: 0.000000
        Button State: 0x00000000
        Tool Type: 0

InputDispatcher 状态

重点区分 FocusedApplication 与 FocusedWindow、各显示的触摸目标、窗口列表、monitor 通道与连接。MonitoringChannels 不是每个 Window 的普通输入通道清单;下面也只是摘录。DispatchFrozen 是分发状态,不是后来的 cached-app freezer 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Input Dispatcher State:
  DispatchEnabled: 1
  DispatchFrozen: 0
  FocusedApplication: name='AppWindowToken{ac6ec28 token=Token{a38a4b ActivityRecord{7230f1a u0 com.meizu.flyme.launcher/.Launcher t13}}}', dispatchingTimeout=5000.000ms
  FocusedWindow: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}'
  TouchStatesByDisplay:
    0: down=true, split=true, deviceId=3, source=0x00005002
      Windows:
        0: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', pointerIds=0x80000000, targetFlags=0x105
        1: name='Window{8cb8f7 u0 com.android.systemui.ImageWallpaper}', pointerIds=0x0, targetFlags=0x4102
  Windows:
    2: name='Window{ba2fc6b u0 NavigationBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x21840068, type=0x000007e3, layer=0, frame=[0,2136][1080,2232], scale=1.000000, touchableRegion=[0,2136][1080,2232], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    3: name='Window{72b7776 u0 StatusBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x81840048, type=0x000007d0, layer=0, frame=[0,0][1080,84], scale=1.000000, touchableRegion=[0,0][1080,84], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    9: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', displayId=0, paused=false, hasFocus=true, hasWallpaper=true, visible=true, canReceiveKeys=true, flags=0x81910120, type=0x00000001, layer=0, frame=[0,0][1080,2232], scale=1.000000, touchableRegion=[0,0][1080,2232], inputFeatures=0x00000000, ownerPid=27619, ownerUid=10021, dispatchingTimeout=5000.000ms
  MonitoringChannels:
    0: 'WindowManager (server)'
  RecentQueue: length=10
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (524.5, 1306.4)]), policyFlags=0x62000000, age=61.2ms
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (543.5, 1309.4)]), policyFlags=0x62000000, age=54.7ms
  PendingEvent: <none>
  InboundQueue: <empty>
  ReplacedKeys: <empty>
  Connections:
    0: channelName='WindowManager (server)', windowName='monitor', status=NORMAL, monitor=true, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    5: channelName='72b7776 StatusBar (server)', windowName='Window{72b7776 u0 StatusBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    6: channelName='ba2fc6b NavigationBar (server)', windowName='Window{ba2fc6b u0 NavigationBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    12: channelName='3c007ad com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher (server)', windowName='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: length=3
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (634.4, 1329.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=17.4ms, wait=16.8ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (647.4, 1333.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=11.1ms, wait=10.4ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (659.4, 1337.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=5.2ms, wait=4.6ms
  AppSwitch: not pending
  Configuration:
    KeyRepeatDelay: 50.0ms
    KeyRepeatTimeout: 500.0ms

这份输出有 3 个 MOVE 等待项,age 为从事件时间到 dump 时刻的差,wait 为从该条目的 deliveryTime 到 dump 时刻的差;都不是画面响应总耗时。5000.000ms 是这里报告的分发超时设置,不是所有设备、所有 ANR 类型的统一规则。官方输入分发 ANR 说明给出的默认值为 5 秒,也讨论无焦点窗口等场景;判断时需关联超时日志、目标窗口和阻塞原因,不能把 3 个待确认事件直接叫 ANR。

新版本中怎么继续查

AOSP Android 16 的 InputDispatcher已有 Perfetto 输入事件追踪后端,但该标签明确要求 userdebug/eng 构建并开启相应 feature flag。不能承诺普通 user 设备只开一个开关就有 android.input.inputevent 数据。

支持并采到相关 ATrace 时,Perfetto 的 android.input 模块可进一步拆分发送、接收、处理和 ACK 往返;结构化 android.input.inputevent 则有独立数据来源和字段。两者不能混为一套必定存在的数据。输入到帧的关联还可能缺失或属于推测,ACK 往返不是物理触摸到屏幕的完整时间;缺数据记为不可用,不能当零延迟。

参考

本文部分图文参考和拷贝自下面几篇文章,同时下面几篇文章讲解了 Input 流程的细节部分,推荐大家在看完这篇文章后,如果对代码细节感兴趣,可以仔细研读下面这几篇非常棒的文章。

  1. http://gityuan.com/2016/12/11/input-reader/
  2. http://gityuan.com/2016/12/10/input-manager/
  3. http://gityuan.com/2016/12/17/input-dispatcher/
  4. https://zhuanlan.zhihu.com/p/29386642

附件

附件目录包含 trace_input_blog_launcher_scroll.html.zip 和 trace_input_blog_list_jank.html.zip,分别是桌面滑动与列表卡顿案例;解压后为带旧查看器的 HTML。本次确认两份文件可读取,内部包含 process dump 与 ftrace 数据,且目标进程不同。旧 HTML 查看器在当前浏览器的兼容性没有在本次复核中验证;也可用支持旧 Systrace 数据的工具导入并核对事件。
点此链接下载文章所涉及到的 Systrace 附件

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - Input 解读

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 本文目录
  • 系列文章目录
  • 正文
  • Input in Systrace
    1. 1. InputDown 事件在 SystemServer 的工作流
    2. 2. InputDown 事件在 App 的工作流
      1. 2.1. App 的 Pending 队列
      2. 2.2. 主线程处理 Input 事件
  • 关键知识点和流程
    1. 1. InputReader
    2. 2. InputDispatcher
    3. 3. InboundQueue
    4. 4. OutboundQueue
    5. 5. WaitQueue
    6. 6. 整体逻辑
  • Input 刷新与 Vsync
  • Input 调试信息
    1. 1. Device 信息
    2. 2. Input Reader 状态
    3. 3. InputDispatcher 状态
    4. 4. 新版本中怎么继续查
  • 参考
  • 附件
  • 本文其他地址
  • 关于我 && 博客