Flutter的渲染和布局

102 阅读6分钟

前言

介绍一下Flutter将小部件层次结构转换为绘制在屏幕上的实际像素所采取的一系列步骤。

Flutter的渲染模型

您可能想知道:如果Flutter是一个跨平台框架,那么它如何提供与单平台框架相当的性能?

我们来想一下安卓应用程序是如何工作的。绘图时,你首先调用安卓框架的Java代码。安卓系统库提供了负责将自己绘制到Canvas对象的组件,然后安卓可以用Skia渲染这个对象,Skia是一个用C/C++编写的图形引擎,它调用CPU或GPU来完成设备上的绘图。

跨平台框架通常通过在底层原生Android和iOSUI库上创建抽象层来工作,试图消除每个平台表示的不一致。应用程序代码通常用解释语言编写,如JavaScript,它必须反过来与Android的Java代码或iOS的Objective-C代码交互以显示UI。所有这些都增加了相当大的开销,尤其是在UI和应用程序逻辑之间存在大量交互的情况下。

相比之下,Flutter最大限度地减少了这些抽象,绕过了系统UI小部件库,转而使用自己的小部件集。绘制Flutter 的UI的Dart代码被编译成本机代码,该代码使用Skia(或者将来的Impeller)进行渲染。Flutter还嵌入了自己的Skia副本作为引擎的一部分,允许开发人员升级他们的应用程序,以保持最新的性能改进,即使手机没有更新新的Android版本。Flutter在其他本机平台上也是如此,例如Windows或macOS。

构建: 从Widget到Element

看一下这个Widget层次结构的代码片段:

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当Flutter需要渲染这个片段时,它调用build()方法,该方法返回一个小部件子树,该子树根据当前应用程序状态呈现UI。在此过程中,build()方法可以根据需要根据其状态引入新的小部件。例如,在前面的代码片段中,Container具有color和子属性。通过查看Container的源代码,您可以看到如果color不为空,它会插入一个表示颜色的ColoredBox:

if (color != null)
  current = ColoredBox(color: color!, child: current);

相应地,Image和Text小部件可能会在构建过程中插入子小部件,例如RawImage和RichText。因此,最终的小部件层次结构可能比代码所代表的更深,如图

image.png

这就解释了为什么当您通过调试工具(例如Flutter检查器,Flutter/Dart DevTools的一部分)检查树时,您可能会看到比原始代码中的结构要深得多的结构。

在构建阶段,Flutter将代码中表示的小部件转换为相应的Element tree,每个Widget对应一个Element。每个Element代表树层次结构给定位置中Widget的特定实例。有两种基本类型的元素:

ComponentElement,元素的宿主。

RenderObjectElement,一个参与布局或绘制阶段的元素。

image.png

RenderObjectElements是他们的Widget和底层RenderObject之间的中介,关于RenderObject我们将在后面讨论。

任何Widget的Element都可以通过其BuildContext引用,BuildContext是树中Widget位置的句柄。这是函数调用中的上下文,例如Theme. of(Context),并作为参数提供给build()方法。

因为Widget是不可变的,包括节点之间的父/子关系,所以对Widget树的任何更改(例如在前面的示例中将Text(“A”)更改为Text(“B”))都会导致返回一组新的Text对象。但这并不意味着必须重建底层表示。Element树在每个帧之间都是持久的,因此起着关键的性能作用,允许Flutter在缓存其底层表示的同时就像Widget层次结构是完全一次性的一样。通过只遍历更改的Widget,Flutter可以只重建Widget树中需要重新配置的部分。

布局和渲染

任何UI框架的一个重要组成部分是能够有效地布局Widget的层次结构,在它们呈现在屏幕上之前确定每个元素的大小和位置。

渲染树中每个节点的基类是RenderObject,它定义了一个用于布局和绘画的抽象模型。每个RenderObject都知道它的父节点,但除了如何访问它们及其约束之外,对它的子节点知之甚少。这为RenderObject提供了足够的抽象,以便能够处理各种用例。

在构建阶段,Flutter为元素树中的每个RenderObjectElement创建或更新一个继承自RenderObject的对象。RenderObjects是原语:RenderParagraph呈现文本,RenderImage呈现图像,RenderTransform在绘制其子级之前应用转换。

image.png

大多数Flutter小部件由继承自RenderBox子类的对象渲染,该子类表示2D笛卡尔空间中固定大小的RenderObject。RenderBox提供了盒子约束模型的基础,为要渲染的每个小部件建立最小和最大宽度和高度。

为了执行布局,Flutter在深度优先遍历中遍历渲染树,并将大小约束从父级传递给子级。在确定其大小时,子级必须尊重其父级给它的约束。子级通过在父级建立的约束内将大小传递给其父级对象来响应。

image.png

在这一次遍历树的结束时,每个对象都在其父对象的约束内具有定义的大小,并准备好通过调用油漆()方法进行绘制。

盒子约束模型作为一种在O(n)时间内布局对象的方法非常强大:

  • 父Widget可以通过将最大和最小约束设置为相同的值来规定子Widget的大小。例如,手机应用程序中最顶部的渲染对象将其子Widget限制为屏幕大小。(子Widget可以选择如何使用该空间。例如,他们可能只是在规定的约束内将他们想要渲染的内容居中。)
  • 父widget可以指定子Widget的宽度,但在高度上给予子级灵活性(或者指定高度,但在宽度上提供灵活性)。一个真实世界的例子是流文本,它可能必须符合水平约束,但垂直变化取决于文本的数量。

另外,子Widget还可以知道它有多少可用空间来决定如何呈现其内容。通过使用LayoutBuilder小部件,子Widget可以检查传递的约束并使用这些约束来确定如何使用它们,例如:

Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

所有RenderObjects的根是RenderView,它代表渲染树的总输出。当平台要求渲染新帧时(例如,因为vsync或纹理解压缩/上传完成),会调用compositeFrame()方法,该方法是渲染树根部的RenderView对象的一部分。这会创建一个SceneBuilder来触发场景的更新。当场景完成时,RenderView对象将合成场景传递给dart:ui中的Windows.render()方法,该方法将控制权传递给GPU来渲染它。