Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Android View 渲染管线与分析方法

遇到渲染问题,我们先确定 Producer、Surface、layer(合成图层)和合成位置,再沿具体线程与 fence(同步完成信号)往下分析。标准 View 管线在这里只充当分类基线,SurfaceView、TextureView、Flutter、Camera 和视频会改变其中若干节点。

源码基线:AOSP android-17.0.0_r1;内核 android17-6.18-2026-06_r6。

按 Producer、Surface 与合成位置分类

为什么需要理解渲染管线

Perfetto 里看到一帧超时,我们先回答三个问题,再去看各段 trace slice(追踪时间区间) 有多长。直接盯着最长的那段,容易把原因归到错误的阶段:

  1. 谁在生产本帧 buffer(图像缓冲区)?
  2. buffer 写入哪个 Surface,是否形成独立 layer?
  3. 本帧在哪里合成,又在哪个时间边界完成 present(帧呈现)?

第一问的答案在 Producer 一侧。Producer 指生成并提交 buffer 的组件:标准 App Window 通常由 HWUI 的 RenderThread 生产 buffer,HWUI 也就是 Android 的硬件加速 UI 渲染器;SurfaceView、Camera、Video、WebView、Flutter、游戏和 React Native 则可能把生产工作交给引擎线程、解码器或硬件模块。BufferQueue 则架在 Producer 与读取 buffer 的 Consumer 之间,负责传递数据。

生产之外,系统侧还有三个角色:SurfaceFlinger(SF)组织各个 layer,HWC(Hardware Composer,硬件合成器)选择硬件合成路径,FrameTimeline 记录预期帧与实际帧的时间。不同出图类型在线程、layer 数量、合成位置与 FrameTimeline 覆盖程度上的差别,就是我们分类的起点。

queueBuffer 表示 Producer 把 buffer 交回队列,latch(采纳) 表示 SurfaceFlinger 在本轮采纳该 buffer,present fence 则是显示管线完成本轮 present 后给出的同步信号。三个事件各自对应显示路径上的一个位置,单独看都只是片段;要还原整帧,我们还得把其他阶段的记录串起来。本节先把公共主线讲清楚,后续章节再解释各出图类型从哪里分开。分析一旦深入到 dma-buf(跨设备共享 buffer 的内核机制)、sync_file(把同步 fence 暴露为文件描述符的接口)、DRM/KMS(内核显示与模式设置子系统) 或调度器实现,我们就回到导语里的内核基线核对——Framework 结论不依靠某个单独的 kernel 函数推导。

用四个坐标识别出图类型

框架名称在这里帮不上忙:SurfaceView、TextureView、WebView、Flutter 和 Compose 都是 API 或框架名称,要确定 trace 里的图形路径,还得看页面实际用到的宿主和队列。同一个 Flutter 页面可能用 SurfaceView 或 TextureView 作宿主,嵌入的 Platform View(嵌入 Flutter 的原生平台视图) 还会改变 layer 拓扑;同一个播放器也可能在普通 BufferQueue、SurfaceView 独立 layer 与 tunneled/sideband(绕开普通 buffer 队列的专用媒体路径) 之间切换。

所以每次分析,我们先固定四个坐标:

坐标要确认的对象Trace 或源码证据
ProducerHWUI RenderThread、GL/Vulkan thread、Chromium、Flutter raster thread(光栅化线程)、Camera HAL(相机硬件抽象层)、MediaCodec、游戏引擎或其他模块线程 slice、GPU submission(命令提交)、decoder/camera 事件
输出目标App Window、独立 Surface、SurfaceTexture、swapchain(轮换使用的一组可显示 buffer)、离屏 HardwareBuffer 或 sideband streamBufferQueue、ANativeWindow(NDK 窗口接口)、SurfaceControl、layer dump
layer 拓扑内容进入宿主窗口,还是形成一个或多个独立 child layerlayer parent、relative layer、Z-order(前后叠放顺序)、BufferTX
合成与 present宿主 HWUI 采样、SurfaceFlinger CLIENT composition(用 GPU 合成)、HWC DEVICE composition(由显示硬件合成)或专用媒体路径RenderEngine(SurfaceFlinger 的 GPU 合成引擎)、composition type、HWC、present fence

线程名只说明某段代码在哪条线程执行。确认渲染类型时,我们还要用对象标识把线程、swapchain/BufferQueue、SurfaceControl 和目标 layer 对到同一条路径:看到 GLThread,要再确认它是否写入独立 layer;看到 SurfaceView 对象,要再确认本帧是否被 HWC 作为 overlay(独立硬件图层) 处理。

版本与架构对照表

Android 9 到 Android 17 的图形栈,用“Legacy”与“BLAST”二分来讲太粗了。对 trace 更有帮助的写法,是逐版本标出可以用哪些对象和观测方法:BLAST 指协调窗口 buffer 更新与 SurfaceControl.Transaction 提交的机制,Legacy 是对更早 BufferQueue/layer 路径的概括称呼。

平台公共显示主线Trace 判读要点
Android 9 / API 28App Window 常见传统 BufferQueue producer/consumer 路径不应期待 BLAST 或 FrameTimeline;以 Choreographer、HWUI、queueBuffer、SF/HWC slice 为主
Android 10 / API 29SurfaceControl.Transaction 能力扩展,该 tag 的 libgui 尚无 BLASTBufferQueue,App Window 按当时 BufferQueue 路径分析不要只凭系统版本断定采用 BLAST,要以进程内对象和 trace 为证
Android 11 / API 30BLAST 进入平台主线,窗口 buffer 与 layer transaction 的组织方式开始改变旧资料中的 BufferStateLayer 只适合对应旧源码 tag,不能拿来解释 Android 17 的 FrontEnd(layer 状态处理阶段)
Android 12 / API 31BLAST 与 FrameTimeline 构成现代 trace 基线标准 App Window 可用 SurfaceFrame/DisplayFrame token(跨阶段关联一帧的标识)对齐;独立 Surface 仍需 layer、BufferQueue 与 fence 证据
Android 13 / API 33AutoSingleLayer 下的 latch-unsignaled 策略成为重要边界acquire fence 尚未 signal(发出完成信号)时,transaction 也不再必定无法 latch;硬件读取仍要遵守同步约束
Android 14 / API 34公共 Choreographer→HWUI→BLAST→SF→HWC 骨架延续;SurfaceView 增加 alpha 与 lifecycle 能力标准窗口仍按公共主线读,独立 Surface 的透明度与生命周期要单独核对
Android 15 / API 35Window/SurfaceView 源码与 API 面开始提供 desired HDR headroom(SDR 白场以上的 HDR 亮度余量),r1 中仍带 feature flag 边界headroom 只是请求;还要核对目标设备的 feature flag(功能开关)、bit depth(色深)、面板与显示策略
Android 16 / API 36SurfaceView 源码增加整数 compositionOrder,r1 API 签名带 @FlaggedApi;公共主线延续多 Surface 页面要记录 parent、relative layer、flag 状态与 Z-order
Android 17 / API 37锚定 FrontEnd RequestedLayerState / LayerSnapshot、预测 present time、现行 BLAST/HWC 路径;SurfaceView blur region 与 compositionOrder 仍带 flag 边界源码按 android-17.0.0_r1 的对象名解释;旧 DispSync、固定 phase offset(相位偏移)和逐个旧 Layer latch 模型不能直接套用

这张表列出的是各版本的观测方法差异。图形栈并没有每版重做:BLAST 改变的只是 buffer 与窗口状态进入 SurfaceFlinger 的组织方式,Producer/Consumer 分工和 acquire/release fence 一直都在。各历史边界要分别核对 Android 10 libgui 目录、Android 11 BLAST、Android 13 默认 latch 策略 与 当前 SurfaceView;默认值仍可被设备配置覆盖。

典型模式对比

把四个坐标套到常见页面上,就得到下面的主干模式;每一行还标注了这类模式容易误判的点:

宿主采样与独立 Layer 的区别

图:标准 UI、TextureView 与 SurfaceView 的合成位置不同。独立 Layer 不保证 overlay;纹理采样按采样路径计成本,把它笼统写成固定 CPU 拷贝会走偏。

模式与章节Producer 与输出对象合成位置容易误判的点
标准 Android View
本篇
MainThread 准备状态,HWUI RenderThread/GPU 产出
→ 宿主 App Window
SF 选择 DEVICE 或 CLIENTMainThread draw() 结束不等于 buffer 已提交
Software / 离屏
13.2、13.6
lockCanvas() 线程、CPU raster(CPU 光栅化)或离屏 GPU
→ 可见 Surface 或离屏 buffer
可见目标进入 SF/HWC;离屏目标由下游消费“软件绘制”不等于没有 BufferQueue;离屏完成 fence 也不是 present fence
混合渲染
13.2
宿主 HWUI + 一个或多个独立 Producer
→ App Window 与 child layer 并存
SF/HWC 合成多个 layer不能用宿主窗口一条 FrameTimeline 代表所有独立 Surface
多窗口
2.14
每个 Window 各有 ViewRoot/Surface;线程可能同进程共享,也可能跨进程
→ 多个 Window layer
每个 display 分别组织输出“多窗口必定同一主线程串行”只适用于部分同进程场景
SurfaceView
13.3
解码器、Camera、GL/Vulkan 或其他 Producer
→ 独立 child Surface/layer
常由 SF/HWC 与宿主拼层独立 layer 有利于 overlay,但不保证低延迟或 DEVICE composition
TextureView
13.3
外部 Producer 写入 SurfaceTexture
→ 内容作为纹理进入宿主 View 树
宿主 HWUI 再采样到 App Window成本是额外采样/宿主合成,不宜笼统写成固定 CPU 拷贝
OpenGL ES
13.4
GL thread / engine thread
→ EGL window surface(EGL 的可显示窗口目标)对应 BufferQueue
SF/HWCeglSwapBuffers() 返回不代表 GPU 写完或已经 present
Vulkan
13.5
engine/render thread
→ VkSwapchainKHR 对接 ANativeWindow
SF/HWC显式 API 可以降低部分 driver 开销,但 CPU 成本取决于引擎、同步和驱动
WebView
13.9
Chromium renderer/compositor、Viz(Chromium 的显示合成服务)与宿主进程协作
→ Chromium surface 与宿主窗口组合,具体拓扑依实现而定
Chromium 合成后进入 Android 显示链只看 App MainThread 会漏掉 renderer/GPU 进程
Flutter
13.7
UI/raster/platform thread 与 Impeller/Skia 渲染后端
→ 宿主可用 SurfaceView 或 TextureView;Platform View 再增加分支
宿主 layer 与 Platform View 共同进入 SF/HWC框架名不能确定宿主 render mode(渲染承载方式),线程组织还随 Flutter/Engine 版本变化
Camera
13.10
Camera HAL、ISP(图像信号处理器)与应用/系统消费者
→ 预览 Surface、ImageReader、编码器等多消费者
预览常通过独立 layer 参与合成request/result 完成不等于预览已 present
Video / HWC
13.11
MediaCodec、解码器、播放器
→ SurfaceView buffer queue 或 tunneled/sideband 路径
HWC overlay、专用媒体路径或 CLIENT fallback解码完成、releaseOutputBuffer 与上屏时间不是同一边界
游戏引擎
13.12
game/render thread、GL/Vulkan queue
→ ANativeWindow swapchain,可能叠加独立 UI/video layer
SF/HWC平均 FPS 会掩盖 frame pacing(帧输出节奏)、queue depth(排队帧数)与 present 抖动
Compose
13.8
Compose runtime 与 UI thread 生成状态,HWUI RenderThread/GPU 产出
→ 默认仍是宿主 App Window
SF/HWCrecomposition(重组)、layout、draw 与 GPU 提交属于不同阶段
React Native
这里只给分型基线
JS、Fabric(新架构 UI 渲染系统)、HWUI;第三方原生组件可另建 Surface
→ 标准 View 树或 SurfaceView/TextureView 分支
取决于宿主与原生组件拓扑JS thread 只是 Producer 路径的一段,不能代表 present

13.6 SurfaceControl 与 HardwareBufferRenderer 与 13.4 OpenGL ES、EGL 与 ANGLE 分别解释 layer 控制和 GLES→Vulkan 翻译。2.14 多窗口、PiP 与桌面模式渲染管线、2.2 帧率、刷新率与显示模式选择、13.13 Android 17 / Android XR 空间 UI 与环境资产渲染性能 继续分析 window/display 分支。本节后半给出所有路径共用的证据收集步骤。

快速识别当前管线

拿到一段 trace,我们可以先做四项检查:

  1. 找到目标 Window、Surface 和 layer,确认是一层还是多层。
  2. 找到 Producer:MainThread/RenderThread、GL/Vulkan thread、解码器、Camera、Chromium、Flutter raster thread 或其他引擎线程。
  3. 确认提交点:queueBuffer、BLAST buffer transaction、SurfaceControl transaction(图层状态更新) 或硬件模块输出。
  4. 确认合成与显示:latch、composition type(合成类型)、RenderEngine、HWC、FrameTimeline 和 present feedback(显示结果反馈)。

这四项检查做完,当前管线基本就能定下来。线程名在其中只提供线索,最终仍要以线程、队列与目标 layer 的对象对应为准。

阅读路径

App 滑动卡顿从本篇开始;有视频、地图或相机预览时,再读 13.2 Android 软件、离屏与混合渲染路径 和 13.3 SurfaceView 与 TextureView 渲染管线。

音视频开发者可以按 13.11 视频 Overlay、Media3 与专业编解码管线 → 2.2 帧率、刷新率与显示模式选择 阅读。播放器卡顿只查刷新率往往不够,还要对齐解码输出、buffer timestamp(内容时间戳)、acquire fence、latch 和 present。

游戏与自研引擎可以按 13.4 OpenGL ES、EGL 与 ANGLE / 13.5 Vulkan 原生管线与 HWUI 多队列 → 13.12 Android 17 游戏引擎渲染链路 → 本节的分析方法阅读。要同时观察 game/render thread、GPU queue、swapchain 深度、FrameTimeline、Game Mode 与温控。

Framework 工程师可先读前面的公共主线,再看 13.6 SurfaceControl 与 HardwareBufferRenderer、2.2 帧率、刷新率与显示模式选择 和 13.5 Vulkan 原生管线与 HWUI 多队列。遇到 vendor(设备或芯片厂商) 显示问题时,还要补充 Composer HAL、display driver(显示驱动) 和面板证据。

公共主线:十二个检查点

要把不同 Producer 的路径放到同一组检查点上比较,先得有一个公共骨架。我们先把标准 App Window 压缩成十二个检查点:

vsync-app → doFrame → syncAndDrawFrame → dequeueBuffer → GPU submit → queueBuffer → BLAST transaction → SF snapshot/latch → HWC strategy → 可选 CLIENT composition → present → present feedback

这十二个点只是跨路径的坐标:实际动作可能重叠执行,特殊 Producer 也可能只走其中几段,未必凑齐完整的 HWUI slice。标准 View 的逐调用链解释放在后文;这里只保留比较不同 Producer 所需的公共检查点。

检查点先回答的问题不能据此断言
vsync-app应用何时被计划唤醒目标进程已经运行
doFrame哪类 callback(帧回调)或前序消息占用预算GPU 一定正常或异常
syncAndDrawFrameUI 状态何时交给 RenderThreadbuffer 已提交或显示
dequeueBufferProducer 是否拿到候选 slot(缓冲区槽位)及其 release fence等待一定由 buffer 数不足造成
GPU submit命令何时进入 GPU queuesubmit 返回即 GPU 完成
queueBufferslot、元数据和 production fence(Producer 完成写入的同步信号)何时交回SF 已采纳像素
BLAST / BufferTXbuffer update 是否到达 SurfaceFlinger 进程acquire fence 已满足
snapshot / latch本轮采纳哪个 layer 状态和 bufferAndroid 13+ 的 buffer 已可读
HWC strategy本帧采用哪种 composition type结果由 API 名称固定决定
CLIENT compositionRenderEngine 是否生成 client target(GPU 合成后的显示目标)App shader(着色器)是回退根因
present / releaseHWC 何时接收输出、何时归还 layer bufferpanel(物理显示面板)已完成光学响应
present feedbackdisplay 到达 Android 可观测显示边界单个 layer 的 release 时间

统一分析方法

分清类型只是第一步;要定位一帧的问题,我们还得把观察、关联和结论分开,落到可核对的对象上。

先区分事实、关联与结论

层次示例是否足以确认根因
观察事实dequeueBuffer 持续 8 ms;目标 layer 为 CLIENT否
时间关联等待与上一帧 release fence 较晚发出 signal 的时段重合还要核对对象
因果结论同一队列无可复用 slot,因为 HWC 延迟归还上一轮 buffer是,但必须有队列、fence 和 layer 证据

“同一时间发生”离“前者导致后者”还差一步:对象身份对上之前,长 slice 只能列为候选。

Step 1:锁定对象与路径

先建立一张对象记录表:

