本文档介绍了 Flutter 工具包的内部工作原理,这些原理使得 Flutter 的 API 成为可能。由于 Flutter 小部件是使用激进组合(aggressive composition)构建的,因此使用 Flutter 构建的用户界面包含大量小部件。为了支持这种工作负载,Flutter 使用次线性算法进行布局和小部件构建,以及使树操作高效且具有多项常数因子优化的数据结构。通过一些额外的细节,这种设计还使得开发者能够轻松创建无限滚动列表,方法是使用回调函数只构建用户可见的小部件。

激进组合性

#

Flutter 最显著的特点之一是其激进组合性。小部件是通过组合其他小部件构建的,而这些小部件本身又由更基本的小部件逐步构建而成。例如,Padding 是一个小部件,而不是其他小部件的属性。因此,使用 Flutter 构建的用户界面由许多小部件组成。

小部件构建的递归最终归结为 RenderObjectWidgets,这些小部件在底层渲染树中创建节点。渲染树是一种数据结构,用于存储用户界面的几何信息,这些信息在布局期间计算,并在绘制命中测试期间使用。大多数 Flutter 开发者不直接编写渲染对象,而是使用小部件来操作渲染树。

为了在小部件层支持激进组合性,Flutter 在小部件层和渲染树层都使用了一些高效的算法和优化,这些将在以下小节中描述。

次线性布局

#

在存在大量小部件和渲染对象的情况下,良好性能的关键在于高效算法。其中最重要的是布局的性能,布局是确定渲染对象几何信息(例如,大小和位置)的算法。其他一些工具包使用 O(N²) 或更差的布局算法(例如,某些约束域中的定点迭代)。Flutter 旨在实现初始布局的线性性能,以及在后续更新现有布局的常见情况下的次线性布局性能。通常,布局所花费的时间应比渲染对象的数量增长得慢。

Flutter 每帧执行一次布局,布局算法以单次遍历完成。约束由父对象通过调用其每个子对象的布局方法向下传递。子对象递归地执行自己的布局,然后通过从其布局方法返回来向上返回几何信息。重要的是,一旦渲染对象从其布局方法返回,该渲染对象将不会再次被访问[1],直到下一帧的布局。这种方法将原本可能分开的测量和布局过程合并为单次遍历,因此,每个渲染对象在布局期间最多被访问两次[2]:一次在树的向下遍历中,一次在树的向上遍历中。

Flutter 对这种通用协议有几种特殊实现。最常见的特殊实现是 RenderBox,它在二维笛卡尔坐标系中操作。在盒布局中,约束是最小和最大宽度以及最小和最大高度。在布局期间,子对象通过选择这些边界内的尺寸来确定其几何信息。子对象从布局返回后,父对象决定子对象在父坐标系中的位置[3]。请注意,子对象的布局不能依赖于其位置,因为位置只有在子对象从布局返回后才确定。因此,父对象可以自由地重新定位子对象,而无需重新计算其布局。

更一般地说,在布局期间,从父级流向子级的唯一信息是约束,从子级流向父级的唯一信息是几何信息。这些不变性可以减少布局期间所需的工作量:

  • 如果子对象未将其自身布局标记为脏,则只要父对象给予子对象与子对象在上次布局期间接收到的相同约束,子对象就可以立即从布局返回,从而中断遍历。

  • 每当父对象调用子对象的布局方法时,父对象会指示它是否使用从子对象返回的尺寸信息。如果(通常情况下)父对象不使用尺寸信息,那么即使子对象选择了一个新尺寸,父对象也无需重新计算其布局,因为父对象保证新尺寸将符合现有约束。

  • 紧密约束是指只能由一个有效几何形状满足的约束。例如,如果最小和最大宽度相等,并且最小和最大高度相等,那么唯一满足这些约束的尺寸就是具有该宽度和高度的尺寸。如果父对象提供紧密约束,那么无论子对象何时重新计算其布局,父对象都无需重新计算其布局,即使父对象在其布局中使用了子对象的尺寸,因为子对象若没有来自父对象的新约束就无法更改尺寸。

  • 渲染对象可以声明它只使用父对象提供的约束来确定其几何信息。这样的声明会通知框架,当子对象重新计算其布局时,该渲染对象的父对象无需重新计算其布局,即使约束不紧密即使父对象的布局依赖于子对象的尺寸,因为子对象若没有来自父对象的新约束就无法更改尺寸。

