首页
/ Flutter 引擎帧生命周期全解:从 RequestFrame 触发到栅格化呈现与资源回收

Flutter 引擎帧生命周期全解:从 RequestFrame 触发到栅格化呈现与资源回收

2026-09-06 15:42:01作者:卓炯娓

Flutter 应用的本质是:把 widget 树转换成描述屏幕绘制方式的 render tree,并随输入事件或时间流逝持续动画更新,其中每一个静止画面称为一帧(frame)。本篇以 Flutter 引擎(engine)侧的帧处理为主线,完整讲解一帧从 RequestFrame 触发、等待 vsync、构建 LayerTree、进入帧流水线(Pipeline),到 Raster 线程栅格化、GPU 提交与平台呈现,最后到帧资源清理与复用的全过程,并结合引擎源码与 Framework 侧调度代码,帮助读者建立"从源码级理解 Flutter 每一帧是如何被生产出来的"的完整心智模型。一帧最终被渲染进 FlutterView——在 Android 上通常是 SurfaceView、iOS 上是 UIView、Windows 上是 HWND、macOS 上是 NSView、Linux 上是 GtkBox 等平台视图。

一、一帧如何开始:RequestFrame 与触发源

所有帧都"诞生"于对引擎 AnimatorRequestFrame 的一次调用。请求来源多种多样:

  • Flutter 视图尺寸变化(resize);
  • 应用生命周期事件,如进入后台(backgrounding)或回到前台(foregrounding);
  • Dart 应用侧通过 dart:uiPlatformDispatcher.scheduleFrame 发起的请求;
  • 嵌入方(embedder)通过 embedder API 中 FlutterEngineScheduleFrame 发起的请求,其 C 接口定义可见 embedder.h,实现位于 embedder.cc

源码中的防重入:信号量去重

文档提到"Flutter 在帧被真正生产之前,会做最小化的簿记工作,主要是忽略重复的排帧请求"。这一点在 animator.cc 中可以直接印证:

  • Animator 持有一个初值为 1 的 pending_frame_semaphore_ 信号量;
  • RequestFrame 首先尝试 TryWait() 该信号量,若等待失败(说明已有一次待处理的排帧请求),则直接返回——多次调用 RequestFrame 最终只会产生对 VsyncWaiter 的一次请求;
  • 请求被接受后,引擎把 AwaitVSync 任务 Post 到 UI 线程任务运行器上,确保 vsync 等待回调尽量在 UI 线程没有处于昂贵调用中途时触发,随后置位 frame_scheduled_

等待 vsync:VsyncWaiter 抽象

帧被调度后,Flutter 会等待操作系统的垂直同步信号再往下推进。这一等待由 VsyncWaiter 抽象完成,它是"获取 vsync 回调的平台特定机制"的基类,不同平台有不同实现(如 VsyncWaiterAndroidVsyncWaiterEmbedder)。其关键接口包括:

  • AwaitVSync():意图是 Animator 希望生产一帧。实现侧应当在此"武装" vsync 闩锁(latch),并在 vsync 到来时恰好一次地调用 FireCallback,且不得阻塞当前线程;
  • AwaitVSyncForSecondaryCallback():意图只是在下一次 vsync 醒来,与帧调度无关,因此无需维护背压(backpressure)等调度不变量;
  • FireCallback(frame_start_time, frame_target_time):把回调调度到 UI 任务运行器上,且应当尽可能贴近 frame_start_time 执行。

从源码结构看,vsync 回调携带一个 FrameTimingsRecorder,用于记录本帧各阶段的时间点,这是后续帧耗时统计(FrameTiming)的基础。

二、构建帧:BeginFrame 与框架侧的 Scene 生产

Flutter 图形工作流的核心是帧流水线(frame pipeline)。流水线负责在 UI 线程(应用代码运行处)与 Raster 线程(执行栅格化与合成处)之间协调工作;引擎线程模型的更多细节可参考 The-Engine-architecture.md 中关于线程的章节。

