opoojkk

flutter渲染和布局

xx
目次

原文:Flutter architectural overview | 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 #

考虑以下一个展示组件层级的代码片段:

1
2
3
4
5
6
7
8
9
Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当Flutter需要渲染这一片段时,它会调用build()方法,该方法返回一棵基于当前应用状态的UI子树。构建过程中,build()方法可以根据状态引入新的组件。例如,在上述代码片段中,Container具有colorchild 属性。查看Container 的源码,可以发现当Color不为空时,它会插入一个代表此颜色的ColoredBox

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

类似地,ImageText组件在构建过程中可能会插入子组件,例如RawImageRichText。因此真实的组件层级可能比代码表面上看起来更深,如下所示[2]

小部件层级树示意图

这解释了为什么通过调试工具(如Flutter inspector)检查组件树时,你可能看到的结构要比你代码中写出的深得多。

在构建阶段,Flutter将代码中表达的组件翻译为对应的Element 树,每个组件都有一个对应的Element实例。每个Element代表组件树某个位置上某个组件的特定实例。Element主要有两种类型:

Widget和Element关系示意图

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是基础的渲染单位,例如:

Widget、Element 和 RenderObject 的区别

大多数Flutter组件由继承自RenderBox的对象进行渲染,RenderBox是二维笛卡尔空间中固定大小的渲染对象。RenderBox提供了一个盒约束模型,规定了每个组件在渲染时的最小和最大宽高。

在布局时,Flutter会以深度优先的顺序遍历渲染树,并从父节点向子节点向下传递大小约束。子节点在确定其大小时,必须遵守父节点提供的约束。子节点随后会通过向父节点 向上传递大小 进行响应,并且约束在子节点中得到了应用。

约束向下传递,大小向上传递

在单次遍历树之后,每个对象已根据父节点的约束条件定义了自己的大小,并准备通过调用paint()方法进行绘制。

盒约束模型是一种强大的组件布局机制,其效率可达O(n)

即使在某些情况下子对象需要知道可用空间大小以决定内容的呈现方式,盒约束模型也能够正常工作。通过使用LayoutBuilder组件,子对象可以检查传递下来的约束并依据约束决定其呈现方式,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

更多关于约束与布局系统的信息和示例,请参考理解约束

所有RenderObject的根节点是RenderView,它代表整个渲染树的输出。当平台要求新帧被渲染时(例如由于垂直同步(vsync)或纹理解压缩/上传完成),会调用渲染树根节点RenderView对象的compositeFrame()方法。这会创建一个SceneBuilder以触发场景更新。当场景完成后,RenderView对象会将合成的场景传递给dart:ui中的Window.render()方法,并将控制权交给GPU以完成渲染。

关于渲染管道中合成和光栅化阶段的详细内容超出了本高层次概述的范围,更多信息可参见Flutter渲染管线的视频演讲

标签:
Categories: