这篇补一个很容易被启动速度和流畅度盖住的话题:交互首帧响应。它关心用户刚操作完之后,屏幕多久给出第一帧反馈;页面完整加载和动画后半段稳定性要用其他指标衡量。
点一个按钮,多久出现按压态;点微信会话,多久看到会话页第一帧开始动;手指滑动列表,多久看到内容跟着手指移动。这些问题和启动慢有关,也和掉帧有关,但分析口径要单独拎出来。
本文用 Perfetto 拆这段时间:从 InputReader read_time 起算,到 App 收到事件,再到第一帧 present。主指标叫 input_to_present_ms,只度量 input read 到关联帧 present;它不覆盖触控 IC、驱动上报前延迟、显示扫描和面板响应。读完之后,你应该能给一个点击或滑动场景定义起点、终点、采集配置、SQL 指标和 App 侧补充 marker。
文章把响应慢拆成 read/dispatch、handling、ACK→first frame、滑动跟手四个独立指标,每个指标都配上采集配置、UI 上先看哪几条轨道和 SQL 写法,最后归纳五条常见归因路径,以及高速相机和 Perfetto 各自的边界。
Perfetto 系列文章
- Android Perfetto 系列目录
- Android Perfetto 系列 1:Perfetto 工具简介
- Android Perfetto 系列 2:Perfetto Trace 抓取
- Android Perfetto 系列 3:熟悉 Perfetto View
- Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
- Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
- Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
- Android Perfetto 系列 7:MainThread 和 RenderThread 解读
- Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
- Android Perfetto 系列 9:CPU 信息解读
- Android Perfetto 系列 10:Binder 调度与锁竞争
- Android Perfetto 系列 11:PerfettoSQL、Trace Processor 与回归检测
- Android Perfetto 系列 12:Trace 数据流与丢失排查
- Android Perfetto 系列 13:Perfetto SDK、Track Event 与 App 现场 Trace
- Android Perfetto 系列 14:heapprofd 与内存 Profiling
- Android Perfetto 系列 15:Boot Trace、长时间现场 Trace 与偶发问题抓取
- Android Perfetto 系列 16:GPU、Power Counters 与硬件瓶颈分析
- Android Perfetto 系列 17:专项场景自动化、问题清单与平台体系
- Android Perfetto 系列 18:响应速度实战,从输入事件到首帧反馈
- 视频(B站) - Android Perfetto 基础和案例分享
- 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享
本文只看交互后的第一帧
旧的 Systrace 响应速度系列已经讲过广义响应慢:启动、页面跳转、亮灭屏、解锁、系统繁忙、Binder 等场景。流畅度系列讲连续帧是否稳定。Perfetto 系列第 07、08 篇又把 MainThread、RenderThread、Vsync、SurfaceFlinger 的帧路径拆过一轮。
这一篇只补一段:
1 | 用户输入 |
本文把主指标统一叫 input_to_present_ms:从 android_input_events.read_time 到关联帧 present,SQL 字段对应 end_to_end_latency_dur。它回答的是“点了以后多久有第一帧反馈”,不是“整个页面多久加载完”,也不是“动画播放过程中有没有掉帧”。标准库给出的 present 是候选关联:is_speculative_frame=false 只表示输入处理切片与同线程 doFrame 相交,不能证明业务因果或目标 layer 已发生视觉变化。下文查询统一输出 estimated_input_to_present_ms;只有另行核对目标 layer、frame token、实际 present 与业务状态后,才能把它提升为已确认的 input_to_present_ms。
如果用 event_time 或业务 marker 作为起点,报告要另列字段,不能和 input_to_present_ms 混在一起比较。

示意图:ACK 与帧显示是不同阶段;候选 present 还需结合 layer 和业务状态确认,不能直接当用户可见首帧。
响应慢要拆成四个指标
响应慢的讨论经常混在一起。开始分析前,先把同一次交互拆成四个指标。
| 指标 | 起点 | 终点 | 适合回答的问题 |
|---|---|---|---|
| Dispatch latency | InputDispatcher 发出事件 | App 收到事件 | 系统分发和 InputChannel 是否慢 |
| Handling latency | App 收到事件 | App 发出 ACK | App input 处理是否慢 |
input_to_present_ms |
input read (read_time) |
关联帧 present | 用户多久看到第一帧反馈 |
| Completion latency | 业务触发点 | 页面或动画稳定 | 整个操作多久完成 |
用户最敏感的是 input_to_present_ms。第一次反馈快,后续内容分批出来,用户会觉得系统有响应;第一次反馈慢,后续动画再稳定,也容易被感知成“点了没反应”。
点击和滑动的终点也不一样:
- 点击按钮:终点可以是按压态 present、弹窗第一帧 present、页面转场第一帧 present。
- 点击会话:终点可以是会话页进入动画第一帧 present,也可以是第一条消息内容绘制完成。
- 手指滑动:终点应该是内容第一次发生可见位移的 present。
Perfetto 能帮你定位系统内部时间,但“第一帧是否真的有视觉变化”经常需要 App marker 或外部视觉工具校准。
还要区分三个“输入时间”:
event_time: 输入事件发生时间,比read_time更靠前,但仍不是触控固件、电信号或外部可见起点。read_time: InputReader 读到事件的时间,android_input_events的 end-to-end 字段从这里算到 present。dispatch_ts/receive_ts: InputDispatcher 发出事件、App 接收事件的时间,用来拆系统分发和 App 处理。
报告里不要混着写“从点击开始”。如果你用 read_time 起算,就写 input read;如果你用业务 click handler 起算,就写 App marker。起点不同,数字不能直接比较。
在写 SQL 前,还要把本次场景的对象字典固定下来。至少包括目标包名、InputChannel、目标窗口、目标 layer、业务 marker 和预期终点:
1 | scenario_id: conversation_click |
自动化脚本可以先按包名和窗口名生成候选对象,但报告只使用人工确认过的对象。否则一次 Trace 里有多个窗口、弹窗、SurfaceView 或 overlay 时,SQL 很容易把输入事件关联到错误的 layer。
多显示器和折叠屏要在字典层面单独处理。输入事件是按 display 派发的:外接屏、多屏车机、折叠屏内外屏上可能同时存在同名窗口和同名 channel。android_motion_events / android_key_events 里有 display_id 列,可以按 event id 确认事件落在哪个 display;round-trip 表 android_input_events 没有 display 列;原始 inputevent 数据可用时,先统一 event ID 表示,再与这两张表核对。ATrace 字符串 ID 可能是十六进制,原始 event_id 是整数,不能直接把两列当字符串等值连接。折叠屏开合还会触发 display 切换和窗口重建,layer 名和 channel 名会换代,跨形态的数据不要合进同一条基线;scenario 字典里把 posture(折叠/展开)当成独立维度记录,横向比较只在同形态内做。
交互场景也要版本化成 schema。点击场景的最小契约可以这样写:
1 | schema_version: input_response_v1 |
滑动场景要把终点从“帧 present”细化成“内容第一次可见位移的 present”:
1 | schema_version: input_response_v1 |
Trace 采集配置
采集分两层:
| preset | build/场景 | 数据源 | 输出口径 |
|---|---|---|---|
lab_input_debug |
userdebug/eng、实验室短窗口 | input/view/gfx ATrace、FrameTimeline、sched、App marker,另加原始 android.input.inputevent |
round trip + 候选 input-to-present,另行确认视觉终点 |
field_input_response_low_overhead |
user/field,平台受控采集 | App marker、FrameTimeline/sched、按版本可得的 input/view ATrace、trigger metadata | 有完整 ATrace 时可查 round trip;否则退到 App marker 口径 |
本地或实验室分析可以打开更完整的 input 数据源。这份短 Trace 配置用于复现点击/滑动响应,重点看 android.input.inputevent 和 atrace_categories: "input":
1 | buffers { |
android.input.inputevent 面向 debuggable system build,也就是 userdebug/eng 这类受控环境。TRACE_MODE_TRACE_ALL 会记录系统处理的输入事件,只适合本地设备和测试。
field/user build 上不要依赖 android.input.inputevent。如果 debuggable/受控环境里要降低隐私风险,也只能用 TRACE_MODE_USE_RULES、严格匹配规则、secure/IME/spy window 审查和访问控制;规则模式不能把它变成普通线上采集能力。Input 事件可能同时发给前台窗口、SystemUI、IME 或 spy window,match_any_packages / match_all_packages 设计不当会扩大采集范围。
field preset 要把 secure window、IME、spy window、多目标分发、脱敏、留存周期和访问控制写进评审项。
普通 App 不能自己开启系统 ftrace、FrameTimeline 或 android.input.inputevent session。<profileable android:shell="true" /> 适合让 release 包开放 CPU/内存 profiler 等本地 profiling;Android 12+ App tracing 默认可用于所有 App,Android 11 及以下的 android.os.Trace 仍要注意 profileable/debuggable 边界。
但它不会让普通 user build App 获得系统 inputevent、ftrace 或 SurfaceFlinger session 的自启动权限。
android.input 同时包含两类来源:android_key_events/android_motion_events 来自原始 android.input.inputevent;android_input_events 则从 ATrace 的 sendMessage/receiveMessage、deliverInputEvent、UnwantedInteractionBlocker::notifyMotion 等切片重建往返和帧关联。两者不能混为一谈。没开原始 inputevent 仍可能有 round trip;只开原始 inputevent 而缺 ATrace,也不会自动得到 round trip。实际可用性取决于系统版本、这些埋点是否存在及 input/view/gfx 等 category 是否采到。
fallback 要提前固定:
| 缺失信号 | 还能输出什么 | 不能输出什么 |
|---|---|---|
缺原始 android.input.inputevent |
ATrace 完整时仍可重建 round trip 与候选帧关联 | 原始 motion/key 详情与 display 信息 |
| 缺输入 ATrace 的 send/receive 或 ACK 切片 | App marker、FrameTimeline/调度上下文 | 完整 round trip;原始 inputevent 表不能直接替代 |
| 缺 FrameTimeline | input round trip、App marker、线程和 SurfaceFlinger 线索 | associated frame、input_to_present_ms |
| SurfaceView/游戏/视频 | App marker、layer、外部视频、厂商工具 | 仅凭 FrameTimeline 认定首个可见变化 |
| data loss / clock sync 异常 | 残留证据里的局部观察 | 强结论和跨数据源精确耗时 |
FrameTimeline 也要写边界:它需要 Android 12 及之后版本。官方文档当前说明 SurfaceView 不受支持,所以视频、游戏、地图这类场景不能只靠 FrameTimeline 判断首帧可见变化,要结合 layer、App marker、外部视频或厂商图形工具校准。
input_to_present_ms 只能说明某个输入事件关联到某帧 present;是否为目标 UI 的首个可见变化,还要由 target layer、业务 marker、scroll counter、截图/视频或领域工具确认。frame_id 有值也不是视觉变化的充分证明。
本地深入排查时,还可以配合 Winscope 的 SurfaceFlinger layers/transactions 数据源看窗口和 layer 状态。但这类数据源可能很重,尤其 MODE_ACTIVE、buffer、HWC 等 trace flag 会明显增加数据量,只适合短时间实验室 Trace。
线上现场仍然要依靠第 13、15、17 篇里的受控 preset、trigger、业务 marker 和隐私规则。
采完 trace 后先跑数据质量检查。input 分析高度依赖时间顺序和 FrameTimeline 关联,ftrace 或中央 buffer 丢数据时,报告要降级:
1 | SELECT name, idx, severity, source, value |
这些异常要按 inputevent、FrameTimeline、ftrace/sched、central_buffer、clock_sync 分组输出;任一关键组异常,都要降低 input_to_present_ms 的证据等级。
linux.sys_stats 能补 CPU 频率采样,但频率判断仍要看 idle、cluster/cpuset、uclamp、thermal、窗口前一条 freq 状态,以及目标线程是否真的在对应 CPU/cluster 上运行。这个 preset 默认不打开 IRQ、softirq、workqueue;怀疑 hardirq、kworker 或 softirq 抢占时,要另开短窗口专项 preset。
IRQ/softirq/workqueue 只进短窗口专项:irq/irq_handler_entry、irq/irq_handler_exit、irq/softirq_entry、irq/softirq_exit、irq/softirq_raise、workqueue/workqueue_execute_start、workqueue/workqueue_execute_end。采完必须检查 ftrace overrun/dropped,不能把这些事件放进默认 field preset。
Perfetto UI 里先看哪几条轨道
一次点击或滑动响应,建议按这个顺序看:
- InputReader / InputDispatcher:确认事件是否读到,是否派发给目标窗口。
- App 主线程:确认事件是否到达,收到后是否马上 Running。
Choreographer#doFrame:确认状态变化是通过Input、Insets Animation、Animation、Traversal还是Commit推进到下一帧。input receive/ACK 不一定发生在 doFrame 里,batched input 也可能通过CALLBACK_INPUT消费。- RenderThread:确认 UI 线程提交后,渲染线程有没有卡在
syncFrameState、DrawFrame、dequeueBuffer、queueBuffer等阶段。 - FrameTimeline:确认关联帧的 expected/actual、token、
layer_name和帧关联方式。 - SurfaceFlinger:确认 buffer 是否及时 latch,是否因为 HWC/GPU composition、acquire/release/present fence 或其他 layer 影响最终上屏。
- CPU scheduling/frequency:确认主线程、RenderThread、InputDispatcher 有没有 Runnable 等待、频率不足、迁核异常。
分析时不要从最长的 slice 开始猜。先把输入事件和第一帧反馈连起来,再判断时间花在哪一段。
点击场景:从 DOWN/UP 到第一帧
点击分析要先选起点。不同产品对“点击响应”的定义不同。
| 场景 | 推荐起点 | 推荐终点 |
|---|---|---|
| 按钮按压态 | ACTION_DOWN 或 input read | 按压态第一帧 present |
| 点击确认 | ACTION_UP 或 click handler | 目标 UI 第一帧 present |
| 页面跳转 | click handler / 业务 marker | 转场第一帧 present |
| 内容可用 | click handler / 业务 marker | 首屏内容 marker |
以“点会话进入聊天页”为例,Perfetto 里按这几步看:
- 在
android_input_events或 InputDispatcher 轨道上找到对应 ACTION_UP。 - 看 dispatch 到 App receive 的耗时,判断系统分发是否慢。
- 看 App 主线程收到事件后是否被及时调度。
- 看 click handler 里是否有 Binder、IO、锁等待、同步布局、图片解码。
- 看
startActivityBinder、Activity launch、窗口创建、focus 切换、starting window、transition 和目标 layer 可见性是否插入等待。 - 看下一次
Choreographer#doFrame是否把状态推进到Animation、Traversal或Commit。 - 看 RenderThread、FrameTimeline 和 SurfaceFlinger,确认关联帧是否真的 present。
- 回到 App marker 和对象字典,确认这一帧是否就是用户可见的会话页第一帧。
这里最容易误判的一点是:Perfetto 可能显示 App 画了一帧,但这一帧没有视觉变化。App actual timeline 的结束点是 App 完成并提交帧,不等于用户已经看到。
确认首帧反馈时,要回到 android_input_events.end_to_end_latency_dur 的关联帧、SurfaceFlinger actual/display frame、layer_name、token/flow 和外部证据。没有业务 marker 时,分析者只能靠 UI 截图、layer 名称或外部视频判断。
事件契约先写清,再落到代码里:
| 事件 | 类型 | 线程 | 语义 | 报告字段 | 隐私 |
|---|---|---|---|---|---|
Conversation#Click |
slice | UI thread | click handler 执行窗口 | business_marker_ts、click_handler_dur |
不带会话 id |
Conversation#TransitionStart |
short slice milestone | UI thread | 业务认为转场开始 | transition_marker_ts |
不带页面参数 |
Conversation#FirstContentDrawn |
short slice milestone | UI thread / render callback | 首屏内容业务完成候选 | content_marker_ts、content_marker_seen |
不带内容文本 |
Feed#ScrollOffsetY |
counter | UI thread | 列表可见位移 | first_offset_change_ts、offset_delta_px |
只记录数值 |
Conversation#FirstContentDrawn 属于 business_state_marker,不是 visual_endpoint。最终报告必须写 present_ts、associated_frame_id、layer_name、association_method;没有 present 关联时,只能写 content_marker_seen=true,不能输出 input_to_present_ms。
下面的代码用于给会话点击场景补三个业务点。读者重点看命名:事件名固定,动态 ID 不进入 trace section。
1 | fun onConversationClick(conversationId: String) { |
这三个 marker 分别对应点击处理、转场开始、首屏内容完成。后两个是 milestone 风格的短 slice,报告只取 ts,不把 dur 当业务耗时;需要严格 instant、参数或 flow 时,用 Perfetto SDK Track Event。Perfetto 负责告诉你系统时间,marker 负责告诉你业务语义;终点仍要取关联帧的 present。
android.os.Trace / ATrace 适合放稳定 slice 名和 counter。动态的会话 id、页面 id、实验分组不要拼进 section 名;需要参数、flow 或跨线程 track 时,用第 13 篇的 Perfetto SDK Track Event,或者把业务元数据写进同一个 case 包。
marker 只是业务状态,present 时间来自关联帧。报告里要把 business_marker_ts、associated_frame_id、present_ts、association_method 分开写。
滑动场景:从第一个有效 MOVE 到内容位移
滑动响应比点击更容易混淆。用户手指刚动时,系统会收到很多 MOVE,但 App 可能还没有越过 touch slop,也可能因为事件合并、采样和 Vsync 节奏,只有某一帧才真的产生内容位移。
推荐口径是:
1 | 第一个业务有效 ACTION_MOVE -> 内容第一次可见位移的 present |
分析步骤:
- 找到 ACTION_DOWN 后的 MOVE 序列。
- 排除 touch slop 内的小幅 MOVE,选出业务认为开始滑动的 MOVE。
- 看 App 主线程是否收到这次 MOVE,是否及时 ACK。
- 看
Choreographer#doFrame的Input阶段是否更新 scroll offset。 - 看下一帧是否提交给 RenderThread。
- 看 FrameTimeline present 时间。
- 用 App counter 或外部视频确认内容是否发生位移。
Perfetto 自己不总能知道“内容移动了多少”。自研 App 可以加一个轻量 counter。下面记录每次非零消费位移,保留第一次变化的时间;如需每帧合并,须另加帧级调度,并记录合并后的采样延迟,不能假设 onScrolled 一帧只调用一次;报告同时写 first_effective_move_ts、first_offset_change_ts、offset_delta_px、touch_slop_px 和 sample_policy。
1 | var gestureScrollY = 0L // 新手势开始时归零,并记录一次基线 counter。 |
android.os.Trace.setCounter(String, long) 从 API 29 起可用;低 minSdk 项目要用 AndroidX tracing 或自己加版本保护。大范围 scroll offset 可用 SDK Track Event counter 或分桶值。这个 counter 不要放在每次布局细节里打爆,只记录用户可见状态,例如 scroll offset、播放器 position、当前 scene id。
computeVerticalScrollOffset() 是滚动条估算值,单位由 LayoutManager 决定,不宜直接当成准确位移 px。这里累加 dy 得到本次手势消费的内容位移;它仍不是屏幕已经移动的证明,item animation、嵌套滚动和数据刷新还要单独校准。先算 move_to_offset_change_ms,再核对确实承载该状态的帧及其 present,不能随便取“下一帧”当视觉终点。

示意图:先量有效 MOVE 到滚动状态变化,再核对承载该状态的帧及 present;counter 更新不是屏幕已移动的证明。
用 SQL 量化输入延迟
先 INCLUDE PERFETTO MODULE android.input,再检查输入 ATrace 能否组成 send → receive → finish → ACK。当前实现对这四段做内连接:任一缺失,该事件可能整行消失,尤其不能据此排除未 ACK 的 ANR。read_time 和帧关联是额外信息,可能为空;缺 FrameTimeline 时仍可能有 round trip。
本文不使用 Chrome EventLatency。Android App 侧以 android.input / android_input_events 和 FrameTimeline 为准;WebView/Chrome 场景另按 Chrome 模块分析。
先选择目标输入事件,避免同一进程里的 DOWN/MOVE/UP、IME、overlay 或多个窗口事件混在一起。
动手前先确认这份 Trace 里 action 到底长什么样,不要照搬 Android 常量名。event_action 是从 trace 里的 slice 名切出来的,Perfetto 自己的测试数据里是 HOVER_MOVE、SCROLL 这种不带 ACTION_ 前缀的写法,event_type 则可能是 MOTION,也可能是 0x1 这类原始值,取决于系统版本中 InputTransport 的 ATrace 命名,不能用它判断是否启用了原始 inputevent 数据源:
1 | INCLUDE PERFETTO MODULE android.input; |
确认取值之后再精确匹配。*UP 还会匹配 POINTER_UP,不能用来选择单指点击。下面按当前切片命名选择 UP,并固定目标进程的 click marker;marker 和输入 channel 要对应接收点击的来源窗口,不一定是跳转后的目标 Activity:
1 | INCLUDE PERFETTO MODULE android.input; |
目标事件选定后,再看 input round trip。重点看 dispatch、handling、ack 和 input-to-present 四列:
1 | INCLUDE PERFETTO MODULE android.input; |
字段解释如下:
dispatch_latency_dur: InputDispatcher 发出事件到 App 收到事件。read_to_dispatch_ms: InputReader 读到事件到 InputDispatcher 发出事件,能暴露分发前排队、policy/interception 或 Dispatcher 前置等待。handling_latency_dur: App 输入通道收到事件到 finish/ACK 的框架口径。它不是 click handler 专属耗时;要定位业务处理,必须再用 ViewRootImpl/InputStage slice、App marker 或 click handler section 切分。ack_latency_dur: App 发出 ACK 到系统收到 ACK。total_latency_dur: dispatch 到 ACK 完成。end_to_end_latency_dur: input read 到标准库候选 present 的差;关联失败时为NULL。它需要额外校验目标 layer、帧 token、帧顺序和非负耗时。is_speculative_frame: true 表示选择同线程未来 doFrame,false 表示输入处理与 doFrame 时间相交。它不校验目标 layer 的视觉变化。当前实现随后按同进程后续非 dropped 帧寻找 present,不能把它当成严格的 input → 指定 surface token 因果链。
end_to_end_latency_dur 为 NULL 不一定代表没有视觉反馈。它可能是事件没有关联到 FrameTimeline 帧,也可能是反馈发生在 SurfaceView、游戏引擎或另一个 layer 上。遇到这种情况,要回到 App marker、SurfaceFlinger、外部视频或领域工具,不要把 NULL 写成“没有绘制”。
排查慢输入时,先查 round-trip 慢事件,不过滤 end_to_end_latency_dur:
1 | INCLUDE PERFETTO MODULE android.input; |
round-trip 查询按 COALESCE(read_time, dispatch_ts) 选窗,避免 read_time 缺失时把仍有 dispatch/ACK 的事件过滤掉。只有要排序候选 input-to-present 时,才额外要求 end_to_end_latency_dur >= 0;关联标志、目标 layer 和实际 present 仍须复查。
如果 read_to_dispatch_ms 高,先看 InputDispatcher 状态、窗口目标、policy/interception、焦点窗口等待、IME/spy/secure window,再看 system_server 调度;不要直接从 read_to_dispatch 高跳到 CPU/freq 结论。如果 handling_ms 很高,优先回到 App 主线程看 ViewRootImpl/InputStage、click handler、Binder、锁和 IO。
如果 handling_ms 不高但 input_to_present_ms 高,优先看 post-ACK 的 App/Framework 异步路径、Activity/Fragment transaction、数据回调、下一帧生产、RenderThread、FrameTimeline、SurfaceFlinger 和调度。
调度部分也要分 Running 和 Runnable。前置条件是 sched_switch、wakeup/waking 完整,且没有 ftrace_cpu_has_data_loss、overrun/dropped 或 ftrace_setup_errors;否则只能写“残留证据中看到 Runnable/Running”,不能写“没有调度等待”。
这条查询示例覆盖 App main、RenderThread、system_server 的 InputReader/InputDispatcher、WindowManager 相关线程和 SurfaceFlinger。实际工程应由 scenario_objects 驱动 role in ('input_reader','input_dispatcher','window_manager','app_main','render_thread','surfaceflinger'),线程名只作为候选:
1 | WITH target_window AS ( |
这里用 thread_state 的 Running/Runnable 状态统一统计,缺记录或只覆盖部分窗口时耗时留空。full 也只表示区间覆盖,不能替代 stats 检查;线程只在窗口的一部分存活时,应缩小窗口后再算。
Runnable 高说明线程想运行但没拿到 CPU,优先看其他 runnable task、RT 任务、cgroup/cpuset、uclamp/capacity、thermal、迁核和频点;Running 高说明线程已经在 CPU 上消耗时间,再去看函数级热点、Binder、锁或布局绘制。唤醒方向要再看 thread_state 的唤醒信息、IRQ context 或相邻 ftrace 事件,不能只靠 CPU 忙做结论。
常见归因路径
Read/dispatch 慢
表现通常是 read_to_dispatch_ms 或 dispatch_latency_dur 高。优先检查:
- InputReader 到 InputDispatcher 之间是否排队。
- InputDispatcher 是否忙。
- system_server 是否 Runnable 等待。
- 窗口焦点、目标窗口、InputChannel 状态是否异常。
- 是否存在 policy/interception、IME、overlay、焦点窗口等待或 WindowManager 状态切换。
- CPU 是否被其他 runnable task、RT 任务、cgroup/cpuset/uclamp、thermal 或迁核策略影响。
这类问题偏平台侧,App 只能提供页面和操作上下文。
Handling 慢
表现通常是 handling_latency_dur 高。优先检查:
- App 主线程收到 input 后是否长时间 Running。
Input阶段是否做了重计算。- click handler 是否同步发 Binder、读文件、等锁、等网络回调。
- 是否有同步布局、图片解码、复杂 Compose recomposition 或 RecyclerView 大量 bind。
这类问题大多能靠 App marker 和主线程 slice 定位。
ACK 快,但 first frame 慢
这种情况很常见:App 很快 ACK 了 input,用户仍然觉得反馈慢。优先检查:
- App 是否没有请求下一帧,或者请求发生得太晚。
- ACK 之后是否在等 Binder 回调、Activity/Fragment transaction、数据回调或窗口切换。
Animation/Traversal是否排在后一个 Vsync。- RenderThread CPU 是否卡在
syncFrameState、DrawFrame。 dequeueBuffer/queueBuffer是否说明 BufferQueue backpressure。- App GPU 完成、acquire/release fence、present fence 是否拖住这一帧。
- SurfaceFlinger/HWC/GPU composition 是否让 display frame 晚于预期。
- 主线程或 RenderThread 是否 Runnable 等待 CPU。
这里要把 input round trip 和帧生产分开看。ACK 快只说明 App 处理输入完成,不等于第一帧已经显示。
ANR 和亚 ANR
交互响应慢多数是亚 ANR 问题:用户已经感到慢,但还没到系统弹 ANR 的程度。Input ANR 主要分两类看:
- 事件已经发给连接,App 长时间没有 ACK,通常会体现在
handling_latency_dur或 ACK 相关时间异常。 - focused event 一直等不到 focused window,通常要看 WindowManager / ActivityTaskManager、窗口创建、焦点切换和 InputDispatcher 状态。
ACK 快,但 first frame 慢 一般不会触发 input ANR,因为输入事件已经处理完;它更像首帧生产或窗口可见性问题。
滑动不跟手
滑动不跟手经常表现为第一帧位移晚、后续帧又恢复稳定。优先检查:
- 第一个有效 MOVE 到 scroll offset counter 变化的时间。
- scroll offset 变化到 present 的时间。
- MOVE 是否被合并,App 是否等到下一次 Vsync 才处理。
- 主线程是否 Runnable 等待,导致第一帧错过当前 Vsync。
- 后续 FrameTimeline 是否稳定;如果稳定,问题主要在起步 latency。
这个场景要避免只看 jank count。用户说“不跟手”,常常说的是输入到第一帧的延迟。
高速相机和 Perfetto 的边界
Perfetto 记录的是系统内部时间,用户眼睛看到的时间还包含触控采样、触控固件处理、显示扫描、面板响应和外部拍摄误差。做竞品、验收或厂商横向对比时,外部视觉测量仍然必要。
推荐组合是:
1 | 高速相机/机械手: |
外部测量给结果,Perfetto 给原因,App marker 给业务语义。三者合在一起,响应速度问题才不会停留在“体感慢”的层面。
一个最小 walkthrough 应该长这样:先定位 ACTION_UP 的 input_event_id,看到 handling_ms = 18、read_to_dispatch_ms = 3、dispatch_ms = 4;再看 系统收到 ACK 后候选帧晚了 50ms,其中 App 主线程在 doFrame 前 Runnable 21ms,FrameTimeline 显示关联帧 missed 1 vsync。
这个证据串可以写成“倾向 App 主线程调度等待导致首帧晚一拍”,但如果 is_speculative_frame = true 或没有外部视频,就不能写成“确认用户可见延迟 76ms”。
一份响应速度报告可以固定成下面的格式:
1 | scenario: conversation click |
这个格式要求报告同时写起点、终点、拆分、证据和边界。证据等级固定为:确认 confirmed、倾向 likely、排除 excluded、降级 degraded、未知 unknown,报告字段里统一使用英文等级。is_speculative_frame = true、缺 FrameTimeline、缺外部视频、data loss、fallback 路径都会降低等级。响应速度问题不能只给一个总耗时;总耗时说明用户慢在哪里感知到,拆分才说明工程上该找谁。没有外部测量时,报告只能写系统内部延迟,不能写用户可见绝对延迟。
收束
交互首帧响应的分析顺序可以固定下来:
- 定义起点:DOWN、UP、有效 MOVE、业务 click。
- 定义终点:按压态第一帧、转场第一帧、内容第一次位移、首屏内容完成。
- 用
android_input_events拆read_to_dispatch、dispatch、handling、ack、input_to_present_ms。 - 用 App marker 说明业务状态,用关联帧 present 作为屏幕终点。
- 用 FrameTimeline、RenderThread、SurfaceFlinger、CPU 调度解释第一帧为什么早或晚。
- 用高速相机校准用户可见延迟。
启动速度看的是一个长场景,流畅度看的是连续帧稳定性,交互首帧响应看的是用户动作后的第一次反馈。三者分开,Perfetto 里的证据才会清楚。
参考文档
- PerfettoSQL standard library - android.input
- TraceConfig reference - AndroidInputEventConfig
- Android Jank detection with FrameTimeline
- ATrace: Android system and app trace events
- Track events
- Trace configuration
- CPU Scheduling events
- Buffers and dataflow
- Capture traces with adb commands - Input
- Android
<profileable>manifest element - ANR detection in InputDispatcher
- ViewRootImpl input pipeline
源码核对版本:b23c67be1159e3f0b0657bd97be9e139e0d8ee2c(2026-09-19)。
关于我 && 博客
欢迎关注 Android Performance。