跳到主内容

Flutter 内部原理

从 Flutter 的创始工程师之一那里了解 Flutter 的内部工作原理。

本文档描述了 Flutter 工具包的内部工作原理,这些原理使得 Flutter 的 API 成为可能。由于 Flutter 小部件是使用积极的组合构建的,因此使用 Flutter 构建的用户界面包含大量的小部件。为了支持这种工作负载,Flutter 使用亚线性算法进行布局和小部件构建,以及使树外科手术高效且具有许多常数因子优化的数据结构。通过一些额外的细节,这种设计也使得开发者能够使用回调来创建无限滚动列表,这些回调构建用户可见的那些小部件。

积极的可组合性

#

Flutter 最具特色的方面之一是其积极的可组合性。小部件是通过组合其他小部件来构建的,而这些小部件本身又是通过逐步更基本的小部件构建的。例如,Padding 是一个小部件,而不是其他小部件的属性。因此,使用 Flutter 构建的用户界面包含许多、许多小部件。

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

为了支持小部件层面的积极可组合性,Flutter 在小部件和渲染树层面使用许多高效的算法和优化,这些算法和优化在以下子部分中描述。

亚线性布局

#

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

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

Flutter 有此通用协议的几个专业化。最常见的专业化是 RenderBox,它在二维笛卡尔坐标系中运行。在盒子布局中,约束是最小和最大宽度以及最小和最大高度。在布局期间,子对象通过在这些边界内选择大小来确定其几何形状。子对象从布局返回后,父对象决定子对象在其坐标系中的位置3。请注意,子对象的布局不能依赖于其位置,因为位置是在子对象从布局返回后才确定的。因此,父对象可以在不需要重新计算其布局的情况下重新定位子对象。

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

  • 如果子对象没有将自己的布局标记为脏,只要父对象向子对象提供与子对象在上次布局期间收到的相同的约束,子对象就可以立即从布局返回,从而切断遍历。

  • 每当父对象调用子对象的布局方法时,父对象都会指示它是否使用从子对象返回的大小信息。如果父对象通常不使用大小信息(这很常见),那么如果子对象选择新的大小,父对象就不需要重新计算其布局,因为父对象保证新的大小将符合现有的约束。

  • 严格的约束是可以由唯一有效的几何形状满足的约束。例如,如果最小和最大宽度彼此相等,并且最小和最大高度彼此相等,那么满足这些约束的唯一大小就是具有该宽度和高度的大小。如果父对象提供严格的约束,那么即使父对象的布局依赖于子对象的大小,每当子对象重新计算其布局时,父对象也不需要重新计算其布局,因为子对象不能在没有来自其父对象的新的约束的情况下更改大小。

  • 渲染对象可以声明它仅使用来自父对象的约束来确定其几何形状。此声明告知框架,即使约束不严格并且父对象的布局依赖于子对象的大小,当子对象重新计算其布局时,该渲染对象的父对象也不需要重新计算其布局,因为子对象不能在没有来自其父对象的新的约束的情况下更改大小。

由于这些优化,当渲染对象树包含脏节点时,只有这些节点和围绕它们的子树的有限部分会在布局期间被访问。

亚线性小部件构建

#

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

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

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

在构建期间,Flutter 还使用 InheritedWidgets 避免遍历父链。如果小部件经常遍历其父链,例如确定当前的主题颜色,那么构建阶段将变为树深度的 O(N²),由于积极的可组合性,这可能非常大。为了避免这些父链遍历,框架通过在每个元素处维护 InheritedWidget 的哈希表来向下推送信息。通常,许多元素将引用相同的哈希表,该哈希表仅在引入新的 InheritedWidget 的元素处发生变化。

线性协调

#

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

  • 旧的子列表为空。
  • 两个列表相同。
  • 列表中有一个或多个小部件在列表中的精确位置被插入或删除。
  • 如果每个列表都包含具有相同 key5 的小部件,则将匹配这两个小部件。

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

树外科手术

#

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

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

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

全局键和非本地树突变被开发者广泛用于实现诸如英雄过渡和导航之类的效果。

常数因子优化

#

除了这些算法优化之外,实现积极的可组合性还依赖于几个重要的常数因子优化。这些优化对于上述主要算法讨论的叶子节点来说最为重要。

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

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

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

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

综上所述,并且加总于积极组合创建的大型树,这些优化对性能具有实质性的影响。

元素树和 RenderObject 树的分离

#

Flutter 中的 RenderObjectElement (Widget) 树是同构的(严格来说,RenderObject 树是 Element 树的子集)。一个显而易见的简化是将这些树合并成一棵树。然而,在实践中,将这些树分开有很多好处

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

  • 清晰度。 更清晰的关注点分离允许 widget 协议和渲染对象协议各自专门针对其特定需求,从而简化 API 表面,降低错误风险和测试负担。

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

无限滚动

#

无限滚动列表对于工具包来说是出了名的难题。Flutter 使用基于builder 模式的简单接口支持无限滚动列表,其中 ListView 使用回调函数按需构建 widget,因为它们在滚动期间对用户可见。支持此功能需要视口感知布局按需构建 widget

视口感知布局

#

与 Flutter 中的大多数事物一样,可滚动 widget 是使用组合构建的。可滚动 widget 的外部是一个 Viewport,它是一个“内部更大”的框,这意味着它的子对象可以超出视口的边界,并且可以滚动到视图中。但是,与其拥有 RenderBox 子对象,视口具有 RenderSliver 子对象,称为切片,它们具有视口感知布局协议。

切片布局协议与框布局协议的结构相匹配,即父对象将约束传递给其子对象,并接收几何体作为回报。但是,两种协议之间的约束和几何体数据不同。在切片协议中,子对象会获得有关视口的信息,包括剩余的可见空间量。它们返回的几何体数据可以实现各种与滚动相关的效果,包括可折叠的标题和视差效果。

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

切片可以组合起来创建定制的可滚动布局和效果。例如,单个视口可以有一个可折叠的标题,然后是一个线性列表,然后是一个网格。所有三个切片将通过切片布局协议进行协作,仅生成实际通过视口可见的子对象,无论这些子对象属于标题、列表还是网格6

按需构建小部件

#

如果 Flutter 具有严格的构建-然后-布局-然后-绘制 管道,那么上述内容不足以实现无限滚动列表,因为有关通过视口可见空间的量的信息仅在布局阶段可用。如果没有额外的机制,布局阶段太晚了,无法构建必要的填充空间的 widget。Flutter 通过交错管道的构建和布局阶段来解决此问题。在布局阶段的任何时候,框架都可以开始按需构建新的 widget,只要这些 widget 是当前执行布局的渲染对象的后代

构建和布局的交错是可能的,这仅仅是因为构建和布局算法中信息传播的严格控制。具体来说,在构建阶段,信息只能向下树传播。当渲染对象执行布局时,布局遍历尚未访问该渲染对象下方的子树,这意味着在子树中生成的写入不会使迄今为止进入布局计算的信息失效。同样,一旦布局从渲染对象返回,该渲染对象将永远不会在此布局期间再次被访问,这意味着后续布局计算生成的写入不会使用于构建渲染对象子树的信息失效。

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

API 人体工程学

#

仅仅速度快,如果框架实际上可以有效地使用,那才重要。为了指导 Flutter 的 API 设计朝着更大的可用性发展,Flutter 一直在与开发人员进行广泛的 UX 研究中反复测试。这些研究有时会确认预先存在的决策,有时会帮助确定功能的优先级,有时会改变 API 设计的方向。例如,Flutter 的 API 有大量文档;UX 研究证实了这些文档的价值,但也强调了对示例代码和说明性图表的特定需求。

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

专门的 API 以匹配开发者的思维模式

#

Flutter 的 WidgetElementRenderObject 树中的节点的基类不定义子模型。这允许每个节点专门针对适用于该节点的子模型。

大多数 Widget 对象都有一个子 Widget,因此仅公开一个 child 参数。一些 widget 支持任意数量的子对象,并公开一个接受列表的 children 参数。一些 widget 根本没有子对象,并且不保留任何内存,并且没有用于它们的参数。类似地,RenderObjects 公开特定于其子模型的 API。RenderImage 是一个叶子节点,并且没有子对象的概念。RenderPadding 接受一个子对象,因此它有一个指向单个子对象的指针的存储空间。RenderFlex 接受任意数量的子对象并将其作为链接列表进行管理。

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

Chip widget 和 InputDecoration 对象具有与相关控件上存在的插槽相匹配的字段。在需要将语义叠加在子对象列表之上的通用子模型会强制使用语义(例如,将第一个子对象定义为前缀值,将第二个子对象定义为后缀值),专用子模型允许使用专用的命名属性。

这种灵活性允许这些树中的每个节点以最符合其角色的方式进行操作。很少需要插入表格中的一个单元格,导致所有其他单元格环绕;同样,很少需要通过索引而不是通过引用从 flex 行中删除一个子对象。

