flutter渲染和布局
目次
本节描述了Flutter的渲染管道,即将一层层的Widgets层级转换为实际绘制在屏幕上的像素的系列步骤。
Flutter的渲染模型 #
你可能会好奇:如果Flutter是一个跨平台框架,它是如何实现和单平台框架接近的性能呢?
首先了解传统Android应用的工作方式是很有帮助的。当进行绘制时,你会先调用Android框架的Java代码。Android系统库提供了一些组件,它们负责将内容绘制到一个Canvas
对象上,之后Android会调用Skia(一个用C/C++编写的图形引擎)通过CPU或GPU将内容绘制到设备上。
跨平台框架通常通过在底层的原生Android和iOS UI库之上创建一个抽象层来工作,试图平滑处理各个平台UI表示之间的不一致。应用代码通常用解释型语言(如JavaScript)编写,并且必须与Java语言的Android库或Objective-C语言的iOS系统库交互来呈现UI。所有这些会增加显著的开销,尤其是在应用的UI与逻辑有大量交互时。
相比之下,Flutter通过尽量减少这些抽象层来提升性能。在其设计中,Flutter绕过原生的系统UI部件库,直接使用自己的插件化小组件集合(Widget)。Flutter的Dart代码会被编译为原生代码,并利用名为Impeller的渲染引擎进行渲染。Impeller随应用一起提供,这使开发者即使在终端设备没有升级到最新系统版本时,也能依赖Flutter框架轻松推进应用性能的改进。这种方式同样适用于Flutter运行在其他原生平台上(如Windows或macOS)。
注意 想了解Impeller支持哪些设备?请查看我可以使用Impeller吗?。更多内容请访问Impeller 渲染引擎。
从用户输入到GPU #
Flutter的渲染管道遵循的核心原则是简单即高效。Flutter为数据流向系统的过程设计了一个简洁的管道,如下图所示:

接下来我们将更详细地了解其中一些阶段。
构建阶段:从Widget到Element #
考虑以下一个展示组件层级的代码片段:
|
|
当Flutter需要渲染这一片段时,它会调用build()
方法,该方法返回一棵基于当前应用状态的UI子树。构建过程中,build()
方法可以根据状态引入新的组件。例如,在上述代码片段中,Container
具有color
和 child
属性。查看Container 的源码,可以发现当Color
不为空时,它会插入一个代表此颜色的ColoredBox
:
|
|
类似地,Image
和Text
组件在构建过程中可能会插入子组件,例如RawImage
和RichText
。因此真实的组件层级可能比代码表面上看起来更深,如下所示[2]:

这解释了为什么通过调试工具(如Flutter inspector)检查组件树时,你可能看到的结构要比你代码中写出的深得多。
在构建阶段,Flutter将代码中表达的组件翻译为对应的Element 树,每个组件都有一个对应的Element实例。每个Element代表组件树某个位置上某个组件的特定实例。Element主要有两种类型:
ComponentElement
:用于承载其他元素。RenderObjectElement
:参与布局或绘制阶段的元素。

RenderObjectElement
是其对应的Widget和底层RenderObject
之间的中介。
任何组件的Element可以通过它的BuildContext
进行引用,BuildContext
是树中组件位置的句柄。例如,在调用Theme.of(context)
时使用的context
,就是传递给build()
方法的参数。
由于Flutter的组件是不可变的(包括节点间的父/子关系),任何对组件树的更改(如将Text('A')
改成Text('B')
)都会返回一组新的组件对象。这并不意味着底层表示会完全重建。Element树从一个渲染帧到下一帧是持久化的,这在性能优化中起着重要作用。Flutter只会遍历发生变化的Widget,重新配置需要调整的Element树部分。
布局和渲染 #
在一个应用中,仅绘制单个组件的情况很少见。因此,任何UI框架的重要特性之一就是高效地布局整个组件树,确定每个元素在屏幕上的大小和位置。
每个渲染树节点的基础类是RenderObject
,它定义了一个抽象模型用于布局和绘制。这个模型非常通用:它并不限定维度的数量,甚至不固定使用直角坐标系(例如使用极坐标系统的示例)。每个RenderObject
只了解它的父节点,但对子节点知之甚少,除了知道如何“访问”它们以及它们的约束条件。这种抽象使得RenderObject
能够应对多种用例。
在构建阶段,Flutter会为Element树中的每个RenderObjectElement
创建或更新一个继承自RenderObject
的对象。RenderObject
是基础的渲染单位,例如:
RenderParagraph
:用于渲染文本;RenderImage
:用于渲染图像;RenderTransform
:对子组件进行变换后绘制。

大多数Flutter组件由继承自RenderBox
的对象进行渲染,RenderBox
是二维笛卡尔空间中固定大小的渲染对象。RenderBox
提供了一个盒约束模型,规定了每个组件在渲染时的最小和最大宽高。
在布局时,Flutter会以深度优先的顺序遍历渲染树,并从父节点向子节点向下传递大小约束。子节点在确定其大小时,必须遵守父节点提供的约束。子节点随后会通过向父节点 向上传递大小 进行响应,并且约束在子节点中得到了应用。

在单次遍历树之后,每个对象已根据父节点的约束条件定义了自己的大小,并准备通过调用paint()
方法进行绘制。
盒约束模型是一种强大的组件布局机制,其效率可达O(n) :
- 父组件可以通过将最大和最小尺寸设置为相同的值来完全控制一个子组件的尺寸。例如,顶层渲染对象会将其子组件的尺寸约束限定为整个屏幕大小,(子组件可以选择如何利用该空间,例如将要渲染的内容居中显示)。
- 父节点可以限制子节点的宽度,同时允许其对高度有一定的弹性(或相反)。一个实际的例子是流式文本,它可能需要适应水平约束但可以根据文本数量调整垂直方向。
即使在某些情况下子对象需要知道可用空间大小以决定内容的呈现方式,盒约束模型也能够正常工作。通过使用LayoutBuilder
组件,子对象可以检查传递下来的约束并依据约束决定其呈现方式,例如:
|
|
更多关于约束与布局系统的信息和示例,请参考理解约束。
所有RenderObject
的根节点是RenderView
,它代表整个渲染树的输出。当平台要求新帧被渲染时(例如由于垂直同步(vsync)或纹理解压缩/上传完成),会调用渲染树根节点RenderView
对象的compositeFrame()
方法。这会创建一个SceneBuilder
以触发场景更新。当场景完成后,RenderView
对象会将合成的场景传递给dart:ui
中的Window.render()
方法,并将控制权交给GPU以完成渲染。
关于渲染管道中合成和光栅化阶段的详细内容超出了本高层次概述的范围,更多信息可参见Flutter渲染管线的视频演讲。