Skip to main content

Chapter 165: Android Deep Path Frame Rendering

本章用一帧画面作为贯穿材料:用户滚动一个基于 View 的列表,列表某一项状态变化触发重新绘制,最终这次变化显示到屏幕上。读完本章后,读者应能追踪这帧从 invalidatelayoutdrawRenderThread、GPU、SurfaceFlinger、Hardware Composer 到面板扫描输出的责任链,并能把一次掉帧定位到 app、render、GPU、composition 或 display 的边界上。

Android 图形路径的核心事实是:应用生成图形缓冲,系统合成当前可见 layer,显示硬件按刷新节奏把最终结果送到面板。AOSP 图形文档把 Surface 描述为 buffer queue 的 producer 侧,并说明可见 surface 最终由 SurfaceFlinger 合成到显示器;SurfaceFlinger 还会使用 OpenGL 与 Hardware Composer 共同完成合成工作,相关背景见 AOSP Graphics

本章讨论的是 Android View 系统常见路径。Compose、游戏引擎、直接使用 Vulkan 或 OpenGL ES 的应用会改写 app 层的绘制入口,但从 surface、buffer、fence、SurfaceFlinger、Hardware Composer 到 panel 的后半段仍然受到同一组系统边界约束。版本边界上,FrameTimeline 需要 Android 12 及以上,JankStatsframeOverrunNanos 也依赖 Android 12 及以上的额外平台数据,详见 Perfetto FrameTimelineJankStats 文档

本章的结论先放在开头:一帧能否准时显示,取决于多段时间预算是否连续满足。app 主线程要在当前 vsync 周期内完成输入、动画、measure、layout、draw 与状态提交;RenderThread 和 GPU 要完成渲染命令与图形工作;producer 要把 buffer 交给 BufferQueueSurfaceFlinger 要 latch buffer、应用 transaction、决定合成方式并提交 present;Hardware Composer、display controller 与 panel 要在目标刷新点前完成硬件侧显示工作。任何一段延迟都会改变用户看到的结果。

165.1 Frame Rendering 的跨层路径总览

Frame Rendering 在本章中指“一次应用可见状态被转换成屏幕像素”的跨层过程。它从应用对象状态变化开始,以面板扫描输出结束。中间经过的对象分属不同责任主体:View 树属于应用进程,RenderThread 属于应用进程内的渲染执行侧,GPU driver 和 graphics HAL 属于平台与厂商边界,SurfaceFlinger 是系统 compositor,Hardware Composer 和 display controller 负责显示硬件路径。

下面的图把本章贯穿的一帧放入完整路径。图的边界是 View 系统里的普通窗口渲染,不覆盖视频解码直出、相机预览、游戏自建 swapchain 的专用优化路径。

这条路径中有两类交接。第一类是线程内和进程内交接,例如 main thread 把 View 状态同步给 RenderThread。第二类是跨进程或硬件边界交接,例如应用通过 BufferQueueGraphicBuffer 交给 SurfaceFlingerSurfaceFlinger 再把合成计划交给 Hardware Composer。定位掉帧时,先判断延迟发生在前半段的 app/render 侧,还是发生在后半段的 composition/display 侧。

VSync 是这条路径的时钟基准。Android 官方文档说明,VSync 同步 app render、SurfaceFlinger composition 与 Hardware Composer present 的节奏,用于减少 stutter 并提升图形表现;HWC 会把 VSync 事件通过 callback 发送给 SurfaceFlingerSurfaceFlinger 再根据显示刷新周期调度后续工作,见 AOSP VSync。这意味着一帧的分析单位应使用目标刷新周期,而非只看某个函数是否“耗时较长”。

以 60 Hz 屏幕为例,常见预算约为 16.67 ms;90 Hz 约为 11.11 ms;120 Hz 约为 8.33 ms。这个数字覆盖的是一帧要赶上的刷新节奏。应用完成主线程工作以后,还要留出 RenderThread、GPU、buffer handoff、SurfaceFlinger 与显示硬件的时间。因此一个主线程 10 ms 的帧在 60 Hz 上可能还能显示流畅,在 120 Hz 上就可能压缩后续阶段的余量。

165.2 App Layer:View Hierarchy、Layout、Draw、Invalidate

View 层负责把应用状态转换成可绘制的 UI 描述。状态变化通常来自输入事件、动画 tick、数据绑定、图片加载完成或业务回调。状态改变以后,View 通过 invalidate 标记绘制区域变脏,或者通过 requestLayout 请求重新 measure 和 layout。invalidate 主要影响绘制内容,requestLayout 会推动尺寸和位置重新计算,后者通常开销更大。

View 树的工作顺序可以概括为 measure、layout、draw。measure 计算子 View 期望尺寸,layout 确定每个 View 的位置,draw 把背景、内容、子节点和前景转换成绘制命令。现代 Android 的 View 绘制会把这些命令组织成 display list 或 RenderNode 状态,再同步给渲染侧执行。应用代码看到的是 onDrawonMeasureonLayout 之类回调,系统路径里真正要交付的是可被渲染线程消费的绘制状态。

列表滚动是观察 View 层成本的典型材料。一次 fling 期间,RecyclerView 可能在同一帧里处理输入、计算滚动距离、复用 item、绑定数据、触发布局、绘制 item 装饰和更新滚动条。如果 onBindViewHolder 里发生同步图片解码、复杂文本排版、频繁 Binder 调用或锁等待,主线程就会延后到达绘制提交点。用户看到的是列表移动不连续,FrameTimeline 或 Systrace 里则表现为 app actual timeline 超过 expected timeline。

View 层常见失败点有三个。第一,布局级联过大,一个局部状态变化推动整棵树重新 measure 和 layout。第二,绘制命令过重,例如大量阴影、复杂 path、过度 alpha blending 或重复绘制背景。第三,主线程被非绘制工作占用,例如同步 I/O、数据库查询、锁竞争或 Binder 调用。Android 性能文档把 UI rendering 描述为 app 生成一帧并显示到屏幕的行为,并指出超过刷新窗口会导致系统跳过帧、用户感知为 jank,见 Android slow rendering

对 View 层做责任判断时,应使用“状态变化范围 → traversal 成本 → 绘制命令规模 → 提交时机”的顺序。局部颜色变化只应推动局部重绘;尺寸变化可能推动父子布局重算;数据集变化可能推动多个 item 重新绑定。这个顺序能把业务代码、View 树结构和系统渲染截止点连接起来。

165.3 Runtime / Thread Layer:Main Thread、RenderThread、Choreographer

Runtime 与线程层把 View 状态变化放入显示节奏。Choreographer 的工作定义来自 Android API 文档:它协调 animation、input 和 drawing 的 timing,并从 display subsystem 接收类似 vertical synchronization 的 timing pulse,然后调度下一帧渲染工作,见 Choreographer API。因此 Choreographer#doFrame 是 View 系统一帧的关键入口。

Main thread 在一帧里处理多个队列来源:输入事件、动画回调、View traversal、应用 posted task、生命周期回调和业务消息。它的风险在于所有这些工作共享同一个 LooperMessageQueue。当业务代码把网络回调后的重数据处理、同步文件读取或大量对象分配投到主线程时,系统并没有额外的显示专用主线程来替它完成 traversal。

RenderThread 承担应用进程内的渲染执行侧。它接收 main thread 同步过来的 View 渲染状态,执行 display list,调用 Skia 后端,准备 GPU command,并在合适时机把结果提交到 surface。RenderThread 的存在让部分渲染工作脱离 main thread,但状态同步仍然需要主线程参与。Android 性能文档也提示,UI thread 在 syncFrameState 期间可能等待 RenderThread 安全复制 UI 线程使用的数据,RenderThread 也可能在帧开始时通过 IPC 获取 buffer 或在 eglSwapBuffers 交回 buffer 时被阻塞,见 slow rendering 中的线程说明