由于这些优化,当渲染对象树包含脏节点时,在布局期间只访问这些节点及其周围有限的子树部分。

次线性小部件构建

#

与布局算法类似,Flutter 的小部件构建算法是次线性的。构建后,小部件由元素树持有,元素树保留了用户界面的逻辑结构。元素树是必需的,因为小部件本身是不可变的,这意味着(除其他外)它们无法记住与其他小部件的父子关系。元素树还持有与有状态小部件关联的状态对象。

响应用户输入(或其他刺激),一个元素可能会变得“脏”,例如,如果开发者在其关联的状态对象上调用 setState()。框架会维护一个脏元素列表,并在构建阶段直接跳到它们,跳过干净元素。在构建阶段,信息会单向流向元素树的下方,这意味着每个元素在构建阶段最多只被访问一次。一旦被清除,元素就不能再次变脏,因为根据归纳法,其所有祖先元素也都是干净的[4]

由于小部件是不可变的,如果一个元素没有将自身标记为脏,并且其父元素用一个相同的小部件重建了它,那么该元素可以立即从构建中返回,从而中断遍历。此外,元素只需要比较两个小部件引用的对象标识,就能确定新小部件与旧小部件相同。开发者利用此优化来实现重投影模式,在这种模式下,小部件在其构建中包含一个作为成员变量存储的预构建子小部件。

在构建期间,Flutter 还避免使用 InheritedWidgets 遍历父链。如果小部件通常遍历其父链(例如,为了确定当前主题颜色),那么构建阶段的时间复杂度将变为 O(N²),其中 N 是树的深度,这会因为激进组合而变得相当大。为了避免这些父级遍历,框架通过在每个元素处维护一个 InheritedWidget 的哈希表来将信息推入元素树。通常,许多元素会引用相同的哈希表,该哈希表只在引入新的 InheritedWidget 的元素处发生变化。

线性协调

#

与普遍看法相反,Flutter 不采用树差异算法。相反,框架通过使用 O(N) 算法独立检查每个元素的子列表来决定是否复用元素。子列表协调算法针对以下情况进行优化:

  • 旧的子列表为空。
  • 两个列表相同。
  • 列表中精确地在一个位置插入或删除了一个或多个小部件。
  • 如果每个列表包含一个具有相同键(key)[5]的小部件,则这两个小部件匹配。

通用方法是通过比较每个小部件的运行时类型和键来匹配两个子列表的开头和结尾,可能会在每个列表的中间找到一个非空范围,其中包含所有不匹配的子对象。然后,框架将旧子列表中该范围内的子对象根据其键放入哈希表。接下来,框架遍历新子列表中的该范围,并按键查询哈希表以查找匹配项。不匹配的子对象被丢弃并从头开始重建,而匹配的子对象则使用其新小部件进行重建。

树操作

#

复用元素对性能至关重要,因为元素拥有两项关键数据:有状态小部件的状态和底层的渲染对象。当框架能够复用一个元素时,用户界面的该逻辑部分的状态得以保留,并且之前计算的布局信息可以复用,通常可以避免整个子树的遍历。事实上,复用元素非常有价值,以至于 Flutter 支持保留状态和布局信息的非局部树变动。

开发者可以通过将 GlobalKey 与其小部件之一关联来执行非局部树变动。每个全局键在整个应用程序中都是唯一的,并注册到一个线程特定的哈希表。在构建阶段,开发者可以将带有全局键的小部件移动到元素树中的任意位置。框架不会在该位置构建一个新的元素,而是会检查哈希表,并将现有元素从其先前位置重新父化到新位置,从而保留整个子树。

