Flutter 内部原理
本文档描述了 Flutter 工具包的内部工作原理,这些原理使得 Flutter 的 API 成为可能。由于 Flutter Widget 是通过强大的组合方式构建的,因此使用 Flutter 构建的用户界面拥有大量的 Widget。为了支持这种工作负载,Flutter 在布局和 Widget 构建方面使用了亚线性算法,以及能够进行高效树操作并包含大量常数因子优化的数据结构。通过一些附加细节,这种设计也使得开发者能够轻松地使用回调函数创建无限滚动列表,这些回调函数会精确地构建用户可见的 Widget。
强大的可组合性
#Flutter 最显著的方面之一是其强大的可组合性。Widget 是通过组合其他 Widget 来构建的,而这些 Widget 本身又由越来越基础的 Widget 组成。例如,Padding
是一个 Widget,而不是其他 Widget 的属性。因此,使用 Flutter 构建的用户界面由大量的 Widget 组成。
Widget 构建的递归最终会触及 RenderObjectWidgets
,这些 Widget 在底层渲染树中创建节点。渲染树是一种数据结构,它存储用户界面的几何信息,这些信息在布局期间计算,并在绘制和命中测试期间使用。大多数 Flutter 开发者不会直接编写渲染对象,而是通过 Widget 来操作渲染树。
为了在 Widget 层实现强大的可组合性,Flutter 在 Widget 和渲染树层都采用了许多高效的算法和优化,这些内容将在接下来的小节中进行描述。
亚线性布局
#拥有大量 Widget 和渲染对象,性能的关键在于高效的算法。至关重要的是布局算法的性能,布局算法决定了渲染对象的几何形状(例如,大小和位置)。一些其他工具包使用 O(N²) 或更差的布局算法(例如,某些约束域中的不动点迭代)。Flutter 的目标是实现初始布局的线性性能,以及在后续更新现有布局的常见情况下的亚线性布局性能。通常,在布局中花费的时间应该比渲染对象的数量增长得更慢。
Flutter 每帧执行一次布局,并且布局算法采用单次遍历。约束由父对象调用子对象的布局方法向下传递到树中。子对象递归地执行自己的布局,然后通过从它们的布局方法返回来将几何信息返回到树中。重要的是,一旦渲染对象从其布局方法返回,该渲染对象将不会在下一个帧的布局之前再次被访问[1]。这种方法将可能单独进行的测量和布局传递合并为一次遍历,结果是,每个渲染对象在布局期间最多被访问两次[2]:一次在向下遍历树时,一次在向上遍历树时。
Flutter 针对此通用协议有几种专门化。最常见的专门化是 RenderBox
,它在二维笛卡尔坐标系中工作。在盒状布局中,约束是最小和最大宽度以及最小和最大高度。在布局期间,子对象通过选择这些边界内的尺寸来确定其几何形状。在子对象从布局返回后,父对象在父对象的坐标系中决定子对象的位置[3]。请注意,子对象的布局不能依赖于其位置,因为位置在子对象从布局返回后才确定。因此,父对象可以自由地重新定位子对象,而无需重新计算其布局。
更普遍地说,在布局期间,唯一从父对象流向子对象的信息是约束,而唯一从子对象流向父对象的信息是几何信息。这些不变量可以减少布局所需的工作量。
如果子对象没有将自己的布局标记为脏,那么只要父对象向子对象提供与子对象在上一个布局中收到的相同的约束,子对象就可以立即从布局返回,从而截断遍历。
每当父对象调用子对象的布局方法时,父对象会指示它是否使用从子对象返回的尺寸信息。如果,如经常发生的那样,父对象不使用尺寸信息,那么即使子对象选择了一个新的尺寸,父对象也不需要重新计算其布局,因为父对象保证新的尺寸将符合现有约束。
严格约束是只能通过一个有效几何形状来满足的约束。例如,如果最小和最大宽度相等,并且最小和最大高度相等,那么满足这些约束的唯一尺寸是具有该宽度和高度的尺寸。如果父对象提供了严格约束,那么即使父对象在其布局中使用子对象的尺寸,当子对象重新计算其布局时,父对象也不需要重新计算其布局,因为子对象在没有父对象的新约束的情况下无法改变尺寸。
渲染对象可以声明它仅使用父对象提供的约束来确定其几何形状。 such a declaration informs the framework that the parent of that render object does not need to recompute its layout when the child recomputes its layout *even if the constraints are not tight* and *even if the parent's layout depends on the child's size*, because the child cannot change size without new constraints from its parent. (这种声明会告知框架,该渲染对象的父对象在子对象重新计算布局时不需要重新计算其布局,即使约束不严格,并且即使父对象的布局依赖于子对象的尺寸,因为子对象在没有父对象的新约束的情况下无法改变尺寸。)
由于这些优化,当渲染对象树包含脏节点时,布局期间只会访问这些节点以及它们周围有限的子树部分。
亚线性 Widget 构建
#与布局算法类似,Flutter 的 Widget 构建算法也是亚线性的。构建完成后,Widget 由Element 树持有,Element 树保留了用户界面的逻辑结构。Element 树是必需的,因为 Widget 本身是不可变的,这意味着(除其他外)它们无法记住它们与其他 Widget 的父子关系。Element 树还保存了与状态 Widget 关联的状态对象。
作为对用户输入(或其他刺激)的响应,Element 可以变脏,例如,如果开发者在关联的状态对象上调用了 setState()
。框架维护一个脏 Element 列表,并在构建阶段直接跳转到它们,跳过干净的 Element。在构建阶段,信息是单向地向下传播到 Element 树,这意味着每个 Element 在构建阶段最多被访问一次。一旦被清理,Element 就不会再次变脏,因为根据归纳法,它所有的祖先 Element 也是干净的[4]。
由于 Widget 是不可变的,如果 Element 没有将自己标记为脏,并且父对象用一个相同的 Widget 重建了该 Element,那么该 Element 可以立即从构建中返回,从而截断遍历。此外,Element 只需要比较两个 Widget 引用对象的身份就可以确定新 Widget 与旧 Widget 相同。开发者利用这种优化来实现重投影模式,其中 Widget 在其构建中包含一个存储在其成员变量中的预构建的子 Widget。
在构建过程中,Flutter 还通过 InheritedWidgets
避免了遍历父链。如果 Widget 经常遍历其父链,例如为了确定当前主题颜色,那么构建阶段将变成树深度的 O(N²) 复杂度,由于强大的组合,树深度可能非常大。为了避免这些父链遍历,框架通过在每个 Element 维护一个 InheritedWidget
的哈希表来将信息向下推送到 Element 树。通常,许多 Element 会引用同一个哈希表,该哈希表仅在引入新 InheritedWidget
的 Element 处发生变化。
线性协调
#与流行的看法相反,Flutter 并不采用树差异算法。相反,框架通过使用 O(N) 算法独立检查每个 Element 的子列表来决定是否重用 Element。
- 旧的子列表为空。
- 两个列表完全相同。
- 在列表中的一个或多个位置上只发生了一次插入或删除。
- 如果每个列表包含一个具有相同 key 的 Widget[5],则这两个 Widget 会匹配。
通用方法是通过比较每个 Widget 的运行时类型和 key 来匹配两个子列表的开头和结尾,可能在每个列表的中间找到一个非空范围,其中包含所有未匹配的子项。然后,框架将该范围内的子项(来自旧子列表)放入一个基于其 key 的哈希表中。接下来,框架遍历新子列表中的范围,并通过 key 查询哈希表进行匹配。未匹配的子项将被丢弃并从头开始重建,而匹配的子项将用它们新的 Widget 进行重建。
树操作
#重用 Element 对性能很重要,因为 Element 拥有两个关键数据:状态 Widget 的状态和底层的渲染对象。当框架能够重用 Element 时,用户界面的该逻辑部分的 State 就会得到保留,并且先前计算的布局信息可以被重用,通常可以避免对整个子树的遍历。事实上,重用 Element 非常有价值,以至于 Flutter 支持非局部树突变,这些突变会保留状态和布局信息。
开发者可以通过将 GlobalKey
与其中一个 Widget 相关联来执行非局部树突变。每个全局 key 在整个应用程序中都是唯一的,并且被注册到线程特定的哈希表中。在构建阶段,开发者可以将带有全局 key 的 Widget 移动到 Element 树中的任意位置。框架不会在该位置构建一个新的 Element,而是会检查哈希表,并将现有的 Element 从其先前的 Nutanix 位置重新 parent 到新的位置,从而保留整个子树。
重新 parent 的子树中的渲染对象能够保留其布局信息,因为布局约束是唯一从渲染树中的父对象流向子对象的信息。新的父对象被标记为布局脏,因为它的子列表已更改,但是如果新父对象向子对象传递了与子对象从旧父对象接收到的相同的布局约束,子对象就可以立即从布局中返回,从而截断遍历。
全局 key 和非局部树突变被开发者广泛用于实现英雄过渡和导航等效果。
常数因子优化
#除了这些算法优化之外,实现强大的可组合性还依赖于几个重要的常数因子优化。这些优化在上述主要算法的叶节点处最为重要。
子模型无关。 与大多数使用子列表的工具包不同,Flutter 的渲染树不拘泥于特定的子模型。例如,
RenderBox
类有一个抽象的visitChildren()
方法,而不是一个具体的firstChild
和nextSibling
接口。许多子类只支持单个子对象,直接作为成员变量保存,而不是子列表。例如,RenderPadding
只支持单个子对象,因此它有一个更简单的布局方法,执行时间更短。视觉渲染树,逻辑 Widget 树。 在 Flutter 中,渲染树在设备无关的视觉坐标系中运行,这意味着 x 坐标值越小越靠左,即使当前的阅读方向是从右到左。Widget 树通常在逻辑坐标中运行,即使用起始和结束值,其视觉解释取决于阅读方向。从逻辑坐标到视觉坐标的转换发生在 Widget 树和渲染树之间的交接处。这种方法更有效,因为渲染树中的布局和绘制计算比 Widget 到渲染树的交接更频繁,并且可以避免重复的坐标转换。
文本由专用渲染对象处理。 绝大多数渲染对象都不了解文本的复杂性。相反,文本由一个专门的渲染对象
RenderParagraph
来处理,它在渲染树中是一个叶节点。开发者不是通过继承文本感知的渲染对象,而是通过组合来将文本集成到其用户界面中。这种模式意味着RenderParagraph
可以在其父对象提供相同的布局约束(这很常见,即使在树操作期间)的情况下,避免重新计算其文本布局。可观察对象。 Flutter 同时使用模型观察和响应式范式。显而易见,响应式范式占主导地位,但 Flutter 对某些叶数据结构使用可观察模型对象。例如,
Animation
s 在值更改时会通知观察者列表。Flutter 将这些可观察对象从 Widget 树传递到渲染树,渲染树直接观察它们,并在它们更改时仅使管道的适当阶段失效。例如,Animation<Color>
的更改可能只会触发绘制阶段,而不是构建和绘制阶段。
总而言之,这些针对由强大组合而创建的大型树进行累加的优化,对性能产生了显著影响。
Element 和 RenderObject 树分离
#Flutter 中的 RenderObject
和 Element
(Widget) 树是同构的(严格来说,RenderObject
树是 Element
树的一个子集)。一个明显的简化是将这些树合并成一棵树。然而,在实践中,将这些树分开有许多好处
性能。 当布局发生变化时,只需要遍历布局树的相关部分。由于组合,Element 树通常有许多额外的节点需要跳过。
清晰性。 职责的清晰分离使得 Widget 协议和渲染对象协议可以针对各自的特定需求进行专门化,从而简化了 API 表面,从而降低了错误风险和测试负担。
类型安全。 渲染对象树可以更安全,因为它可以在运行时保证子对象是合适的类型(例如,每个坐标系都有自己的渲染对象类型)。组合 Widget 可以与布局中使用的坐标系无关(例如,暴露应用程序模型一部分的同一个 Widget 可以用于盒状布局和条状布局),因此在 Element 树中,验证渲染对象的类型需要树遍历。
无限滚动
#无限滚动列表对于工具包来说一向是困难的。Flutter 使用基于构建器模式的简单接口支持无限滚动列表,其中 ListView
使用回调来按需构建 Widget,这些 Widget 在用户滚动时变得可见。支持此功能需要视口感知布局和按需构建 Widget。
视口感知布局
#与 Flutter 中的大多数事物一样,可滚动 Widget 也是通过组合构建的。可滚动 Widget 的外部是 Viewport
,它是一个“内部更大”的框,意味着它的子对象可以超出视口的边界,并且可以滚动到视图中。然而,视口没有 RenderBox
子对象,而是拥有 RenderSliver
子对象,称为条状,它们具有视口感知的布局协议。
条状布局协议与盒状布局协议的结构相匹配,即父对象将约束向下传递给子对象,并接收几何信息作为回报。然而,约束和几何数据在两个协议之间有所不同。在条状协议中,子对象会获得关于视口的信息,包括剩余可见空间量。它们返回的几何数据能够实现各种滚动链接效果,包括可折叠的标题和视差。
不同的条状以不同的方式填充视口中可用的空间。例如,一个生成子对象线性列表的条状会按顺序布局每个子对象,直到条状没有更多子对象或空间用完。同样,一个生成二维网格子对象的条状只填充其可见的网格部分。由于它们了解可见空间量,因此条状可以生成有限数量的子对象,即使它们有可能生成无限数量的子对象。
条状可以组合以创建定制的可滚动布局和效果。例如,单个视口可以有一个可折叠的标题,然后是一个线性列表,最后是一个网格。这三个条状将通过条状布局协议进行协作,无论这些子对象属于标题、列表还是网格,它们都只生成可见于视口中的子对象[6]。
按需构建 Widget
#如果 Flutter 有严格的构建-然后-布局-然后-绘制管道,那么上述内容不足以实现无限滚动列表,因为关于视口可见空间量的信息仅在布局阶段可用。没有额外的机制,布局阶段太晚了,无法构建填充空间所需的 Widget。Flutter 通过交织管道的构建和布局阶段来解决这个问题。在布局阶段的任何时候,框架都可以按需开始构建新的 Widget,前提是这些 Widget 是当前执行布局的渲染对象的后代。
构建和布局的交织之所以可能,完全是由于构建和布局算法中信息传播的严格控制。具体来说,在构建阶段,信息只能向下传播到树中。当渲染对象执行布局时,布局遍历还没有访问该渲染对象下方的子树,这意味着在该子树中构建生成的写入不会使到目前为止已进入布局计算的任何信息无效。同样,一旦布局从渲染对象返回,该渲染对象在该布局期间将永远不会再次被访问,这意味着后续布局计算生成的任何写入都不能使用于构建渲染对象子树的信息失效。
此外,线性协调和树操作对于在滚动期间有效地更新 Element 以及在 Element 滚动进出视口边缘时修改渲染树至关重要。
API 易用性
#只有当框架能够有效地使用时,速度快才算有意义。为了指导 Flutter 的 API 设计朝着更高的可用性发展,Flutter 在广泛的开发者用户体验研究中进行了反复测试。这些研究有时证实了预先的设计决策,有时有助于确定功能的优先级,有时则改变了 API 设计的方向。例如,Flutter 的 API 文档非常详尽;用户体验研究证实了这种文档的价值,但也特别强调了示例代码和说明图的必要性。
本节讨论了 Flutter API 设计中的一些为了提高可用性而做出的决策。
API 专门化以匹配开发者的思维模式
#Flutter 的 Widget
、Element
和 RenderObject
树中节点的基类不定义子模型。这使得每个节点都可以针对适用于该节点的子模型进行专门化。
大多数 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,该 Widget 直接使用 Expanded
和 SizedBox
来实现效果。
类似地,隐藏 Widget 子树可以通过根本不包含 Widget 子树来轻松完成。然而,开发者通常期望有一个 Widget 来完成这项工作,因此 Visibility
Widget 存在,将这种模式封装在一个简单的可重用 Widget 中。
显式参数
#UI 框架往往有很多属性,以至于开发者几乎不可能记住每个类的每个构造函数参数的语义含义。由于 Flutter 使用响应式范式,Flutter 中的构建方法通常有很多构造函数调用。通过利用 Dart 对命名参数的支持,Flutter 的 API 能够使这些构建方法保持清晰和易于理解。
此模式扩展到任何具有多个参数的方法,并且特别扩展到任何布尔参数,以便方法调用中的孤立的 true
或 false
字面量始终是自解释的。此外,为了避免通常由 API 中的双重否定引起的混淆,布尔参数和属性始终以正面形式命名(例如,enabled: true
而不是 disabled: false
)。
规避陷阱
#Flutter 框架中许多地方使用的一种技术是定义 API,使其不存在错误条件。这消除了整个类别的错误。
例如,插值函数允许插值的开始或结束值为 null,而不是将此定义为错误情况:两个 null 值之间的插值始终为 null,从 null 值插值或插值到 null 值相当于插值到给定类型的零模拟值。这意味着偶然将 null 传递给插值函数的开发者不会遇到错误情况,而是会得到一个合理的结果。
一个更微妙的例子是 Flex
布局算法。这种布局的概念是将分配给 Flex 渲染对象的空间在其子对象之间分配,因此 Flex 的大小应该是可用空间的全部。在原始设计中,提供无限空间会导致失败:这将意味着 Flex 的大小是无限的,这是一种无用的布局配置。相反,API 已调整,以便在为 Flex 渲染对象分配无限空间时,渲染对象的大小会调整以适应子对象的所需大小,从而减少了可能错误的情况。
该方法还用于避免出现允许创建不一致数据的构造函数。例如,PointerDownEvent
构造函数不允许将 PointerEvent
的 down
属性设置为 false
(这种情况是自相矛盾的);相反,构造函数没有 down
字段的参数,并始终将其设置为 true
。
总的来说,该方法是定义输入域中所有值的有效解释。最简单的例子是 Color
构造函数。它不接受四个整数,一个代表红色,一个代表绿色,一个代表蓝色,一个代表 Alpha,每个都可能超出范围,而是采用单个整数值,并定义每个位的含义(例如,最低八位定义红色分量),以便任何输入值都是有效的颜色值。
一个更复杂的例子是 paintImage()
函数。该函数接受十一个参数,其中一些参数的输入域相当宽,但它们被仔细设计成大部分是相互正交的,因此无效组合非常少。
主动报告错误情况
#并非所有错误条件都可以通过设计消除。对于剩余的错误,在调试版本中,Flutter 通常会尽早捕获错误并立即报告。断言被广泛使用。构造函数参数会被详细地进行健全性检查。生命周期会被监控,当检测到不一致时,会立即抛出异常。
在某些情况下,这会被推向极致:例如,在运行单元测试时,无论测试还在做什么,每个布局过的 RenderBox
子类都会检查其内在尺寸方法是否满足内在尺寸约定。这有助于捕获 API 中可能未被使用的错误。
当抛出异常时,它们会包含尽可能多的可用信息。Flutter 的一些错误消息会主动探测相关的堆栈跟踪,以确定实际错误的可能位置。另一些会遍历相关的树来确定错误数据的来源。最常见的错误包括详细说明,有时还包括避免错误的示例代码,或指向进一步文档的链接。
响应式范式
#可变树 API 存在一个二元访问模式:创建树的原始状态通常使用一组非常不同的操作,而后续更新则使用另一组。Flutter 的渲染层使用此范式,因为它是维护持久树的有效方法,这对于高效布局和绘制至关重要。然而,这意味着直接与渲染层交互充其量很笨拙,最坏情况则容易出错。
Flutter 的 Widget 层引入了一种使用响应式范式[7]的组合机制来操作底层渲染树。此 API 通过将树创建和树突变步骤合并为单个树描述(构建)步骤来抽象出树操作,在该步骤中,在系统状态每次更改后,由开发者描述用户界面的新配置,框架计算出反映此新配置所需的树突变序列。
插值
#由于 Flutter 框架鼓励开发者描述与当前应用程序状态匹配的界面配置,因此存在一种机制可以在这些配置之间进行隐式动画。
例如,假设在状态 S1 中,界面由一个圆组成,而在状态 S2 中,它由一个正方形组成。如果没有动画机制,状态更改将导致一个刺眼的界面变化。隐式动画允许圆在几帧内平滑地变为正方形。
每个可以隐式动画的特征都有一个有状态的 Widget,它会记录输入的当前值,并在输入值更改时开始一个动画序列,在指定的持续时间内从当前值过渡到新值。
这是使用 lerp
(线性插值)函数以及不可变对象实现的。每个状态(例如,圆和正方形)都表示为一个不可变对象,该对象配置有适当的设置(颜色、笔触宽度等)并知道如何自行绘制。当需要绘制动画的中间步骤时,开始和结束值将传递给适当的 lerp
函数,以及一个表示动画中点的 *t* 值,其中 0.0 表示开始,1.0 表示结束[8],该函数返回一个表示中间阶段的第三个不可变对象。
对于从圆到正方形的过渡,lerp
函数将返回一个表示“圆角正方形”的对象,其半径由 *t* 值导出的分数描述,颜色通过颜色 lerp
函数插值,笔触宽度通过双精度 lerp
函数插值。该对象实现了与圆和正方形相同的接口,然后可以在请求时绘制自身。
这种技术允许状态机制、状态到配置的映射、动画机制、插值机制以及与如何绘制每一帧相关的特定逻辑完全彼此分离。
这种方法具有广泛的适用性。在 Flutter 中,可以插值基本类型,如 Color
和 Shape
,但也可以插值更复杂的类型,如 Decoration
、TextStyle
或 Theme
。这些通常由可以自身插值的组件构成,并且对更复杂的对象进行插值通常与递归地插值描述复杂对象的_所有_值一样简单。
一些可插值对象由类层次结构定义。例如,形状由 ShapeBorder
接口表示,存在各种形状,包括 BeveledRectangleBorder
、BoxBorder
、CircleBorder
、RoundedRectangleBorder
和 StadiumBorder
。单个 lerp
函数无法预测所有可能的类型,因此接口定义了 lerpFrom
和 lerpTo
方法,静态 lerp
方法会委托给它们。当被告知从形状 A 插值到形状 B 时,首先会询问 B 是否可以 lerpFrom
A,如果不能,则会询问 A 是否可以 lerpTo
B。(如果两者都不可能,则函数在 *t* 值小于 0.5 时返回 A,否则返回 B。)
这允许类层次结构被任意扩展,后期添加的类能够在其与先前已知的_值之间进行插值。
在某些情况下,插值本身无法用任何可用类来描述,并且定义了一个私有类来描述中间阶段。例如,在 CircleBorder
和 RoundedRectangleBorder
之间进行插值时就是这种情况。
这种机制还有一个额外的优点:它可以处理从中间阶段到新值的插值。例如,在圆到正方形过渡的中间,形状可以再次改变,导致动画需要插值到三角形。只要三角形类可以 lerpFrom
圆角正方形中间类,就可以无缝执行过渡。
结论
#Flutter 的口号“一切皆 Widget”围绕着通过组合 Widget 来构建用户界面,而 Widget 又由逐渐更基础的 Widget 组成。这种强大的组合结果是大量的 Widget,需要精心设计的算法和数据结构来高效处理。通过一些额外的设计,这些数据结构还可以使开发者轻松创建无限滚动列表,这些列表在 Widget 可见时按需构建。
脚注
至少对于布局而言。它可能需要为绘制、必要时构建可访问性树以及必要时进行命中测试而再次访问。 ↩︎
当然,现实情况要复杂一些。一些布局涉及内在尺寸或基线测量,这确实需要对相关子树进行额外的访问(通过积极缓存来减轻最坏情况下的二次方性能)。然而,这些情况出奇地罕见。特别是,对于常见的收缩包装情况,不需要内在尺寸。 ↩︎
严格来说,子对象的位置不是其 RenderBox 几何形状的一部分,因此不必在布局期间计算。许多渲染对象在相对于自己的原点将单个子对象隐式定位在 0,0 处,这根本不需要计算或存储。一些渲染对象避免在最后一刻计算其子对象的位置(例如,在绘制阶段),以完全避免计算,前提是它们之后不会被绘制。 ↩︎
此规则存在一个例外。如按需构建 Widget部分所述,某些 Widget 可以由于布局约束的变化而被重建。如果在同一个帧中,一个 Widget 因不相关的原因变脏,并且同时受到布局约束变化的影响,它将被更新两次。这种冗余构建仅限于 Widget 本身,并且不会影响其后代。 ↩︎
Key 是一个可选的、不透明的对象,它与 Widget 相关联,其相等运算符用于影响协调算法。 ↩︎
为了可访问性,并为应用程序在 Widget 构建到屏幕显示之间提供几毫秒的额外时间,视口会为可见 Widget 前后几百像素创建(但不绘制)Widget。 ↩︎
这种方法最早由 Facebook 的 React 库普及。 ↩︎
实际上,*t* 值允许超出 0.0-1.0 的范围,对于某些曲线也是如此。例如,“弹性”曲线会短暂地过度,以表示弹跳效果。插值逻辑通常可以根据需要推断到开始或结束值之外。对于某些类型,例如插值颜色时,*t* 值实际上会被限制在 0.0-1.0 的范围内。 ↩︎