字段用途
package、UID、PID、关键 TID用应用包名、UID(应用身份标识)、PID(进程 ID)和 TID(线程 ID)锁定 Producer 与线程
displayId、mode、刷新率区分内外屏、虚拟显示和显示模式切换
Window、Surface、layer id对齐 WindowManagerService(WMS)、SurfaceFlinger(SF)、Perfetto 与 Winscope(窗口和图层检查工具)
parent、Z-order确认宿主内容和独立 child layer
BufferQueue / BLAST对齐 dequeue、queue 与 BufferTX
surface/display token对齐 App SurfaceFrame 与 SF DisplayFrame
输入动作与时间窗锁定用户看到的目标帧

控件或框架名只能给出候选;要定案,我们还得用 Producer、输出 Surface、layer 拓扑和合成结果来验证。

Step 2:记录 Producer、Consumer 与 fence 的关系

路径Producer第一接收点SF 主要对象
标准 View / ComposeHWUI RenderThread / GPU应用进程内 BLAST宿主窗口 transaction
SurfaceViewcodec、Camera、GL/Vulkan 等独立 Surface Consumer(接收 buffer 的组件)独立 child layer
TextureViewcodec、Camera、GLApp 内 SurfaceTextureHWUI 采样后的宿主窗口
WebViewChromium 与宿主 HWUIfunctor(把 Chromium 绘制接入 HWUI 的桥接对象)或 child Surface以现场拓扑为准
HardwareBufferRendererHWUI RenderThread / GPU调用方 HardwareBuffer调用方提交后才送显

三类 fence 各回答一个问题:acquire fence 说明“buffer 何时可读”,release fence 说明“buffer 何时可复用”,present fence 说明“本轮 Display 何时到达显示边界”。顺着这组关系,我们把两类常见现象分开查:dequeueBuffer 长时间等待时,检查同一队列的可用 slot、Consumer 持有量和上一轮 release;BufferTX 积压时,检查 transaction readiness(事务是否满足处理条件)、acquire 的状态,以及 buffer 最终被 latch 还是 drop(丢弃)。增加队列深度可能增加 buffer 内存占用,并在实际排队变深时增加延迟;反过来,仅提高上限未必带来这两样变化,动手前先确认排队确实变深了。

acquire 管可读,release 管复用,present 管显示反馈

图:三类同步与光学边界合图。Producer 的写完成信号到了 Consumer 侧就是 acquire,真正读取前要遵守它;受限 latch-unsignaled 只是先采纳。release 约束旧 buffer 的下次安全写入,slot 归 FREE 或收到 callback/channel 通知时 fence 未必 signal,SF 还可按 CLIENT/DEVICE 合成路径合并/替换释放信息。present 是目标 display 的时间基准,回答本轮显示何时到达,与 release 各答一问;panel 扫描与像素光学完成要另靠 driver 或外部测量补证,触摸到发光的完整延迟也一样。

API 37 的 Surface.isProducerThrottlingEnabled() 查询 EGL/Vulkan Producer 的 CPU throttling 配置;要证明当前调用确实发生了背压,还得回到等待证据本身。setProducerThrottlingEnabled(false) 关掉的只是 queue/present 侧的这一类节流,帧率投票照旧,dequeue 和 swapchain acquire 的正常同步等待也照旧。Android 17 r1 中这组 API 带 bq_producer_backpressure_control flag;异步模式始终启用此节流,设置无效。

Step 3:对齐同一帧

采集 trace 时,要覆盖目标 App、SF、system_server 和上游 Producer 的线程调度,以及 gfx/view/wm 图形类别、FrameTimeline、BufferQueue/SF、GPU 与设备可提供的 HWC/Display 轨迹;layer 层级和 transaction 变化可配合 Winscope 检查。

轨道重点
App / RenderThread / 引擎doFrame、draw、dequeue/queue、GPU submit
codec / Camera / Chromium / Flutter非标准 Producer 的节奏
BufferQueue / BLASTqueue 周转、transaction、BufferTX
SurfaceFlingertransaction、snapshot/latch、RenderEngine
FrameTimelineexpected/actual(预期/实际时间)、surface/display token、jank type(卡顿类型)
GPU / HWC / Display异步执行、composition type 与 present

我们按“锁定 token(跨阶段关联帧记录的标识)/layer → 帧开始 → CPU/GPU 生产 → transaction 到达 → acquire/latch → SF/HWC/present → 上一帧 release”的顺序复原路径。相邻帧会重叠,只看时间是否接近,容易把对象配错。

下面的 Perfetto SQL 用于找出目标进程中带有 jank 标记的实际帧,并用 surface token 关联对应的预期帧:

INCLUDE PERFETTO MODULE android.frames.timeline;

WITH expected_unique AS (
  SELECT upid, surface_frame_token,
         MIN(id) AS expected_id, MIN(ts) AS expected_ts,
         MIN(dur) AS expected_dur
  FROM expected_frame_timeline_slice
  WHERE surface_frame_token IS NOT NULL
  GROUP BY upid, surface_frame_token
  HAVING COUNT(*) = 1 AND MIN(dur) >= 0
)
SELECT p.name AS process_name, a.id AS actual_id, a.layer_name,
       a.surface_frame_token, a.display_frame_token,
       e.expected_id, e.expected_ts, e.expected_dur,
       a.ts AS actual_ts, a.dur AS actual_dur,
       a.jank_type, a.present_type, a.on_time_finish
FROM actual_frame_timeline_slice AS a
JOIN process AS p ON a.upid = p.upid
LEFT JOIN expected_unique AS e
  ON a.upid = e.upid
 AND a.surface_frame_token = e.surface_frame_token
WHERE p.name = 'com.example.app'
  AND a.surface_frame_token IS NOT NULL
  AND a.layer_name IS NOT NULL
  AND a.dur >= 0
  AND a.jank_type IS NOT NULL AND a.jank_type != 'None'
ORDER BY a.ts DESC
LIMIT 20;

这里按 Actual id 保留完整记录。固定 r1 的 Perfetto 解析器 按 upid + surface_frame_token 只保留一份 Expected,因为共享该 token 的工作使用同一预测;Expected 的 Layer 名可能仅属于最先出现的 Layer,与每条 Actual 的 Layer 未必一一对应。查询仅在该进程/token 有恰好一条完整 Expected 时关联;expected_id 为 NULL 表示缺失、未结束或异常重复。带 jank 分类的记录也可能是 Buffer Stuffing 等类型,Late Present 只是其中一种。独立 Surface 缺少完整 App FrameTimeline 时,回到 Producer、BufferQueue、layer、fence 和 present。

Step 4:按证据模式归因

模式主要证据不能直接推断
主线程 / RenderThread CPU 重expected/actual 与明确 CPU 子段一致整段 wall time(实际经过时间)都是 GPU
App GPU 晚完成submit 不晚,GPU stage(执行阶段)或 acquire readiness(可读取条件)较晚queueBuffer 返回等于完成
Buffer 队列阻塞dequeue 与同队列 release/Consumer 对上只需增加 buffer
Producer 抖动codec/Camera/引擎 cadence(输出间隔)不稳SF 复用旧 buffer 就是 SF 卡顿
SF CPU/GPU 超预算SF actual、CPU/GPU 与 jank type 相互印证SF actual 全属主线程 CPU
HWC 路径变化composition type、client target、GPU/功耗同变SurfaceView 必然获得 DEVICE
显示后段延迟App、latch、composition 按时,present 晚framework trace 覆盖面板响应
VRR 口径错误内容帧率、render rate、refresh rate 不一致固定 16.6 ms 适合所有模式

帧率、端到端延迟和功耗要分别记录。队列更深可能让吞吐稳定但交互更慢;CLIENT composition 可能保持帧率,却增加 GPU、带宽和功耗。

选型与复核

下面的决策树用于按输出对象和合成需求选择渲染入口:

普通 Android UI?
├── 是:View / Compose → HWUI → 宿主窗口
└── 否:上游能否直接向 Surface 生产 buffer?
    ├── 是:需要独立 layer 或避免宿主二次采样?
    │   ├── 是:SurfaceView / Surface / ANativeWindow
    │   └── 否:需要宿主任意纹理效果?是 → TextureView
    └── 否:RenderNode 输出到自管 HardwareBuffer?是 → HardwareBufferRenderer

SurfaceView 提供的是独立 layer 条件,DEVICE、低功耗或低延迟还要看现场合成结果;TextureView 通常增加一次宿主采样。WebView、Flutter 和游戏必须核查真实宿主、Producer、BufferQueue、layer 与 HWC。