这三者的责任边界可以这样判断:Choreographer 决定一帧何时开始;main thread 决定当前 UI 状态能否按时完成 traversal;RenderThread 决定渲染命令能否按时生成并提交。一次掉帧如果表现为 doFrame 开始晚,先看主线程是否被前一个消息占用或被调度延迟。如果 doFrame 准时开始但 traversal 长,问题多半在 View 层和业务回调。如果 traversal 结束后 RenderThread 或 GPU 阶段拖长,责任已经越过单纯 View 回调。

在高刷新率设备上,这个边界更敏感。60 Hz 下一个 12 ms 的主线程 traversal 仍可能留下部分后续余量;120 Hz 下同样的 12 ms 已经超过一帧周期。排查时要把设备刷新率、当前 frame deadline、main thread 工作、RenderThread 工作和 GPU completion 放在同一条时间线上比较。

165.4 Graphics Layer:Skia、OpenGL ES / Vulkan、GPU Command

Graphics layer 把 View 层的绘制描述转换成 GPU 可执行工作。Android 上常见的 2D UI 绘制由 Skia 承担,Skia 后端再通过 OpenGL ES、Vulkan 或平台支持的图形路径生成 GPU command。应用直接使用 OpenGL ES 或 Vulkan 时,会绕过 View 的部分绘制抽象,但仍然要把最终图像写入一个 surface 对应的 buffer。

GPU command 的执行具有异步特征。CPU 侧准备命令、提交命令;GPU 按队列执行 shader、纹理采样、blend、raster、tile memory 或 render target 操作;完成状态通过 fence 暴露给后续阶段。应用线程看到 eglSwapBuffers 或 Vulkan present 相关调用返回,并不等价于所有 GPU 像素工作已经完成。后续的 SurfaceFlinger 或 display 硬件会依据 fence 判断 buffer 何时可安全消费。

Skia 和 GPU 阶段的常见成本来自绘制复杂度与内存带宽。复杂 path、模糊阴影、频繁离屏渲染、大纹理上传、过多 alpha blending、多个 render target 切换、shader 编译抖动和高分辨率 framebuffer 都会延长这一段。移动 SoC 还受到功耗和温控约束;持续高 GPU 负载可能触发降频,后续帧的 GPU completion 时间随之增加。

一个可迁移的判断顺序是:先看 main thread 是否准时提交;再看 RenderThread 是否在执行绘制命令时超时;再看 GPU completion 是否晚于 post time;最后把 GPU fence 与 SurfaceFlinger 的 latch 时间对应起来。Perfetto FrameTimeline 把 app actual timeline 的结束定义为 max(gpu time, post time),这正好说明 CPU post 和 GPU 完成需要一起看,见 FrameTimeline actual timeline

当列表滚动里出现“主线程耗时正常但画面仍掉帧”时,图形层是下一段检查对象。典型场景是每个 item 都包含圆角裁剪、大图缩放和阴影,main thread 很快生成命令,但 GPU 在多个 frame 上持续晚完成。此时减少 View 回调耗时不能解决根因,应该收敛绘制复杂度、稳定纹理尺寸、降低离屏渲染次数,并检查 GPU timeline。

165.5 Surface Layer:Surface、BufferQueue、GraphicBuffer

Surface layer 是应用渲染结果进入系统合成路径的交接面。Surface 可以理解为应用写入图形缓冲的入口;它背后连接 BufferQueue 的 producer 侧。AOSP 文档说明,几乎所有在系统中移动图形 buffer 的路径都依赖 BufferQueue,consumer 创建并拥有这个结构,producer 通过 dequeueBuffer 取得空闲 buffer,填充后通过 queueBuffer 交回,consumer 再通过 acquireBuffer 取得内容,见 BufferQueue and Gralloc

GraphicBuffer 承载真实图像内存。Gralloc 根据宽、高、像素格式和 usage flags 分配图形内存,buffer 在进程之间通过 handle 传递,buffer 内容本身通常不做大规模复制。这个设计把移动端图形路径的重点从“复制像素”转成“传递所有权、同步状态和使用 fence”。

Surface layer 有两个关键同步点。第一个是 producer 取得 buffer。如果 queue 里没有可用 buffer,producer 可能阻塞在 dequeue 阶段。第二个是 consumer 取得已完成 buffer。如果 GPU fence 尚未 signal,consumer 即使拿到 handle,也要等待内容安全可读。用户感知的延迟可能来自任何一个同步点。