RenderParagraph 对象是最极端的情况:它有一个完全不同类型的子对象,即 TextSpan。在 RenderParagraph 边界处,RenderObject 树过渡为 TextSpan 树。

将 API 专门化以满足开发人员期望的总体方法不仅适用于子模型,还适用于其他方面。

存在一些相当简单的 widget,其目的仅仅是让开发人员在寻找问题解决方案时能够找到它们。使用 Expanded widget 和零尺寸的 SizedBox 子对象可以轻松地在行或列中添加空格,但发现这种模式是不必要的,因为搜索 space 会发现 Spacer widget,它直接使用 ExpandedSizedBox 来实现该效果。

类似地,隐藏一个 widget 子树可以通过根本不在 build 中包含该 widget 子树来轻松完成。然而,开发人员通常期望有一个 widget 来执行此操作,因此 Visibility widget 存在于此,将这种模式包装在一个简单的可重用 widget 中。

显式参数

#

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

这种模式扩展到具有多个参数的任何方法,特别是扩展到任何布尔参数,以便方法调用中的孤立的 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 的 widget 层引入了一种使用响应式范例7的组合机制,用于操作底层的渲染树。此 API 通过将树创建和树修改步骤合并到一个树描述(build)步骤中来抽象树操作,在该步骤中,在每次系统状态更改后,开发人员描述用户界面的新配置,并且框架计算必要的树修改序列以反映此新配置。

插值

#

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

例如,假设在状态 S1 中,界面由一个圆组成,但在状态 S2 中,它由一个正方形组成。如果没有动画机制,状态更改将导致界面发生剧烈的变化。隐式动画允许圆在几个帧中平滑地变成正方形。

每个可以隐式动画处理的特性都有一个有状态的 widget,它会记录输入当前值,并在输入值发生更改时开始动画序列,从当前值过渡到指定持续时间的新值。

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

对于圆到正方形的过渡,lerp 函数将返回一个表示“圆角矩形”的对象,其半径描述为从 t 值派生的分数,颜色使用 lerp 函数进行插值,笔画宽度使用 lerp 函数进行插值。该对象,它实现了与圆和正方形相同的接口,在请求绘制自身时能够绘制自身。

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

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

一些可插值对象由类层次结构定义。例如,形状由 ShapeBorder 接口表示,并且存在各种形状,包括 BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。单个 lerp 函数无法预测所有可能的类型,因此该接口定义了 lerpFromlerpTo 方法,静态 lerp 方法会委派给这些方法。当被告知从形状 A 插值到形状 B 时,首先询问 B 是否可以从 A lerpFrom,然后,如果不能,则相反地询问 A 是否可以 lerpTo B。(如果两者都不可能,则该函数从 t 值小于 0.5 时返回 A,否则返回 B。)

这允许类层次结构任意扩展,后续添加可以插值到先前已知的值和自身之间。

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

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

结论

#

Flutter 的口号“一切都是 widget”围绕着通过组合 widget 来构建用户界面,而这些 widget 又由逐渐更基本的 widget 组成。这种积极的组合导致了大量的 widget,需要精心设计算法和数据结构才能有效地处理这些 widget。通过一些额外的设计,这些数据结构也使开发人员能够创建在需要时按需构建 widget 的无限滚动列表。


脚注

  1. 至少对于布局而言。如果需要,它可能会重新审视用于绘制、构建可访问性树和进行命中测试的情况。

  2. 当然,现实情况要复杂一些。有些布局涉及固有尺寸或基线测量,这确实需要遍历相关的子树(使用积极的缓存来缓解最坏情况下的二次性能问题)。然而,这些情况出乎意料地少见。特别是,固有尺寸对于常见的自适应尺寸包装的情况并非必需。

  3. 从技术上讲,子组件的位置不属于其 RenderBox 几何体的一部分,因此实际上不需要在布局期间计算。许多渲染对象隐式地将单个子组件定位在其自身的原点处,坐标为 0,0,这不需要任何计算或存储。一些渲染对象会尽可能推迟计算子组件的位置(例如,在绘制阶段),以避免在之后未绘制时进行计算。

  4. 存在一个例外。如 按需构建小部件 部分所述,某些小部件可能会因布局约束的变化而重建。如果某个小部件在同一帧中由于无关原因标记为“脏”,并且又受到布局约束变化的影响,它将被更新两次。这种冗余构建仅限于该小部件本身,不会影响其子组件。

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

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

  7. 这种方法最初是由 Facebook 的 React 库推广的。

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