复盘完成前确认:

  • package/PID、display、Window、layer、BufferQueue 和时间窗已经锁定;
  • Producer、第一 Consumer、SF layer 与送显路径已经记录;
  • acquire、release、present fence 没有混用;
  • token、frame number(帧序号) 或明确时序指向同一帧;
  • CPU wall time(实际经过时间)、GPU execution(执行时间) 与 fence wait(同步等待) 已分开;
  • 上一帧 release 是否让当前帧等待已经检查;
  • FrameTimeline、GPU/HWC 缺失时的证据边界已说明;
  • DEVICE/CLIENT 只作为设备现场结论;
  • 帧率、端到端延迟、功耗分别验收;
  • 修复前后设备、场景、输入和采集配置一致。

公共主线的源码入口

下面的源码入口覆盖公共主线,全部固定到 android-17.0.0_r1:

内核侧按 android17-6.18-2026-06_r6 核对 dma-buf(共享 buffer 的内核机制)、dma-fence/sync_file、DRM atomic 与 vblank。Vendor GPU、Composer、display driver 和面板时序要用目标设备源码与 trace 补证。

用角色和边界读 trace

看到任何 App 出图问题,我们先确定 Producer、Surface/layer、合成位置和 present 边界,再进入线程耗时。标准窗口从 vsync-app → doFrame → RenderThread → BLAST → SurfaceFlinger → HWC 建立基线;特殊框架与硬件 Producer 只是在某些节点替换角色或增加分支。

四个信号各管一段:queueBuffer 说明内容已提交,BufferTX 说明 SurfaceFlinger 侧仍有待处理的 buffer 更新,latch 说明本轮已经采纳,present fence 提供显示管线完成呈现的时间反馈。要区分帧开始晚、Producer 完成晚、Consumer 等待、退回 GPU 合成和显示后段延迟,我们得按顺序把这四类记录对齐。

View、HWUI 与 BLAST 标准路径

分类确定内容属于普通 App Window 之后,我们沿 ViewRootImpl、RenderThread、BLAST 和 SurfaceFlinger 还原一帧。

标准 HWUI App Window 的页面主体由普通 View 或 Compose host(Compose 宿主容器) 组织,RenderThread 生成宿主窗口 buffer。承载主体内容的独立 Surface、浏览器 compositor(合成器)、游戏引擎 swapchain、Camera HAL 或视频解码器 Producer 走的是其他管线,要按各自的路径分析。

Compose 独立于 Android platform 发布,我们在这里只说明它在标准 App Window 中的位置;某个 Jetpack 版本的行为要从它自己的版本核对,从 Android 平台源码推不出来。

如何确认这是标准 HWUI 页面

要确认页面属于标准类型,我们依据的是主体内容由谁生产、经哪里提交。普通 View、纯 Compose 或 ComposeView 页面通常属于标准类型;页面里出现的框架控件名只能当线索。

条件标准类型的证据需要切换章节的信号
主体 ProducerViewRootImpl、HWUI RenderThread 与 App GPU queueChromium、Flutter raster、Camera HAL、MediaCodec 或游戏引擎主导主体内容
输出目标当前 App Window 的 Surface / BLAST BufferQueue独立 Surface、SurfaceTexture 输入、sideband stream 或另一条 swapchain
layer 拓扑主体像素落在 App Window layer出现承载主体内容的 child layer 或多个独立 Producer layer
帧节奏Choreographer#doFrame 与窗口 SurfaceFrame 能解释主体更新引擎、解码器或硬件模块维护独立 cadence(输出间隔)

常规列表页、详情页和设置页是典型标准页面。它们可能包含复杂布局、耗时较高的 shader 或大量 Compose 重组;“标准”只描述路径,不评价负载大小。页面嵌入视频、地图、相机预览或大型 WebView 后,我们就要按实际占主体内容的 Producer 和 layer 重新分类。

一帧的完整旅程

标准页面可以按 12 个观察点还原。系统状态栏、导航栏、壁纸等 layer 仍会同时参加 display composition(显示合成);下面我们只跟踪目标 App Window。

⓪ 帧为什么被安排

输入、动画、invalidate()、requestLayout()、Insets 或窗口状态变化会让后续帧有工作。ViewRootImpl.scheduleTraversals() 在尚未安排 traversal 时,向主线程消息队列插入同步屏障,并通过 Choreographer.postVsyncCallback() 注册 CALLBACK_TRAVERSAL 阶段的 TraversalCallback;固定 r1 中该对象实现 Choreographer.VsyncCallback。Choreographer 再用 mFrameScheduled 合并尚未执行的帧申请。异步帧消息可以越过普通同步消息;至于 CPU 调度和正在执行的任务,同步屏障管不到它们。

invalidate() 只标记需要重绘的脏区域并安排后续 traversal,绘制发生在后面的 traversal 里。同一个 pending(尚未执行) frame 内的连续调用会被合并,并不会各自申请一份 VSync。

Traversal 申请、同步屏障与帧合并

图:多次重绘申请可合并到待执行帧,真正绘制稍后发生。屏障允许异步消息绕过同步消息;正在执行的任务和 CPU 调度归调度器管,屏障管不到。

① vsync-app 安排应用帧事件

Scheduler 根据预测 present time,以及描述工作时长和就绪余量的 app workDuration、readyDuration 参数计算应用 wakeup(唤醒时刻),再经 EventThread / DisplayEventReceiver 把 VSync 事件送到进程。

SurfaceFlinger 里的 vsync-app 事件是调度和分发线索;要确认目标应用真的收到了事件,还得核对 App 接收侧事件、Choreographer#doFrame 和调度记录,EventThread 的发送本身就有失败分支。这几条记录同时帮我们区分事件分发、目标线程可运行与真正上 CPU;线程本来就在运行时,可能不会出现新的 sched_wakeup。

VSync 计划、分发与 App 执行

图:从 SF 的帧计划继续核对 App 接收和线程执行。 发送可失败;线程已运行时不一定产生新的 sched_wakeup。

② MainThread 组织本帧

Choreographer#doFrame() 的 callback 顺序是:

INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT

这一段有三个关键点:

  • INPUT 的重要工作之一是按帧集中消费 batched motion event(批量触摸事件);普通输入事件也会通过 InputChannel 与 Looper 消息循环异步处理。
  • Traversal 按本帧状态决定是否执行 measure、layout 与 draw,并不会每帧都完整遍历整棵 View 树。
  • 硬件加速路径的 draw() 主要更新 RenderNode/DisplayList(可复用的绘制指令记录),描述“画什么”;像素生产发生在后面的 RenderThread/GPU 阶段。

③ syncAndDrawFrame() 交给 RenderThread

ViewRootImpl.performDraw() 经 ThreadedRenderer / HardwareRenderer 更新窗口绘制树的根节点 root RenderNode(窗口绘制树的根节点),然后调用 syncAndDrawFrame()。native 侧由 RenderProxy 把 DrawFrameTask 投递到 RenderThread。

UI 线程会在 DrawFrameTask::postAndWait() 等待 RenderThread 完成必要的同步。DrawFrameTask::run() 先执行 syncFrameState();该函数返回 info.prepareTextures,表示本轮纹理准备是否允许提前放行 UI 线程:

  • 返回 true 时,RenderThread 可以在 draw 前 unblockUiThread();
  • 返回 false 时,UI 线程要等 draw 或 waitOnFences() 之后再被放行。

这解释的只是 UI/RenderThread 的同步边界;GPU 是否完成、何时 present,都还要看后面的阶段。

下面的源码骨架用于展示 UI thread 的等待与放行位置。它省略了 callback、skip-frame 和错误分支,不是可编译代码。

RenderProxy::syncAndDrawFrame()
    DrawFrameTask::drawFrame()
        postAndWait()

DrawFrameTask::run()
    canUnblockUiThread = syncFrameState(info)
    if (canUnblockUiThread)
        unblockUiThread()
    if (canDrawThisFrame)
        CanvasContext::draw()
    else
        CanvasContext::waitOnFences()
    if (!canUnblockUiThread)
        unblockUiThread()

上面的骨架里,canUnblockUiThread 就是 syncFrameState() 在 CanvasContext::prepareTree() 之后返回的 info.prepareTextures。它为 true 时 UI thread 在 draw 前放行;纹理缓存空间不足等情况会返回 false,UI thread 保持等待,直到本轮 draw 或 fence wait 结束。

UI 更新绘制记录,RenderThread 同步结果决定何时放行 UI

图:固定 r1 UI 记录与 RT 放行分支合图。MainThread 按脏状态更新/复用窗口 RenderNode 和 DisplayList,经 RenderProxy 投递 DrawFrameTask 后在 postAndWait 等必要同步。syncFrameState 在 prepareTree 之后返回 info.prepareTextures:true 可在 draw 前 unblock UI,false 等 draw 或 waitOnFences 之后再放行,draw 与 wait 由本帧条件决定。图省略 callback/skip-frame/错误分支,只解释 UI/RT 同步;UI draw 或 unblock 各自只说明那一步完成,GPU 完成、buffer 可读和 present 都要后面的证据。