重新父化子树中的渲染对象能够保留其布局信息,因为布局约束是渲染树中从父级流向子级的唯一信息。新的父级因其子列表已更改而被标记为布局脏,但如果新的父级将子级从其旧父级接收到的相同布局约束传递给子级,子级可以立即从布局返回,从而中断遍历。

开发者广泛使用全局键和非局部树变动来实现诸如 hero 动画过渡和导航等效果。

常数因子优化

#

除了这些算法优化之外,实现激进组合性还依赖于几项重要的常数因子优化。这些优化在上述主要算法的叶子节点处最为重要。

  • 子模型无关。与大多数使用子列表的工具包不同,Flutter 的渲染树不限于特定的子模型。例如,RenderBox 类有一个抽象的 visitChildren() 方法,而不是具体的 firstChildnextSibling 接口。许多子类只支持单个子对象,直接作为成员变量持有,而不是子对象列表。例如,RenderPadding 只支持单个子对象,因此其布局方法更简单,执行时间更短。

  • 可视渲染树,逻辑小部件树。在 Flutter 中,渲染树在与设备无关的可视坐标系中操作,这意味着 x 坐标中较小的值总是偏向左侧,即使当前阅读方向是自右向左。小部件树通常在逻辑坐标中操作,这意味着其起始结束值的视觉解释取决于阅读方向。从逻辑坐标到可视坐标的转换是在小部件树和渲染树之间的交接中完成的。这种方法更高效,因为渲染树中的布局和绘制计算比小部件到渲染树的交接更频繁,并且可以避免重复的坐标转换。

  • 文本由专门的渲染对象处理。绝大多数渲染对象都不知道文本的复杂性。相反,文本由一个专门的渲染对象 RenderParagraph 处理,它是渲染树中的一个叶子节点。开发者不是通过继承一个文本感知的渲染对象,而是通过组合的方式将文本整合到他们的用户界面中。这种模式意味着 RenderParagraph 可以避免重新计算其文本布局,只要其父级提供相同的布局约束,这很常见,即使在树操作期间也是如此。

  • 可观察对象。Flutter 同时使用了模型-观察和响应式范式。显然,响应式范式占主导地位,但 Flutter 对一些叶子数据结构使用了可观察模型对象。例如,Animation 在其值发生变化时会通知观察者列表。Flutter 将这些可观察对象从小部件树传递到渲染树,渲染树直接观察它们,并且在它们变化时仅使管道的适当阶段失效。例如,对 Animation<Color> 的更改可能只会触发绘制阶段,而不是构建和绘制两个阶段。

综上所述,并结合激进组合所创建的大型树结构,这些优化对性能产生了显著影响。

元素树和渲染对象树的分离

#

Flutter 中的 RenderObjectElement (Widget) 树是同构的(严格来说,RenderObject 树是 Element 树的子集)。一个明显的简化方法是将这些树合并为一棵树。然而,实际上,将这些树分开有许多好处:

  • 性能。当布局发生变化时,只需要遍历布局树的相关部分。由于组合,元素树通常会有许多额外节点,如果合并,则必须跳过这些节点。

  • 清晰度。更清晰的职责分离允许小部件协议和渲染对象协议各自针对其特定需求进行专门化,从而简化了 API 表面,降低了出现错误和测试的负担。

  • 类型安全。渲染对象树可以更具类型安全,因为它可以在运行时保证子对象是适当的类型(例如,每个坐标系都有其自己的渲染对象类型)。组合小部件可以不考虑布局期间使用的坐标系(例如,暴露应用程序模型一部分的相同小部件可以用于盒布局和 sliver 布局),因此在元素树中,验证渲染对象的类型将需要树遍历。

无限滚动

#

无限滚动列表对于工具包来说是出了名的困难。Flutter 通过一个基于构建器模式的简单接口支持无限滚动列表,其中 ListView 使用回调函数在小部件在用户滚动过程中变得可见时按需构建它们。支持此功能需要视口感知布局按需构建小部件