vsync 到来时,Flutter 从 Animator 中恰如其名的 BeginFrame 开始生产帧。在引擎中,AwaitVSync 的 vsync 回调会把工作拆分为 BeginFrameEndFrame 两个私有方法同步背靠背调用——注释中解释了拆分原因:允许测试基础设施(如 ShellTest::PumpOneFrame)在两者之间插入一次 Render 调用,同时避免被普通 vsync 任务打断。

BeginFrame 的源码行为(见 animator.cc)包括:

  1. 结束本帧对应的 "Frame Request Pending" 异步 trace 事件,清空非帧内渲染产生的 layer tree 任务,递增帧号;
  2. 通过 FrameTimingsRecorder 记录 build start 时间点,并结束所有挂起的 PointerEvent trace flow;
  3. 在流水线中为这一帧预留位置:调用 layer_tree_pipeline_->Produce() 获取 ProducerContinuation。若拿不到有效 continuation(流水线已满,说明消费者 Rasterizer 消费太慢),则记录 PipelineFull 事件并调用 RequestFrame() 在下个 vsync 间隔重试——这是流水线背压机制的直接体现;
  4. 拿到 continuation 后,从 recorder 中读取 vsync 目标时间作为帧截止时限(dart_frame_deadline_),随后通过 delegate 的 OnAnimatorBeginFrame(frame_target_time, frame_number) 通知框架开始生产帧。

框架侧:onBeginFrame 到 Scene 生产

当引擎与 Flutter Framework 一起运行时,onBeginFrame 由框架中的 handleBeginFrame 处理(位于 scheduler/binding.dart),其职责是启动 Scene 的生产过程。这一过程的完整说明见 RendererBinding.drawFrame 的文档。整个流程最终产出一个 Scene,并通过 FlutterView.render 交还引擎。

在引擎侧,Scene 被表示为 LayerTree。调用 FlutterView.render 实际上是把 layer tree 经 AnimatorRender 方法转交:源码中 Animator::Render(view_id, layer_tree, device_pixel_ratio) 会把 LayerTreeTask 存入 layer_trees_tasks_(以 view_id 为键,忽略同一 view 的重复 Render 调用),并在没有当前帧计时器时自行创建一个(这正是 warm-up 帧能够"脱离 vsync 范围"调用 render 的关键路径,见下文第五节)。

随后 EndFrame 把本帧所有 view 的 layer tree 任务打包成一个 FrameItem,通过 producer_continuation_.Complete(...) 提交进流水线:若本帧 item 是流水线中的第一个,则通知 Rasterizer 开始绘制(OnAnimatorDraw);若不是第一个,它只是安静地排队,等待 Rasterizer 消费。

三、帧流水线 Pipeline:单生产者-单消费者的有界队列

文档将 pipeline 定义为"图形工作流的心脏",其实现位于 pipeline.h。这是一个单生产者、单消费者、带最大队列深度的线程安全资源队列,支持两个核心操作:

  • Produce(生产):生产者调用 Produce() 生成一个 ProducerContinuation,当流水线未满时,资源可被入队。资源就绪后,生产者对 continuation 调用 Complete(resource) 完成入队并唤醒等待中的消费者;若队列已满,Produce() 返回失败的 continuation。
  • Consume(消费):消费者调用 Consume(consumer) 阻塞等待资源,取到后回调执行消费逻辑,并返回 MoreAvailableDone 告知队列是否还有剩余 item。

几个与帧处理直接相关的细节:

  • 流水线深度决定背压强度:在 animator.cc 中,FramePipeline 的深度在启用 Metal 的平台上固定为 2;其他平台上,若 Platform 线程与 Raster 线程是同一个线程则深度为 1,否则为 2。深度越小,UI 线程"超前生产"的空间越小,背压越及时。
  • ProduceIfEmpty:源码中还提供 ProduceIfEmpty() 变体——只有当队列为空时才真正入队,队列非空时直接释放槽位,注释明确指出"由此返回的 continuation 不保证帧一定会被渲染"。
  • 内建 trace 支持:流水线自动生成 PipelineItem(从 Produce 到 Consume 的异步 flow)、PipelineProduce 事件以及 "Pipeline Depth" 计数器,因此用性能工具(timeline/trace)观察帧在流水线中的滞留时间是开箱即用的。

从源码结构看,FramePipelinePipeline<FrameItem>(类型别名见 rasterizer.h),一个 FrameItem 携带一组 LayerTreeTask(对应一个 vsync 中所有需要渲染的 view)与一个 FrameTimingsRecorder,即"一帧"在流水线里的完整表示。

四、栅格化:Rasterizer 把 LayerTree 变成像素

栅格化(rasterization)是把内存中的 layer tree 转换成表面上像素的过程。这部分代码运行在 Raster 线程上,并与 GPU 协同;在某些平台上,Raster 线程与 Platform 线程可以是同一线程(对应流水线深度取 1 的场景)。

栅格化始于 RasterizerDraw 方法调用:

  1. 从流水线取出 LayerTreeDraw(pipeline) 消费流水线中的下一个 FrameItem。文档特别解释了"为什么 Draw 接收的是流水线而不是 layer tree 本身"——流水线是帧工作负载跨线程簿记的载体,消费者必须持有并管理它引用的 layer tree(Rasterizer 还要保留上一个 layer tree 的引用直到下一帧渲染,用于外部纹理等场景),因此直接持有 layer tree 无法正确表达帧工作负载的生命周期。
  2. headless 检查:Rasterizer 会快速检查应用是否处于 headless 模式(例如已进后台)。源码中,这一约束由 shell 提供的 GetIsGpuDisabledSyncSwitch() 同步开关表达:某些平台上应用后台化时,GPU 操作必须被禁止。若命中,帧将被丢弃。
  3. 获取表面:栅格化以向 SurfaceAcquireFrame 方法请求"GPU 可绘制表面"开始。该调用把平台相关逻辑委托给各 embedder 实现——embedder 在 FlutterRendererConfig 中配置的回调会为其获取合适的 Metal、OpenGL、Vulkan 或软件表面。
  4. Preroll 与 Paint 递归遍历:表面就绪后,LayerTree 经由逐层的递归 PrerollPaint 调用被栅格化到表面上。这些调用的行为因 layer 类型而异,但最终一般归结为通过 Skia 或 Impeller 执行绘制。
  5. 提交与呈现:layer tree 遍历完毕、所有图形操作收集完成后,帧被提交给 GPU;embedder 会收到回调以执行平台侧的后续处理——通常是通过平台视图实现把表面呈现(present)出来。

Draw 的返回状态在 rasterizer.h 中以 DrawStatus 枚举体现(kDonekNotSetUpkYieldedkPipelineEmptykGpuUnavailable),单 view 的绘制结果则有 DrawSurfaceStatuskSuccesskRetrykFailedkDiscarded)——其中 kDiscarded 的注释明确说明"layer tree 尺寸与 view 尺寸不匹配时丢弃,通常发生在 resize 期间",这正好对应第一节中"resize 触发 RequestFrame"的场景。

整个"生产—流水线—栅格化"过程循环往复,直到流水线排空。

五、Warm-up frame:抢占 vsync 之前的时间窗口

通常情况下,Flutter Framework 在收到操作系统的 vsync 事件后才开始生产帧。但应用启动(或热重载)后,vsync 可能要数毫秒之后才到来。为了利用"widget 树首次配置完成"到"引擎请求刷新"之间的时间,框架会调度一个 warm-up frame(预热帧),入口是 PlatformDispatcher.scheduleWarmUpFrame,框架侧实现在 SchedulerBinding.scheduleWarmUpFrame

  • 预热帧可能永远不会真正渲染——因为它在 PlatformDispatcher.onBeginFrameonDrawFrame 的作用域之外调用了 FlutterView.render,引擎并没有为它准备合法的"绘制上下文";
  • 但它会迫使框架完整走一遍构建(build)、布局(layout)、绘制(paint)流程,这三步合计可能耗时数毫秒。因此当引擎随后请求真正的帧时,大部分工作已经完成,框架只需极少的额外工作即可产出帧。