BufferQueue 还会制造一种特殊的流畅但高延迟状态。Perfetto 文档把 BufferStuffing 描述为 app 持续向 SurfaceFlinger 发送新 frame,内部 buffer queue 堆积了尚未 present 的 buffer,后续 frame 至少晚一个 vsync 才显示,并可能进入 dequeue blocking wait,见 FrameTimeline jank types。这类问题的用户表现通常是动画帧率看起来稳定,但输入到显示的响应滞后。

定位 surface 层时,应看四个对象:producer 是否准时 queue buffer,buffer 是否被 fence 阻塞,queue 深度是否持续升高,consumer 是否按预期 acquire。把这四点串起来后,才能判断延迟来自 app 生产过快、GPU 完成过晚、buffer 数量不足,还是 SurfaceFlinger 消费节奏发生变化。

165.6 System Composition Layer:SurfaceFlinger、Layer、Transaction

SurfaceFlinger 是 Android 显示路径中的系统 compositor。AOSP 图形文档说明,SurfaceFlinger 消费当前可见 surface,并利用 WindowManager 提供的信息把它们合成到显示器;文档还明确指出 SurfaceFlinger 是能修改显示内容的系统服务,见 AOSP Graphics components。这一定义给出了系统边界:应用提交的是自己窗口的 buffer 和状态,最终屏幕由 SurfaceFlinger 统一决定。

Layer 是 SurfaceFlinger 处理显示对象的基本单位。每个应用窗口、状态栏、导航栏、输入法、弹窗、动画 leash 或截图层都可能对应一个或多个 layer。Layer 状态包括位置、大小、裁剪、alpha、z-order、变换、可见性、buffer、dataspace、secure flag 和 damage region 等。应用侧或 WindowManager 侧通过 transaction 更新这些状态。

一帧进入 SurfaceFlinger 后,关键步骤是接收 transaction、latch buffer、更新 layer state、计算可见区域、决定合成策略、提交 present。transaction 晚到会导致状态变化错过当前合成点;buffer 晚到会导致当前 layer 继续使用旧 buffer;damage 或可见区域计算异常会扩大合成成本;z-order 或 alpha 变化可能让原本可由硬件 overlay 处理的 layer 退回 GPU composition。

SurfaceFlinger 的合成策略通常在 client composition 和 device composition 之间选择。client composition 由 SurfaceFlinger 作为 OpenGL ES 客户端使用 GPU 合成一个目标 buffer;device composition 由 Hardware Composer 使用显示硬件合成多个 layer。实际路径受 overlay plane 数量、格式、缩放、旋转、alpha、HDR、protected content、display mode 和厂商实现限制影响。

对一次掉帧做系统合成层判断时,先看 app frame 是否已经按时交给 SurfaceFlinger。如果 app late,composition 侧只能复用旧内容或推迟显示。如果 app on time,而 SurfaceFlinger actual timeline 超过 expected timeline,就应检查合成 layer 数量、GPU composition 时间、device composition 阻塞、transaction 批量更新和显示模式切换。FrameTimeline 会把部分问题归类为 SurfaceFlingerCpuDeadlineMissedSurfaceFlingerGpuDeadlineMissedDisplayHAL,这些分类直接对应 composition 与 display 边界。

165.7 Hardware Layer:Hardware Composer、Display Controller、Panel

Hardware layer 负责把系统合成计划落到显示硬件。Hardware Composer 是 display subsystem 的 HAL 抽象。AOSP 文档说明,SurfaceFlinger 可以把部分 composition 工作委托给 HWC,以减轻 OpenGL 和 GPU 工作;HWC 还必须支持事件,其中包括 VSync,见 Hardware Composer 说明。因此 HWC 同时承担合成能力暴露和显示节奏反馈。

HWC 的核心价值是使用 display controller、overlay plane 和硬件 scaler 等单元直接合成 layer。对于视频播放、全屏 surface、状态栏叠加、受保护内容或低功耗场景,device composition 能减少 GPU 工作和内存带宽压力。它的限制也来自硬件:overlay plane 数量有限,某些像素格式、缩放比例、旋转角度、alpha 组合、HDR dataspace 或 secure buffer 组合可能超出硬件能力。

Display controller 接收合成后的图层或目标 buffer,按 display mode 和刷新周期驱动 panel。面板侧还涉及刷新率、tearing control、亮度、HDR、variable refresh、触控同步和功耗策略。对应用而言,这些通常是不可直接控制的系统事实;对系统分析而言,它们决定 present fence、retire fence 与用户真正看到像素之间的时间关系。

硬件层的失败表现有三类。第一,HWC 无法使用 device composition,SurfaceFlinger 退回 GPU composition,GPU 负载增加。第二,DisplayHAL 或 display controller 完成 present 晚于目标 vsync,FrameTimeline 可能把责任标记到 display 边界。第三,刷新率或显示模式变化改变预算,让原本刚好赶上的 app/render 工作开始错过 deadline。

把 hardware layer 放回列表滚动例子中,常见现象是:app 与 RenderThread 看起来准时,SurfaceFlinger CPU 也没有明显超时,但 present 仍晚。此时应检查 SurfaceFlinger 和 HWC 之间的 present path、composition type、display mode、fence 时间,以及是否存在受保护 layer、HDR layer、过多半透明 layer 或硬件 plane 限制。

165.8 Jank、Missed Frame、VSync、FrameTimeline 的系统分析路径

Jank 在系统分析中应被定义为“frame 没有按调度器预测的 present 时间显示”。Perfetto FrameTimeline 文档给出的定义是:当帧实际显示时间与调度器预测 present time 不匹配时,该帧被视为 janky;它会引起不稳定帧率或输入延迟增加,见 Android Jank detection with FrameTimeline。这个定义比单纯看某个函数耗时更适合跨层分析。

FrameTimeline 提供 expected timeline 和 actual timeline。expected timeline 表示系统给 app 或 SurfaceFlinger 的目标时间窗口;actual timeline 表示实际完成所用时间。App actual timeline 从 Choreographer#doFrame 或 native choreographer callback 开始,到 max(gpu time, post time) 结束。SurfaceFlinger actual timeline 覆盖它主线程开始处理到屏幕更新的路径,并把 composer 和 DisplayHAL 放入显示栈下游分析中。

掉帧分析应按同一顺序执行:先确认刷新率和 deadline,再看 app expected/actual 是否匹配,再看 RenderThread 与 GPU completion,再看 buffer queue 是否堆积,再看 SurfaceFlinger expected/actual,再看 composition type 与 display present。这个顺序的好处是不会把后段症状误归因给前段,也不会把 app 生产过快导致的 latency 问题误判成单帧 CPU 过慢。

常见分类可以映射到责任边界。AppDeadlineMissed 指 app 从 choreographer wake-up 到 post 或 GPU 完成的总时间超过预期。BufferStuffing 指 app 持续提交新 buffer,queue 堆积造成迟到显示。SurfaceFlingerCpuDeadlineMissed 指 compositor 主线程超时。SurfaceFlingerGpuDeadlineMissed 指 compositor CPU 侧可能按时,但 GPU composition 未赶上。DisplayHALSurfaceFlinger 已经按时交给 HAL,但显示硬件没有在目标 vsync present。

Android Vitals 的 slow rendering 和 frozen frames 适合做质量入口。官方文档把 16 ms 到 700 ms 之间的超时帧归入 slow frames,把超过 700 ms 的 UI frame 归入 frozen frames,并说明这些统计主要覆盖基于 View 或 Canvas 的 UI toolkit,详见 Android Vitals slow rendering。工程排查时,可以先用这些指标找到用户可见问题,再用 FrameTimeline 或 Perfetto 分解责任边界。

最终的判断句应回到贯穿材料:一次列表滚动掉帧,先确定该帧的目标刷新周期;若 app actual 晚于 expected,检查主线程、View traversal、RenderThread、GPU 和 buffer post;若 app on time 而 SurfaceFlinger actual 晚,检查 layer、transaction、composition 和 HWC;若 SurfaceFlinger on time 而 present 晚,检查 DisplayHAL、display mode、fence 和 panel path。这样才能把“卡顿”还原为一段具体的系统路径。

最小自检任务

一个基于 View 的 Android 列表页面在 120 Hz 设备上滚动时出现明显滞后。FrameTimeline 显示某个应用帧的 expected window 约为 8.33 ms,app actual timeline 为 17 ms,jank type 是 AppDeadlineMissed;同一 display frame 中 SurfaceFlinger actual timeline 按时完成,composition type 主要是 device composition;Perfetto 中该帧的 main thread 在 doFrame 内执行了较长的 item 绑定和 layout,RenderThread 没有明显 GPU deadline miss。请写出这次 jank 的跨层路径、责任主体、资源边界、策略判断和用户可见结果。

答案要点

  • 这次问题首先应归入 app/render 前半段,因为 app actual timeline 已经超过 120 Hz 下约 8.33 ms 的 frame window,而 SurfaceFlinger 和 device composition 按时完成。
  • 请求路径是列表滚动触发状态更新,View 层执行 invalidate 或 layout 请求,Choreographer#doFrame 调度 main thread traversal,main thread 处理 item 绑定、measure、layout、draw,再把渲染状态同步给 RenderThread
  • 责任主体是应用进程内的主线程和 View 层业务代码,直接证据是 doFrame 内 item 绑定和 layout 耗时较长;SurfaceFlinger、HWC 和 display controller 在这个样例中属于下游按时消费方。
  • 资源边界是 120 Hz 刷新周期下更短的 per-frame 时间预算。17 ms app actual 已经超过两个 120 Hz 周期附近的量级,后续 RenderThread、buffer handoff 与 display present 没有足够余量。
  • 策略判断是 FrameTimeline 把该帧标记为 AppDeadlineMissed,表示从 choreographer wake-up 到 post 或 GPU 完成的 app 侧总时间超过预期。
  • 用户可见结果是滚动位置或 item 状态晚于手指和动画节奏显示,表现为列表滞后、跳动或局部帧缺失。
  • 修复方向应先收敛 item 绑定和 layout 范围,减少同步工作、降低布局级联、稳定 item 尺寸,再复查 app actual timeline 是否回到 expected window 内。

本章知识点总结

  • 一帧路径:Android View 帧从 app 状态变化开始,经过 main thread、RenderThread、GPU、surface、SurfaceFlinger、HWC 和 panel 才变成屏幕像素。
  • VSync 基准:VSync 把 app render、SurfaceFlinger composition 和 HWC present 对齐到显示刷新周期。
  • View 责任:View 层负责把状态变化转换成 measure、layout、draw 和 display list 更新。
  • 主线程边界:main thread 同时处理输入、动画、traversal、业务消息和生命周期回调,任何长任务都会压缩帧预算。
  • 渲染线程RenderThread 接收 View 渲染状态,执行 Skia 后端和 GPU command 准备,并与 surface buffer 交接。
  • GPU 判断:GPU 问题要同时看 CPU post、GPU completion 和 fence,因为命令提交返回不等于像素工作完成。
  • Surface 交接Surface 连接 producer 侧,BufferQueue 通过 dequeue、queue、acquire、release 管理 producer 与 consumer 的 buffer 流转。
  • Buffer 延迟:queue 堆积会造成流畅但高延迟的显示状态,FrameTimeline 可将其标记为 BufferStuffing
  • 系统合成SurfaceFlinger 负责 latch buffer、应用 transaction、维护 layer 状态并选择 client 或 device composition。
  • 硬件合成:Hardware Composer 利用显示硬件合成 layer,但受 overlay 数量、格式、变换、HDR 和 secure buffer 等能力限制。
  • FrameTimeline:FrameTimeline 用 expected timeline 与 actual timeline 比较 app、SurfaceFlinger 和 display path 是否按时完成。
  • 归因顺序:掉帧定位应按 app、render、GPU、buffer、composition、display 的顺序缩小责任边界。