视口感知布局

#

与 Flutter 中的大多数事物一样,可滚动小部件是使用组合构建的。可滚动小部件的外部是一个 Viewport(视口),它是一个“内部更大”的盒子,这意味着它的子对象可以超出视口的边界并可以滚动到视图中。然而,视口不是拥有 RenderBox 子对象,而是拥有 RenderSliver 子对象,这些子对象被称为 slivers,它们具有视口感知的布局协议。

sliver 布局协议的结构与盒布局协议相似,即父对象将约束传递给其子对象,并接收几何信息作为回报。然而,这两种协议之间的约束和几何数据有所不同。在 sliver 协议中,子对象会获得有关视口的信息,包括剩余的可见空间量。它们返回的几何数据支持各种与滚动相关的效果,包括可折叠标题和视差滚动。

不同的 sliver 以不同的方式填充视口中的可用空间。例如,一个生成线性子列表的 sliver 会按顺序布局每个子对象,直到 sliver 用尽子对象或用尽空间。类似地,一个生成二维子网格的 sliver 只填充其可见的网格部分。由于它们知道有多少空间是可见的,因此即使 sliver 有潜力生成无限数量的子对象,它们也可以生成有限数量的子对象。

Sliver 可以组合起来创建定制的可滚动布局和效果。例如,一个单一视口可以包含一个可折叠标题,后面跟着一个线性列表,然后是一个网格。所有这三个 sliver 将通过 sliver 布局协议协同工作,只生成那些通过视口实际可见的子对象,无论这些子对象属于标题、列表还是网格[6]

按需构建小部件

#

如果 Flutter 有一个严格的先构建-再布局-再绘制的流水线,那么上述方法将不足以实现无限滚动列表,因为关于视口可见空间量的信息只有在布局阶段才可用。如果没有额外的机制,布局阶段构建填充空间所需的小部件就太晚了。Flutter 通过交错流水线的构建和布局阶段来解决这个问题。在布局阶段的任何时候,框架都可以按需开始构建新的小部件,只要这些小部件是当前正在执行布局的渲染对象的后代

构建和布局的交错之所以可能,仅仅是因为构建和布局算法对信息传播有严格的控制。具体来说,在构建阶段,信息只能沿着树向下传播。当渲染对象执行布局时,布局遍历尚未访问该渲染对象下方的子树,这意味着在该子树中构建所产生的写入不能使到目前为止已进入布局计算的任何信息失效。同样,一旦布局从某个渲染对象返回,该渲染对象在本次布局期间将不再被访问,这意味着后续布局计算所产生的任何写入不能使用于构建该渲染对象子树的信息失效。

此外,线性协调和树操作对于在滚动期间高效更新元素以及在元素在视口边缘滚动进出视图时修改渲染树至关重要。

API 人机工程学

#

只有当框架能够实际有效地使用时,快速才有意义。为了指导 Flutter 的 API 设计以提高可用性,Flutter 在与开发者的广泛用户体验研究中反复进行了测试。这些研究有时证实了预先存在的设计决策,有时有助于指导功能优先级,有时则改变了 API 设计的方向。例如,Flutter 的 API 有大量文档;用户体验研究证实了此类文档的价值,但也特别强调了对示例代码和说明图的需求。

本节讨论了 Flutter API 设计中为提高可用性而做出的一些决策。

根据开发者思维模式定制 API

#

Flutter 的 WidgetElementRenderObject 树中节点的基类不定义子模型。这使得每个节点都可以针对适用于该节点的子模型进行专门化。

大多数 Widget 对象都有一个单独的子 Widget,因此只暴露一个 child 参数。有些小部件支持任意数量的子对象,并暴露一个接受列表的 children 参数。有些小部件根本没有子对象,不占用任何内存,也没有与子对象相关的参数。类似地,RenderObjects 暴露特定于其子模型的 API。RenderImage 是一个叶子节点,没有子对象的概念。RenderPadding 接受单个子对象,因此它有一个指向单个子对象的指针的存储空间。RenderFlex 接受任意数量的子对象并将其作为链表管理。