④~⑥ RenderThread 生产窗口 buffer

RenderThread 使用 App Window 的 Surface / BufferQueue Producer:

  1. dequeueBuffer() 取得候选 slot 与 release fence。没有可用 slot 或队列约束不满足时,可能等待、超时或返回 WOULD_BLOCK;已 queue 过 buffer 且达到 max-dequeued 上限时,固定 r1 返回 INVALID_OPERATION。返回的 release fence 由 Producer 的 EGL/Vulkan 等实现遵守,等待可能移到后端或首次写入前;分析时要把这部分等待和 BufferQueue 的选槽等待分开记。
  2. HWUI 的 Skia OpenGL/Vulkan pipeline 录制并提交 GPU 命令。
  3. queueBuffer() 提交 slot、frame number、timestamp(内容时间戳)、dataspace、crop/transform(裁剪与变换) 等元数据,以及表示 Producer 写入完成的 producer completion fence(Producer 写入完成的同步信号)。

CPU 命令提交完成、queueBuffer() 返回时,GPU 仍可能继续写入。Producer completion fence 在 Consumer 侧作为 acquire fence,约束 Consumer 何时能安全读取这块 buffer。

⑥' BLAST 把 buffer 变成 transaction

标准 App Window 的 BLASTBufferQueue 位于应用进程。onFrameAvailable() 接到可用通知后,在可获取 buffer 时经 acquireNextBufferLocked() 从 Consumer 取得 BufferItem,再调用 Transaction::setBuffer() 填入 buffer、acquire fence、frame number 和 release callback。普通路径合并 pending transaction 后自行 apply();同步窗口更新可先合入调用方提供的 transaction,由同步协调方统一提交,apply 的时机也就落在协调方手里。

SurfaceFlinger 进程收到含 buffer 的 transaction、计入 pending 之后,BufferTX - <layerName> 增加;buffer 被 latch 或 drop 后减少。App 侧 queueBuffer() 与 SurfaceFlinger 侧 BufferTX 是两个不同的时间点。

下面的源码骨架用于标出 queueBuffer() 之后的 BLAST transaction 边界。它展示的是普通非同步分支;callback、暂时取不到可 acquire 的 buffer、同步事务合并这几种情况都留在图外。

BufferQueueProducer::queueBuffer(slot, QueueBufferInput{fence, ...})
    slot.state = QUEUED
    frameAvailableListener.onFrameAvailable(BufferItem)

BLASTBufferQueue::onFrameAvailable(BufferItem)
    acquireNextBufferLocked()
        Transaction.setBuffer(
            surfaceControl, buffer, acquireFence, frameNumber, ...)
        Transaction.apply()

SurfaceFlinger::setTransactionState(...)
    TransactionHandler::queueTransaction(...)

这段路径传递的是 slot、buffer handle(缓冲区句柄)、元数据和同步对象,整帧像素不会经 Binder(Android 进程间通信机制) 复制。SF 侧收到 transaction 后还要检查 readiness、生成本轮 layer snapshot(状态快照),再执行 latch 和 composition;至于显示侧走到了哪一步,要由 present 一侧的记录来回答。

BLAST 提交所有者分两支,BufferTX 只计 SF 待处理更新

图:固定 r1 BLAST 提交与 SF pending 结果合图。App 内 onFrameAvailable 之后,能 acquire 时取得 BufferItem,仍受数量与队列约束,暂时取不到就延后。普通路径 setBuffer/merge 后由 BLAST apply;同步更新合入调用方 transaction,由协调方统一提交,不是每通知立即独立 apply。传的是 buffer 句柄/fence/framenumber/release 信息,整帧像素不经 Binder 复制。SF 收到目标 buffer 更新并计入 pending 才增加 BufferTX;latch 或 drop 都会下降,尚未处理时查 readiness 与顺序。这个计数只反映 SF 侧的待处理更新,App queue 时刻、全部 buffer 数、GPU 量都要另找指标;下降也只说明本轮处理结束,present 要另看显示侧记录。

⑦~⑧ SF FrontEnd 与 latch

transaction 到达后,接下来的工作都在 SurfaceFlinger 进程里:它在 vsync-sf 节奏下集中 flush transaction,FrontEnd 把请求合入 RequestedLayerState,再由 LayerLifecycleManager / LayerSnapshotBuilder 生成本轮 LayerSnapshot。

latch 表示本轮采纳了目标 layer 的新 buffer。一般路径会检查 acquire fence;符合 shouldLatchUnsignaled()、简单单 layer update、队列顺序等条件时,可以先 latch 尚未 signal 的 buffer,也就是先采纳 transaction,不等待 Producer 完成信号。RenderEngine 或 HWC 真正读取内容时仍要遵守 fence。

latch 未完成 buffer 的条件与限制

图:固定 r1 允许在受限条件下先采纳未 signal 的 buffer。 AutoSingleLayer 还检查 first transaction、非 early 配置与简单 buffer update;读取方仍遵守 fence。

⑨~⑪ HWC composition 与 present

SurfaceFlinger 为目标 display 筛选并组织参与输出的 layer,与 HWC 协商 composition type(CLIENT/DEVICE 等类型);被遮挡、过滤或属于其他 display 的 layer 不一定交给这一 display 的 HWC。HWComposer::getDeviceCompositionChanges() 只有满足 canSkipValidate 时才尝试 presentOrValidate():本次没有 CLIENT composition,且 Composer 支持预期 present 时间,或当前已到 earliest-present 边界。其结果分为:

  • PresentSucceeded 表示这次组合 HAL 调用已经走完 present 分支,并保存了 present/release fences;
  • Validated 表示 validate 已在本次调用中完成;随后还要读取 HWC 要求改变的 composition type(CLIENT GPU 合成或 DEVICE 硬件合成等类型) 或其他 request,并调用 acceptChanges();
  • 不满足 skip 条件时,单独调用 validate() 检查当前 layer 组合能否由硬件处理;
  • 有 CLIENT layer 时,RenderEngine 先生成 client target,再通过 setClientTarget() 把 buffer 和对应的 acquire fence 交给 HWC;
  • presentAndGetReleaseFences() 在已由前一步完成 present 的 fast path(已由前一步完成 present 的快速路径) 中只需 flush commands;其他路径调用 present(),并收集每个 layer 的 release fence。

PresentSucceeded 是 Composer/HWC 调用状态,panel 此时未必已完成扫描。

validate 与 presentOrValidate 的分支

图:没有 CLIENT 且时间条件允许时,可尝试组合调用。PresentSucceeded 表示本次已走完 HAL present 调用,本轮不会再进入需要新 CLIENT 绘制的分支;面板是否扫描完成,还要显示侧的后续证据。

⑫ present 与 release feedback

HWC 返回每个 display 的 present fence 和每个 layer 的 release fence。SurfaceFlinger 还会根据 CLIENT/DEVICE composition 合并或替换 release 信息,再通过 callback/release channel(释放通知通道) 回传 Producer。

Present fence 是 Android 显示栈里 display-present 的时间基准;panel 扫描、像素响应,以及从触摸输入到屏幕发光的完整延迟,还需要 driver trace(驱动追踪) 或外部测量补证。Release fence 则约束旧 buffer 何时能安全复用。

BLAST Buffer 生命周期

Slot 状态机

下面的状态图展示普通非 shared-buffer 模式的主要循环。它没有穷尽 cancelBuffer、丢弃 queued buffer、attach/detach、断连和 shared-buffer 等分支:

stateDiagram-v2
    [*] --> FREE
    FREE --> DEQUEUED: Producer dequeueBuffer
    DEQUEUED --> QUEUED: Producer queueBuffer
    QUEUED --> ACQUIRED: BLAST Consumer acquireBuffer
    ACQUIRED --> FREE: Consumer releaseBuffer

releaseBuffer() 把 slot 归还到可 dequeue 的状态,但 slot 仍可能携带上一轮 release fence。Producer 再次写入前必须遵守该 fence;FREE 只表示 slot 已成为候选,硬件是否允许立即覆盖,要看它携带的 fence。

状态主要持有方含义
FREEBufferQueue可作为 dequeue 候选,可能带待遵守的 release fence
DEQUEUEDProducer/RenderThread已取出,准备写入或正在写入
QUEUEDBufferQueueProducer 已提交,等待 Consumer acquire;acquire fence 可能尚未 signal
ACQUIREDBLAST/BufferItemConsumerConsumer 已取得,后续通过 transaction 进入 SF pending/latch/release 路径

队列深度由运行时约束共同决定