框架实现中有几个值得注意的工程细节(见 binding.dartscheduleWarmUpFrame 的实现):

  • 该调用会在 runApp 启动时、RendererBinding.performReassemble 热重载时、以及 allowFirstFrame 解除 deferFirstFrame 阻塞时被调度;
  • 它会 lockEvents 锁定事件,防止触摸等事件插队到预热帧之前;
  • 预热帧完成后会调用 resetEpoch() 重置时间戳纪元,避免热重载场景下时间戳出现"回跳"导致隐式动画在旧时间被触发并逐帧跳完;
  • 引擎侧对应的是 Animator::Render 中"框架可以直接带着构建好的 scene 调用 render"的分支(见 animator.cc 注释 "A major reason is to render warm up frames"),以及 Animator::OnAllViewsRendered():普通帧在 vsync 任务结束时自然收尾,而 warm-up 帧由于 Render 发生在 vsync 任务之外,需要这个显式的"所有 view 已渲染"通知来告诉 Animator 何时把 layer tree 送入流水线。

此外,启动时预热帧可能在引擎报告首个 view metrics(PlatformDispatcher.onMetricsChanged)之前就产生,因此第一帧可能以零尺寸产出。

六、帧资源的清理与复用

原文档中"Cleaning up frame resources"一节尚标注为待补充,但当前仓库源码已经提供了清晰的实现线索,可以据此梳理帧资源的回收机制:

  • 复用上一帧的 layer tree:当不需要重新生成 layer tree 时(例如仅外部纹理更新),Animator::AwaitVSync 的回调会走 DrawLastLayerTrees 分支而非 BeginFrame,直接委托 RasterizerDrawLastLayerTrees 重绘上一次的 layer tree。Rasterizer::GetLastLayerTree(view_id) 提供对上一帧 layer tree 的访问。源码注释解释了这一优化的动机:视频、相机流等外部纹理的更新节奏与 Flutter 应用不同步,引擎可以只带更新后的纹理重绘同一份 layer tree,而不必等框架重新生成描述相同内容的 tree。
  • 视图资源释放Rasterizer::CollectView(view_id) 要求在 view 被移除时显式释放其显示资源,因为 Rasterizer 在被请求绘制未知 view 时会隐式分配资源。
  • 表面生命周期Rasterizer::Setup(surface)Rasterizer::Teardown() 成对管理屏幕表面的生命周期,teardown 后不能再渲染,且 Skia 资源缓存设置会被失效,需要在新表面获取后重新设置。

从源码结构看,"保留上一帧 layer tree 引用直到下一帧"与"流水线持有正在渲染帧的引用"是同一套生命周期约束的两面:流水线负责帧工作负载的跨线程记账,Rasterizer 负责 GPU 资源与上一帧数据的持有,二者的交接点正是 FrameItem

七、关键源码索引

关注点 源码位置
帧调度与 BeginFrame/EndFrame/Render animator.hanimator.cc
有界帧流水线(Produce/Consume/背压) pipeline.h
vsync 等待抽象与平台回调 vsync_waiter.h
栅格化、表面获取、DrawStatus rasterizer.h
渲染表面(AcquireFrame) surface.h
LayerTree 数据结构 layer_tree.h
框架侧 scheduleWarmUpFrame / handleBeginFrame binding.dart
embedder API:FlutterEngineScheduleFrame embedder.h

小结

把整条链路串起来:触发源调用 Animator::RequestFrame(信号量去重)→ VsyncWaiter 等待 vsync → BeginFrame 在流水线 Produce() 预留位置(满则下帧重试)→ 框架 handleBeginFrame 驱动 build/layout/paint 产出 Scene,经 FlutterView.render 进入 Animator::RenderEndFrameFrameItem 形式 Complete 入流水线 → Rasterizer::Draw 消费流水线,经 headless 检查、Surface::AcquireFrame 获取 Metal/OpenGL/Vulkan/软件表面,递归 Preroll/Paint 由 Skia 或 Impeller 绘制并提交 GPU → embedder 回调呈现表面;期间 scheduleWarmUpFrame 在启动与热重载时抢占真实 vsync 前的空闲时间。掌握这条链路后,你既能读懂 timeline 中 Animator::BeginFramePipelineFullPipelineItem 等事件的真实含义,也能定位"掉帧发生在生产侧还是消费侧"这类问题的根源。

登录后查看全文
热门项目推荐
相关项目推荐