在某些罕见情况下,会使用更复杂的子模型。RenderTable 渲染对象的构造函数接受一个子对象数组的数组,该类暴露了控制行数和列数的 getter 和 setter,并且有特定的方法可以通过 x,y 坐标替换单个子对象、添加一行、提供一个新的子对象数组的数组,以及用单个数组和列计数替换整个子列表。在实现中,该对象不像大多数渲染对象那样使用链表,而是使用可索引数组。

Chip 小部件和 InputDecoration 对象具有与相关控件上存在的槽位匹配的字段。当“一刀切”的子模型会强制将语义叠加在子对象列表之上时(例如,将第一个子对象定义为前缀值,第二个定义为后缀),专用的子模型则允许使用专用的命名属性来代替。

这种灵活性使得这些树中的每个节点都可以以最符合其角色的方式进行操作。很少会有人想在表格中插入一个单元格,导致所有其他单元格换行;同样,也很少会有人想通过索引而不是引用来从 flex 行中移除子对象。

RenderParagraph 对象是最极端的例子:它有一个完全不同类型的子对象 TextSpan。在 RenderParagraph 边界处,RenderObject 树转换为 TextSpan 树。

根据开发者期望定制 API 的整体方法不仅仅适用于子模型。

一些相当简单的小部件的存在,专门是为了让开发者在寻找问题解决方案时能够找到它们。一旦知道如何使用 Expanded 小部件和零尺寸的 SizedBox 子部件,向行或列添加空间就很容易了,但无需发现该模式,因为搜索 space 就会发现 Spacer 小部件,它直接使用 ExpandedSizedBox 来实现效果。

类似地,隐藏小部件子树可以通过根本不将小部件子树包含在构建中来轻松完成。然而,开发者通常期望有一个小部件来做这件事,因此 Visibility 小部件的存在就是为了将这种模式封装在一个简单的可复用小部件中。

显式参数

#

UI 框架往往有许多属性,以至于开发者很少能记住每个类的每个构造函数参数的语义含义。由于 Flutter 使用响应式范式,Flutter 中的构建方法通常会包含许多构造函数调用。通过利用 Dart 对命名参数的支持,Flutter 的 API 能够使此类构建方法清晰易懂。

这种模式扩展到任何具有多个参数的方法,特别是扩展到任何布尔参数,因此方法调用中孤立的 truefalse 字面量总是自解释的。此外,为了避免 API 中双重否定常引起的混淆,布尔参数和属性总是以肯定形式命名(例如,enabled: true 而不是 disabled: false)。

避免陷阱

#

Flutter 框架在许多地方使用的一种技术是定义 API,使错误条件不复存在。这消除了整类错误。

例如,插值函数允许插值的任意一端或两端为 null,而不是将其定义为错误情况:在两个 null 值之间插值始终为 null,从 null 值或到 null 值插值等同于插值到给定类型的零值。这意味着不小心向插值函数传递 null 的开发者不会遇到错误情况,而是会得到合理的结果。

一个更微妙的例子是 Flex 布局算法。这种布局的概念是将分配给 flex 渲染对象的空间在其子对象之间分配,因此 flex 的大小应该是所有可用空间的全部。在最初的设计中,提供无限空间会导致失败:这意味着 flex 应该具有无限大小,这是一个无用的布局配置。相反,API 进行了调整,以便当无限空间分配给 flex 渲染对象时,渲染对象会根据子对象的期望尺寸调整自身大小,从而减少了可能的错误情况数量。

这种方法也用于避免构造函数允许创建不一致的数据。例如,PointerDownEvent 构造函数不允许将 PointerEventdown 属性设置为 false(这种情况会自相矛盾);相反,构造函数没有 down 字段的参数,并且总是将其设置为 true

总的来说,这种方法是为输入域中的所有值定义有效的解释。最简单的例子是 Color 构造函数。它不是接受四个整数(分别代表红色、绿色、蓝色和 alpha 值),每个都可能超出范围,而是接受一个单一整数值,并定义每个位的含义(例如,最低八位定义红色分量),从而使任何输入值都是一个有效的颜色值。

一个更复杂的例子是 paintImage() 函数。此函数接受十一个参数,其中一些参数的输入域相当广泛,但它们经过精心设计,大部分彼此正交,因此无效组合非常少。

积极报告错误

#

并非所有错误条件都可以通过设计消除。对于那些仍然存在的错误,在调试构建中,Flutter 通常会尝试尽早捕获并立即报告它们。断言被广泛使用。构造函数参数经过详细的健全性检查。生命周期受到监控,当检测到不一致时,会立即抛出异常。

在某些情况下,这甚至被推向极端:例如,在运行单元测试时,无论测试正在做什么,每个被积极布局的 RenderBox 子类都会检查其固有尺寸方法是否满足固有尺寸契约。这有助于捕获那些在其他情况下可能不会被使用的 API 中的错误。

当抛出异常时,它们会包含尽可能多的可用信息。Flutter 的一些错误消息会主动探测相关的堆栈跟踪,以确定实际错误的极可能位置。另一些则遍历相关树结构以确定错误数据的来源。最常见的错误包括详细的说明,有时还包括避免错误的示例代码或指向更多文档的链接。

响应式范式

#

基于可变树的 API 存在一种二分访问模式:创建树的原始状态通常使用的操作集与后续更新非常不同。Flutter 的渲染层使用这种范式,因为它是维护持久树的有效方式,而持久树是高效布局和绘制的关键。然而,这意味着直接与渲染层交互最好是笨拙的,最坏则是容易出错的。

Flutter 的小部件层引入了一种使用响应式范式[7]的组合机制来操作底层渲染树。该 API 通过将树创建和树变动步骤合并到单个树描述(构建)步骤中,从而抽象出树操作。在该步骤中,系统状态每次更改后,开发者都会描述用户界面的新配置,框架则计算反映此新配置所需的一系列树变动。

插值

#

由于 Flutter 框架鼓励开发者描述与当前应用程序状态相匹配的界面配置,因此存在一种机制可以在这些配置之间进行隐式动画。

例如,假设在状态 S1 中,界面由一个圆形组成,但在状态 S2 中,它由一个正方形组成。如果没有动画机制,状态变化将导致界面突然改变。隐式动画允许圆形在多个帧内平滑地变为正方形。

每个可以隐式动画化的功能都带有一个有状态小部件,该小部件记录输入的当前值,并在输入值发生变化时启动动画序列,在指定持续时间内从当前值过渡到新值。

这是使用基于不可变对象的 lerp(线性插值)函数实现的。每个状态(在此例中是圆形和正方形)都被表示为一个不可变对象,该对象配置有适当的设置(颜色、描边宽度等),并知道如何绘制自身。当需要绘制动画中间步骤时,起始和结束值以及表示动画进度的 t 值(其中 0.0 表示 start,1.0 表示 end[8])被传递给适当的 lerp 函数,然后该函数返回第三个表示中间阶段的不可变对象。

对于圆形到正方形的过渡,lerp 函数将返回一个表示“圆角正方形”的对象,其半径是根据 t 值计算出的分数,颜色使用颜色的 lerp 函数插值,描边宽度使用双精度浮点数的 lerp 函数插值。该对象实现了与圆形和正方形相同的接口,因此在需要时能够绘制自身。

这种技术使得状态机制、状态到配置的映射、动画机制、插值机制以及与如何绘制每一帧相关的特定逻辑可以完全彼此分离。

这种方法适用范围广泛。在 Flutter 中,像 ColorShape 这样的基本类型可以进行插值,更复杂的类型如 DecorationTextStyleTheme 也可以。这些类型通常由自身可插值的组件构成,而对更复杂对象进行插值通常就像递归地插值描述这些复杂对象的所有值一样简单。

一些可插值对象由类层次结构定义。例如,形状由 ShapeBorder 接口表示,并且存在各种形状,包括 BeveledRectangleBorder(斜角矩形边界)、BoxBorder(盒边界)、CircleBorder(圆形边界)、RoundedRectangleBorder(圆角矩形边界)和 StadiumBorder(体育场边界)。单个 lerp 函数无法预测所有可能的类型,因此接口定义了 lerpFromlerpTo 方法,静态 lerp 方法会委托给它们。当被要求从形状 A 插值到形状 B 时,首先会询问 B 是否可以 lerpFrom A;如果不能,则转而询问 A 是否可以 lerpTo B。(如果两者都不可能,则当 t 值小于 0.5 时函数返回 A,否则返回 B。)

这允许类层次结构被任意扩展,后来的添加能够在新旧值之间进行插值。

在某些情况下,插值本身无法由任何可用类描述,这时会定义一个私有类来描述中间阶段。例如,在 CircleBorderRoundedRectangleBorder 之间插值时就是这种情况。

这种机制还有一个额外的优点:它可以处理从中间阶段到新值的插值。例如,在圆形到正方形过渡进行到一半时,形状可以再次改变,导致动画需要插值到一个三角形。只要三角形类能够从圆角正方形中间类中 lerpFrom,过渡就可以无缝进行。

总结

#

Flutter 的口号“一切皆为小部件”围绕着通过组合小部件来构建用户界面,而这些小部件又由越来越基本的小部件组成。这种激进组合的结果是产生了大量小部件,需要精心设计的算法和数据结构才能高效处理。通过一些额外的设计,这些数据结构还使得开发者能够轻松创建无限滚动列表,这些列表会在小部件可见时按需构建它们。


脚注


  1. 至少对于布局是如此。如果需要,它可能会在绘制、构建可访问性树以及命中测试时再次被访问。 ↩︎

  2. 当然,现实情况要复杂一些。某些布局涉及固有尺寸或基线测量,这确实需要额外遍历相关子树(使用激进缓存来缓解最坏情况下二次性能的可能性)。然而,这些情况出乎意料地罕见。特别是,在常见的收缩包裹(shrink-wrapping)情况下不需要固有尺寸。 ↩︎

  3. 从技术上讲,子对象的位置不属于其 RenderBox 几何信息的一部分,因此在布局期间实际上无需计算。许多渲染对象会隐式地将其单个子对象放置在相对于其自身原点的 0,0 位置,这完全不需要任何计算或存储。一些渲染对象会避免计算其子对象的位置,直到最后可能的时刻(例如,在绘制阶段),以便在它们随后不被绘制的情况下完全避免计算。 ↩︎

  4. 此规则存在一个例外。如 按需构建小部件 一节所述,一些小部件可能会因布局约束的变化而重建。如果一个小部件因无关原因在同一帧内将自身标记为“脏”,并且同时受到布局约束变化的影响,它将被更新两次。这种冗余构建仅限于小部件本身,不影响其后代。 ↩︎

  5. 键是一个不透明对象,可选地与一个小部件关联,其相等运算符用于影响协调算法。 ↩︎

  6. 为了可访问性,并给予应用程序在小部件构建和在屏幕上显示之间额外的几毫秒时间,视口会在可见小部件之前和之后创建(但不绘制)几百像素的小部件。 ↩︎

  7. 这种方法最初由 Facebook 的 React 库推广开来。 ↩︎

  8. 实际上,t 值允许超出 0.0-1.0 的范围,某些曲线确实如此。例如,“弹性”曲线会短暂地过冲,以表示弹跳效果。插值逻辑通常可以根据需要外推超出起点或终点。对于某些类型,例如在颜色插值时,t 值实际上被钳制在 0.0-1.0 的范围内。 ↩︎