“BLAST 始终维护三个 buffer”并不符合源码。BufferQueue 有一组 slot,我们逐项看运行时约束:

  • maxDequeuedBufferCount:Producer 同时持有的 DEQUEUED 上限;
  • maxAcquiredBufferCount:BLAST Consumer 同时持有的 ACQUIRED 上限;
  • synchronous/async(同步/异步队列模式) 队列模式、Producer 是否允许阻塞,以及 queued backlog(已经排队但尚未消费的数量);
  • 当前各 slot 的状态;
  • release fence、BLAST pending release(等待处理的释放信息);
  • 当前/最大刷新率对应的 acquired-count 策略。

Triple buffering(三缓冲) 是常见运行状态,但每台设备、每个窗口是否固定用三个 slot,要看上面的约束。它可以减少 Producer 与 Consumer 相互等待,也可能让 in-flight frame(仍在处理链中的帧)增多。是否增加一整帧延迟,取决于 pacing(帧节奏)、queue depth(排队深度)、latch 和 present;要下这个判断,光看“有第三个 slot”还不够。

dequeueBuffer() 等的未必是“上一帧那块 buffer”。普通模式可选择满足队列和分配约束的 FREE slot,并把该 slot 的 release fence 返回;哪怕该 fence 尚未 signal,这个 FREE slot 也仍在候选之列。没有合适候选时再按阻塞/超时配置处理,达到 max-dequeued 上限则可能直接报错。Producer 安全写入前的 fence 等待要另行定位。

dequeue 的等待、上限错误与返回 fence 分开定位

图:普通非 shared-buffer 模式的结果分类与复用同步合图,列的是常见结果,完整分支顺序要看源码。无候选或队列约束时按 mode/timeout 等待、超时或返回 WOULD_BLOCK;固定 r1 下已 queue 过且达到 max-dequeued 持有上限,可直接返回 INVALID_OPERATION。成功时可能返回一块 FREE 候选,它附带的 release fence 尚未 signal,候选资格照样成立。slot 归还状态与安全覆写是两回事:EGL/Vulkan 等后端在写前遵守 release 依赖,这段等待未必发生在选槽内部;随后为本次生产提交完成 fence,Consumer 侧称之为 acquire。

BLAST 的 release 路径

正常的 buffer 释放信息由 SF release callback/release channel 回到 BLAST,再由 BLASTBufferItemConsumer::releaseBuffer() 归还 slot。Transaction completed callback 还会对 stale submitted buffer(已被后续提交替代的旧 buffer)生成 fake release(补发资源归还通知,仍携带应遵守的 fence,不是伪造内核 fence 已完成);诊断时要区分释放来源,transaction-completed 回调只是其中一路。

正常释放与 stale buffer 补释放

图:按来源区分正常释放、完成回调补通知和真正的 fence 完成。 fake release 是补回调,不是伪造内核已完成信号。

Release 信息可携带与当前刷新率相关的 current max acquired count(当前允许的 ACQUIRED 数量上限);BLAST 对 EGL Producer 可以暂存一部分已 release buffer,在当前刷新率低于设备最大刷新率时控制延迟和分配行为。诊断 queue depth 时要查看当时的 acquired/dequeued 上限和 pending release,只数 trace 上的三个 buffer 名称是不够的。

刷新率与 EGL pending release 策略

图:BLAST 可结合当前 acquired 上限暂存 EGL 的部分释放信息。 字段表示数量上限,不是实时已取得数;该持有策略有 EGL 条件。

Buffer stuffing recovery

Choreographer/HWUI 路径包含 buffer stuffing recovery,用于缓解 Producer 提交过快造成的队列堆积。BBQBufferQueueProducer::waitForBufferRelease() 记录等待,ViewRootImpl / ThreadedRenderer 把信号传到 Choreographer.onWaitForBufferRelease();等待超过相应阈值时,后续 doFrame() 可以主动延后一帧,使 queued buffer 数下降。

Android 17 r1(android-17.0.0_r1)的阈值是最近 frame interval(帧间隔) 的一半。进入 recovery 后,DELAY_FRAME 会请求下一次 VSync 并跳过当前 doFrame();后续 recovery 还可能对 animation frame time 应用一个 frame interval 的负 offset,即从动画帧时间中减去一个 frame interval。

buffer_stuffing_multi_recovery 控制同一段动画是否允许多次恢复;buffer_stuffing_recovery_threshold 启用时,在允许多次 recovery 的分支中,已累计 delay 达到 100 ms 后不再追加此次主动 delay;它按帧间隔累计,最后一次追加可能跨过 100 ms,100 ms 只是一条近似封顶线。两项都是可变的 aconfig flag(平台配置开关),目标设备的取值必须从配置或 trace 确认。

Stuffing 恢复先看触发与资格,再判断是否追加主动 delay

图:固定 r1 恢复条件合图,列的是触发与追加条件,完整的 Choreographer 状态机要看源码。buffer 释放等待超过最近 frame interval 一半即触发 isStuffed;multi flag 控制同段动画是否允许重复恢复。追加检查只出现在允许多次恢复且 threshold flag 开启的分支:追加前先看累计 delay 是否已达 100 ms,未达则按 frame interval 追加,最后一次可能跨过 100 ms,这条线只是近似封顶。本次允许恢复时 DELAYFRAME 请求 next VSync 并跳过当前 doFrame,后续恢复还可能 OFFSET,从动画帧时间减去一个帧间隔;空闲足够长再复位,OFFSET 与复位都由条件触发,谈不上每次发生或立即发生。它降低的是积压机会,GPU 与显示的处理能力维持原样;设备 flag、dequeue wait、Buffer Stuffing/recovery 与 backlog 回落要联合验证。

这项机制的收益是降低队列过深带来的额外输入到显示延迟;GPU 和显示硬件的处理能力维持原样。Perfetto 中看到 Buffer stuffing recovery、buffer stuffed 或 Negative offset 时,要同时检查 dequeue wait、FrameTimeline Buffer Stuffing 和 queue backlog 是否回落。

渲染时序图(BLAST Sequence)

下面的图把 App、BLAST、SurfaceFlinger 与 HWC 放在同一条时序线上:

sequenceDiagram
    participant MT as App MainThread
    participant RT as HWUI RenderThread
    participant BBQ as BLASTBufferQueue
    participant GPU as GPU queue
    participant SF as SurfaceFlinger
    participant HWC as HWC / Composer
    participant DD as Display path

    MT->>MT: ⓪ invalidate/requestLayout/animation
    SF-->>MT: ① EventThread / DisplayEventReceiver event
    MT->>MT: ② INPUT → ANIMATION → INSETS → TRAVERSAL → COMMIT
    MT->>RT: ③ syncAndDrawFrame
    RT->>BBQ: ④ dequeueBuffer
    RT->>GPU: ⑤ Skia record/submit
    RT->>BBQ: ⑥ queueBuffer + producer completion fence
    BBQ->>SF: ⑥' Transaction::setBuffer/apply
    DD-->>SF: timing sample, when needed
    SF->>SF: ⑦ scheduler plans vsync-sf wakeup
    SF->>SF: ⑧ transaction flush / snapshot / latch
    SF->>HWC: ⑨ validate or presentOrValidate
    alt presentOrValidate already presented
        HWC-->>SF: cached present/release fences
    else validate completed, present still needed
        opt CLIENT composition required
            SF->>GPU: ⑩ RenderEngine draws client target
            SF->>HWC: setClientTarget + acquire fence
        end
        SF->>HWC: ⑪ present
        HWC-->>SF: present fence + release fences
    end
    DD-->>SF: ⑫ present fence signals later
    SF-->>BBQ: release callback/channel

图里 BLAST 消费 BufferItem 的位置在 App 进程,SF 侧消费的是 SurfaceControl transaction。queueBuffer() 返回只说明提交动作完成:这一路仍可能因 Producer throttling、BLAST 同步或回调路径等待,后续 dequeueBuffer() 也可能因可用 slot 或队列约束等待,我们要用实际 slice 和 fence 把这些等待区分开。

CPU 提交可先返回,Consumer 真正读取仍等写入完成

图:三轨只给一种可能的异步依赖,条长非实测。RT 提交 GPU 和 queue 时带上本次写完成 fence,CPU 返回只说明命令已交出,GPU 可能仍在写,queue 本身仍可因节流/BLAST/回调等待。SF 可准备,或在满足 latch-unsignaled 条件时先采纳;图里的先采纳条只表示 transaction 被采纳,RenderEngine/HWC 真正读取仍须 acquire 满足。present/release 与光学边界另看,按同一 buffer/token/fence 核对;这组示意只给一种可能的顺序,别固化成每帧固定先后或耗时。

Trace 视角

“正常耗时 < 1 ms / 2 ms / 8 ms”这类固定阈值不适合跨刷新率、设备和场景复用。判断某一阶段慢不慢,我们应把它与本帧的 expected timeline(预期时间线)、线程状态和当前 display budget(该帧可用的显示时间预算) 对齐。

阶段主要信号需要回答的问题
App 收帧Choreographer#doFrame、app FrameTimelineApp 是按时醒来,还是 runnable 等待或消息队列处理已经延误
MainThreadINPUT、ANIMATION、INSETS_ANIMATION、TRAVERSAL、COMMIT哪个 callback 或业务工作先增加
UI→RTsyncAndDrawFrame、RenderThread DrawFrames <vsyncId>UI 在等状态同步,还是 RT 已进入 draw
Buffer 生产dequeueBuffer、GPU slice/fence、queueBufferslot 等待、CPU submission、GPU completion、queue 中哪一段较晚
App→SFBufferTX - <layerName>、transaction、latchbuffer update 是否到达 SurfaceFlinger,是否被本轮采纳
SF/HWCSF actual、composition type、DisplayHAL、present feedback系统 CPU、CLIENT GPU、HWC/display 哪段晚

MainThread、RenderThread 与 GPU 分开看

固定 r1 的 DrawFrameTask::run() 用 DrawFrames <vsyncId> 标记 RenderThread 帧区间,<vsyncId> 对应本帧的 FrameTimeline VSync id。该区间的 wall time 也可包含阻塞等待,和纯 CPU Running 时间、GPU duration(GPU 执行时长)是三个不同的量。线程状态还要区分:

  • Running 长:线程获得 CPU 后工作量大;
  • Runnable 长:线程已可运行但没有及时上 CPU;
  • Sleeping/blocked 长:线程处于睡眠或阻塞状态,可能在等待 futex(线程同步原语)、Binder、buffer、fence 或其他资源。

提高 CPU 频率对阻塞等待的帮助有限;要解释这些等待,我们应把 sched_wakeup、sched_switch、线程状态、CPU frequency 和目标 slice 对齐,再讨论调度、cpuset(可运行的 CPU 集合)、uclamp(调度利用率上下限) 或 thermal(温控限制)。

四个关口

一帧可以按以下顺序排查:

  1. doFrame 内哪个 callback 的耗时先变长?
  2. RenderThread 是同步、绘制、GPU 提交,还是 dequeueBuffer 等待?
  3. queueBuffer 之后,BufferTX、latch 是否按时?
  4. App 按时交付后,SF actual、composition type、HWC/DisplayHAL 和 present 是否按时?

主线程短 + RenderThread 短 + Jank 只是起点;要下“SurfaceFlinger 或 HWC 问题”的结论,应同时核对 jank_type、present_type、on_time_finish 与 expected/actual 的区间末端。分类不是 None,App 也可能仍按时完成:固定 r1 的 SurfaceFrame::classifyJankLegacyLocked() 在 App 按时完成但提前 present 时,也可能标记 Prediction Error 或 SurfaceFlinger Scheduling。GPU completion、buffer transaction、acquire fence、display prediction error 都可能发生在两个 CPU slice 之外。

用 RecyclerView 滑动校准观察顺序

拿一次跟手滑动当例子,可以把上述关口连成一帧:

  1. ACTION_MOVE 经输入通道到达应用,同一 VSync 周期积累的 batched motion event 在 INPUT callback 中消费;滚动容器据此更新偏移并请求 redraw(重绘)。
  2. ANIMATION、INSETS_ANIMATION 与 TRAVERSAL 继续执行。RecyclerView 是复用 DisplayList,还是重新 bind(绑定数据)、measure、layout item,要以本帧 slice 和 layout request 为准。
  3. ThreadedRenderer.draw() 更新 root RenderNode,RenderThread 取得窗口 buffer、提交 GPU 命令并 queueBuffer()。
  4. BLAST 把 BufferItem 放入 transaction。SurfaceFlinger 计入待处理队列时 BufferTX 增加,latch 或 drop 后下降。
  5. SF 与 HWC 协商 composition type,DisplayFrame 与 present fence 给出显示栈时间边界;触摸到光子的完整时延还需要输入、driver 与外部测量。

手指抬起进入 fling 后,RecyclerView 的 ViewFlinger runnable 通过 postOnAnimation() 继续调度,在主线程回调中调用 OverScroller.computeScrollOffset() 计算位移——OverScroller 只承担状态与插值,动画节奏仍由主线程按帧驱动。INPUT 工作减少属于正常变化。该实现按本次归档的 AndroidX RecyclerView 源码快照 核对,目标应用仍需记录实际依赖版本。

拖动与 fling 的位移来源

图:拖动依赖输入,惯性滚动由按帧 runnable 继续更新位移。 两者仍进入同一宿主 HWUI 显示链;OverScroller 不是独立线程。

FrameTimeline 与 Jank 检测

FrameTimeline 从 Android 12 起为标准 App Window 提供 SurfaceFrame 和 DisplayFrame 的 expected/actual(预期/实际) 时间线。App 通过 FrameTimeline VSync id 选择预期 present 时间线;该信息沿 HWUI/BLAST transaction 进入 SurfaceFlinger。

我们先分清两类 token:

  • surface_frame_token 是应用工作对应的 VSync token,同一进程的多个 layer 可以共享它;须结合 upid 与 layer_name 区分记录;
  • display_frame_token 关联合成后的 DisplayFrame。

一个 DisplayFrame 可以包含多个进程、多个 layer 的 SurfaceFrame;追踪端到端要同时携带 surface token 与 display token 两个标识,二者各管一段。

多个 SurfaceFrame 汇入一个 DisplayFrame

图:显示合成帧可汇入多个进程、多个 Layer 的 SurfaceFrame。surface token 管应用侧的一帧,display token 关联合成后的帧,端到端追踪要把两个都带上。

Expected 与 Actual

比较 deadline(截止时间) 时,应使用完整区间:

  • expected end = expected.ts + expected.dur;
  • actual end = actual.ts + actual.dur;
  • overrun = actual end - expected end,表示超出预期截止时间的时长。

SurfaceFrame::setAcquireFenceTime() 把 App actual end 设置为 max(acquire fence signal time, queue time);fence 尚未 signal 时,暂用 queue time。对标准 HWUI 窗口,actual end 因而反映 Producer 完成可读与 buffer post(提交入队) 两个时间点中较晚的一项;UI thread 返回时刻在这里派不上用场。

jank_type 给出的是卡顿分类线索,根因还要靠对应阶段的记录来确定。Buffer Stuffing 要结合 dequeue wait、queued backlog、BufferTX、latch、release fence 和 recovery trace 来看;SQL jank_type 中的 SurfaceFlinger CPU Deadline Missed、SurfaceFlinger GPU Deadline Missed、Display HAL、Prediction Error 分别指向 SF CPU、SF GPU、显示 HAL 或预测误差,也要与对应的线程或硬件记录对齐。

下面的 Perfetto SQL 用于按 SurfaceFrame token 对齐目标应用的 expected/actual 记录:

INCLUDE PERFETTO MODULE android.frames.timeline;

WITH expected_unique AS (
  SELECT upid, surface_frame_token,
         MIN(id) AS expected_id, MIN(ts) AS expected_ts,
         MIN(dur) AS expected_dur
  FROM expected_frame_timeline_slice
  WHERE surface_frame_token IS NOT NULL
  GROUP BY upid, surface_frame_token
  HAVING COUNT(*) = 1 AND MIN(dur) >= 0
)
SELECT
  a.id AS actual_id, a.surface_frame_token, a.display_frame_token,
  a.ts AS actual_ts, a.dur AS actual_dur,
  e.expected_id, e.expected_ts, e.expected_dur,
  CASE WHEN e.expected_id IS NOT NULL
       THEN (a.ts + a.dur) - (e.expected_ts + e.expected_dur)
  END AS overrun_ns,
  a.jank_type, a.present_type, a.on_time_finish, a.layer_name
FROM actual_frame_timeline_slice AS a
JOIN process AS p ON a.upid = p.upid
LEFT JOIN expected_unique AS e
  ON a.upid = e.upid
 AND a.surface_frame_token = e.surface_frame_token
WHERE p.name = 'com.example.app'
  AND a.surface_frame_token IS NOT NULL
  AND a.layer_name = 'TARGET_LAYER_FROM_TRACE'
  AND a.dur >= 0
ORDER BY a.ts;

先把 com.example.app 和 TARGET_LAYER_FROM_TRACE 替换为现场进程名与精确 Layer 名;Layer 不保证包含包名。Actual 按 id/Layer 保留,Expected 按解析器的 upid + surface_frame_token 语义关联,不要求 Layer 相等;同 token 的多个 Layer 可共享一份预测。Expected 缺失、未结束或异常重复时,overrun_ns 为 NULL。overrun_ns > 0 只表示 Actual end 晚于 Expected end;要落到根因,还得回到对应时窗的 CPU、GPU、BufferQueue 与 SF/HWC,不同 Layer 的 Actual 也应分别统计。

Actual 按 Layer 保留,Expected 按进程与应用 token 唯一关联

图:固定 r1 的 Expected 共享、SQL 完整性与 deadline 解释合图。前一 SQL 扫描非空 Layer 的 jank 记录,后一 SQL 按现场精确目标 Layer 筛选;两者保留自身 WHERE,并保留独立的 Actual id/Layer 与 dur>=0。Expected 只在 upid+surface_frame_token 恰好一条且完整时匹配,Layer 不强制相等;LEFT JOIN 遇到缺失、未结束或异常重复时保留 Actual 并把 overrun 置为 NULL,如实标记匹配缺口。区间末端用 ts+dur 比较;App Actual end 取 max(queue,acquireSignal),fence pending 时暂用 queue;这个末端既非 UI 返回时刻,也非 present 时刻。正 overrun 只说明晚于预期,display token 另关联合成帧,jank/present 类型与阶段证据共同解释。

FrameTimeline 的覆盖边界

标准 HWUI App Window 是 FrameTimeline 覆盖较完整的场景。SurfaceView 主体、Camera、Video、游戏引擎或其他独立 Producer 不一定提供同样完整的 App actual slice。缺少目标 layer/token 时,应回到 producer queue、frame number、BufferTX、latch、HWC 和 present timing。

Compose 在标准 App Window 中的位置

纯 Compose 或 ComposeView 不会因声明式 UI 自动创建独立 Surface。在普通宿主中,Compose 内容仍通过 Android owner(连接 Compose 与 Android View/HWUI 的宿主对象)写入当前 App Window;syncAndDrawFrame() 之后继续走 RenderThread、BLAST、SurfaceFlinger 与 HWC。

Compose 改变的是 MainThread 生成 UI 内容的方式:

  • Composition 执行相关 composable(Compose UI 函数),更新需要重新生成的 UI tree;
  • Layout 测量节点尺寸并确定放置位置;
  • Drawing 把绘制操作交给 Android Canvas/HWUI。

状态在不同阶段被读取时,可能只触发对应阶段或后续阶段的必要工作,不表示每帧完整重做三阶段。SubcomposeLayout、Lazy layout 等可以在布局期间组合子项;intrinsic 查询与 Lookahead 也有自己的测量/预测边界。套用“普通测量过程允许同一 child 随意反复 measure”的概括会走偏,阶段交错要按对应的 Compose 实现逐个分析。

判断类型时看 Producer、Surface 和 layer:

页面结构默认分类
View 页面嵌 ComposeView标准 HWUI
Compose 页面嵌普通 AndroidView通常仍是标准 HWUI
Compose 中嵌 SurfaceView/视频/CameraSurfaceView 或混合类型
Compose 中嵌 TextureViewTextureView 或混合类型
页面主体为 WebView/Flutter 等引擎对应引擎类型

Compose Runtime、Compiler、Kotlin、BOM(集中约束依赖版本的版本清单) 和 tracing 的版本必须单独记录,strong skipping(跳过输入未变化的 composable)、slice 名或 compiler 行为要到对应版本里自己核对,android-17.0.0_r1 在这里给不出答案。没有 composition tracing 时,我们能看到窗口级 MainThread/HWUI 成本;要精确归因到某个 composable,就得补上 composition tracing。

系统策略的核对方法

ADPF hint 的效果核对

ADPF(Android Dynamic Performance Framework,Android 动态性能框架)的 PerformanceHintManager.Session 可以报告 target/actual work duration(目标/实际工作时长)。HWUI 的 CanvasContext / HintSessionWrapper 也会管理 hint session。提示交给 Power HAL(电源硬件抽象层) 和 vendor 实现后,效果由实现决定:绑定固定 CPU、保持固定频率或每次提升性能都只是可能性,API 并未把它们承诺下来。

若要证明 ADPF 产生效果,至少应对齐 session 上报、vendor hint 响应和后续调度/频率变化;单看频率升高或 API 调用,结论立不住。

HWC 路径看 composition 结果

HWC 能否使用 overlay,以及是否支持某种 transform(变换)、format(像素格式)、dataspace(颜色空间与范围)、blend(混合) 或 protected content(受保护内容),取决于设备实现和当时可用资源。应查看每个 layer 的 composition type、client composition、FrameTimeline GPU composition、HWC trace 或 dumpsys。

功耗下降、SF slice 变短或 present 提前,每一项都只是旁证;要坐实目标 layer 使用 DEVICE composition,还是回到 per-layer 的 composition type 和 HWC/GPU composition 证据。

预取的实际收益

RecyclerView GapWorker、Compose Lazy prefetch(预取)、图片预热和 shader/pipeline cache 可以把能够提前确定的工作移到关键 deadline 之前,但未知的后续 UI 状态它生成不了,INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT 顺序也不因此改变。

证明预取有效,需要看到 prefetch 工作提前执行,并且后续关键帧的 bind/measure/upload 或 pipeline creation 成本下降;滑动变顺只是结果。要认定“系统提前补帧”,还得看上面两条是否成立。

预取把已知工作移出后续关键帧

图:先证明工作提前发生,再确认后续关键帧的对应成本下降。预取只能搬已知工作,未知状态要等运行时才确定;Choreographer 五阶段顺序照旧。

结论与证据门槛

想确认的结论至少要保存的证据单独不足的信号
UI/RenderThread 调度等待下降同场景 runnable delay、线程状态、CPU 落点和优先级/cgroup(控制组)对比单帧 duration 变短
ADPF 产生设备侧响应HintSession 上报、vendor hint 响应、随后发生的调度或频率变化只有 API 调用或只有 cpufreq 上升
BufferQueue 深度改变max dequeued/acquired、slot/async 配置及 queue wait 对比dequeueBuffer 变短
stuffing recovery 生效recovery trace、主动 delay 与 backlog 回落只有 FrameTimeline Buffer Stuffing
HWC 使用 DEVICE compositionper-layer composition type、HWC 或 GPU composition 证据功耗下降、SF slice 变短或 present 提前

标准路径的源码入口

标准路径的平台源码入口如下:

Kernel 侧固定到 android17-6.18-2026-06_r6:

Vendor governor(厂商的频率调节策略)、Power HAL、GPU driver、Composer、display driver 与 panel 时序不在 common kernel 结论范围内,必须用目标设备源码和 trace 补充证据。

版本与实现边界

标准路径跨版本比较时,要单独核对的节点集中在下表:

平台标准页面相关节点分析影响
Android 11 / API 30BLAST 进入平台主线旧 tag 仍需按当时 Layer/transaction 对象分析
Android 12 / API 31App Window BLAST 与 FrameTimeline 形成现代基线;PerformanceHintManager 公开可关联 SurfaceFrame/DisplayFrame,ADPF 仍只提供 hint
Android 13 / API 33AutoSingleLayer latch-unsignaled 成为默认策略transaction latch 与 acquire-fence signal 可以分离
Android 14 / API 34标准 Choreographer/HWUI/BLAST/SF/HWC 拓扑延续SurfaceView 的 alpha/lifecycle 变化不要套到 App Window
Android 15 / API 35Window 可表达 desired HDR headroom;edge-to-edge(内容延伸到系统栏区域)可能改变 Insets/Traversal 成本HDR/Insets 变化不自动改变标准页面分类
Android 16 / API 36标准主拓扑延续跨版本实验要固定 vendor、刷新率、应用构建与 Jetpack 版本
Android 17 / API 37当前锚点:FrontEnd snapshot、现行 BLAST acquired-count、buffer stuffing recovery、预测 present time 与 HWC fast path方法名和 flag 以 android-17.0.0_r1 为准,不从当前 tag 反推首引版本

结论

标准 HWUI 页面的一帧由 MainThread 准备状态,RenderThread/GPU 生成 App Window buffer,BLAST 把 BufferItem 包装成 SurfaceControl transaction,SurfaceFlinger/HWC 完成合成与 present。

定位问题时,我们依次对齐 doFrame、syncAndDrawFrame/DrawFrames <vsyncId>、dequeue/GPU/queue、BufferTX、latch、FrameTimeline 和 present feedback。每个信号只回答一个阶段;只有按同一 SurfaceFrame、frame number 或明确的相邻时序把它们对齐,才能区分 UI 工作量、调度等待、GPU 执行、BufferQueue 等待、SF 合成与 display 后段延迟。