深入Flutter(二) - 线性时间复杂度的 build 和 layout

文章系列
深入Flutter(一) 积极推进“组合”方式
深入Flutter(三) Element树和RenderObject树
深入Flutter(四) Infinite scrolling – 无限滚动
深入Flutter(五) 专业化API

原文:Inside Flutter

译者额外添加flutter的渲染流程图,有助于理解本文:
图一、
在这里插入图片描述
图二、
在这里插入图片描述

Sublinear layout --亚线性(sublinear)布局(layout)

With a large number of widgets and render objects, the key to good performance is efficient algorithms. Of paramount importance is the performance of layout, which is the algorithm that determines the geometry (for example, the size and position) of the render objects. Some other toolkits use layout algorithms that are O(N²) or worse (for example, fixed-point iteration in some constraint domain). Flutter aims for linear performance for initial layout, and sublinear layout performance in the common case of subsequently updating an existing layout. Typically, the amount of time spent in layout should scale more slowly than the number of render objects.
对于大量的widget和render对象,高效的算法是获得良好性能的关键。 最重要的是Layout期间的性能,在这期,确定render对象的几何形状(这里的几何形状是指诸如大小和位置)。 一些其他一些工具集使用的是(时间复杂度)O(N²) 或更差的layout算法(例如,某些constraint区域中的定点迭代)。 Flutter旨在为初始化Layout时提供线性级别的性能(复杂度是O(N)),并在随后更新现有Layout的常见情况下实现亚线性layout性能。 通常,在layout中花费的时间应该比render对象的数量要小。
(译者补充:大概意思是时间复杂度还是O(N)级别,但是大部分时候都小于N,不需要全遍历)

Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
Flutter每帧执行一次layout,并且layout算法单向一次性完成。 Constraint是通过父级对象(译者注:这里的父子关系不是继承,而是组合的父容器被组合的子对象,并且Layout的实现是在RenderObject上)在其每个子级对象上调用Layout方法而沿树向下传递的。 子级递归地执行自己的layout,然后在自己的layout方法中return几何形状(大小等信息),沿着树上传。 重点是,一旦render对象从其layout方法return后,该render对象将不会再次访问,直到下一帧的layout为止。 这种方法将原本可能是分离的measure和layout传递结合到了一起,因此,在layout期间,每个render对象最多被访问2次:一次在树(tree)从上往下,一次在树(tree)从下往上。

Flutter has several specializations of this general protocol. The most common specialization is RenderBox, which operates in two-dimensional, cartesian coordinates. In box layout, the constraints are a min and max width and a min and max height. During layout, the child determines its geometry by choosing a size within these bounds. After the child returns from layout, the parent decides the child’s position in the parent’s coordinate system. Note that the child’s layout cannot depend on its position, as the position is not determined until after the child returns from the layout. As a result, the parent is free to reposition the child without needing to recompute its layout.
Flutter有数种特定情况的通用协议。 最常见的情况是RenderBox,它在二维笛卡尔坐标中运行。 在box layout中,constraint决定最小和最大宽度以及最小和最大高度。 在layout中,子对象通过在(constraints规定的)范围内确定自己的几何形状,也就是大小。 子对象在layout 中 return后,父对象将决定子对象在父对象t坐标系中的位置。 请注意,子对象的layout不能决定自己的位置,因为需要直到子对象从layout中return 后才能确定位置。 结果就是,父对象可以自由地重新定位子对象,而无需重新计算子对象的layout。
(译者补充:这里讲的父子对象都是渲染树上的renderObject)

More generally, during layout, the only information that flows from parent to child are the constraints and the only information that flows from child to parent is the geometry. These invariants can reduce the amount of work required during layout:
更具体的讲,layout期间,从父widget对象流到子widget对象的唯一信息是constraint,而从子widget对象流到父widget对象的唯一信息是几何(大小)。 这些固定的流程可以减少layout间所需的工作量:

  1. If the child has not marked its own layout as dirty, the child can return immediately from layout, cutting off the walk, as long as the parent gives the child the same constraints as the child received during the previous layout.
    1、如果子对象没有将自己的layout标记为dirty,则只要父对象给子对象与上一次layout中收到的子对象相同的constraint,子对象就可以直接从layout中立即return,无需继续往下走。

  2. Whenever a parent calls a child’s layout method, the parent indicates whether it uses the size information returned from the child. If, as often happens, the parent does not use the size information, then the parent need not recompute its layout if the child selects a new size because the parent is guaranteed that the new size will conform to the existing constraints.
    2、每当父对象调用子对象的layout方法时,父对象就会间接判断是否使用从子对象 return的size信息。 通常情况下,父对象并不使用size信息,即使子对象选择了新的size,父对象也无需重新计算子对象的layout,因为父对象可以保证新的size符合现有constraint。

  3. Tight constraints are those that can be satisfied by exactly one valid geometry. For example, if the min and max widths are equal to each other and the min and max heights are equal to each other, the only size that satisfies those constraints is one with that width and height. If the parent provides tight constraints, then the parent need not recompute its layout whenever the child recomputes its layout, even if the parent uses the child’s size in its layout, because the child cannot change size without new constraints from its parent.
    3、严格的constraint (紧约束)是仅需通过一种有效的几何形状来实现的。 例如,如果最小和最大宽度彼此相等,而最小和最大高度彼此相等,则满足这些约束(constraint)的唯一尺寸就是具有该宽度和高度的尺寸。 如果父对象提供严格的constraint(紧约束),无论何时,子对象即使重新计算其layout,父对象也无需重新计算它的layout,甚至即使父对象在layout中使用了子对象的size,同样也不用重新计算,因为如果没有父对象的新constraint,子对象也就无法更改size。

  4. A render object can declare that it uses the constraints provided by the parent only to determine its geometry. 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.
    4、一个render对象可以声明仅使用父对象提供的constraint来确定其几何形状。 这样的声明通知framework:即使子项的layout是不严格的constraint(松约束)(最大最小范围不同)且父项的layout大小取决于子项的layout大小,子项已经执行了layout后,该renderObject的父项也无需重新计算其layout大小,因为如果子项没有获取到其父级的新的constraint,子项也无法改变自己的大小。

As a result of these optimizations, when the render object tree contains dirty nodes, only those nodes and a limited part of the subtree around them are visited during layout.
这些优化的成果是,当renderObject树包含dirty节点时,在Layout期间仅访问那些(标记为dirty的)节点和对应子树的有限部分。

Sublinear widget building --widget在build 期间的亚线性(时间复杂度)

Similar to the layout algorithm, Flutter’s widget building algorithm is sublinear. After being built, the widgets are held by the element tree, which retains the logical structure of the user interface. The element tree is necessary because the widgets themselves are immutable, which means (among other things), they cannot remember their parent or child relationships with other widgets. The element tree also holds the state objects associated with stateful widgets.
与layout算法类似,Flutter的widget 的 build算法也是亚线性(O(N)时间复杂度)的。 经过构建后,widget由element树保留维护,element树保留了用户界面的逻辑结构。 element树是必需的,因为widget本身是不可变的(immutable),这意味着(除特殊情况外)它们无法记住与其他widget的父子关系。 element树还保存与stateful widgets关联的state对象。(译者注:Element 其实就是BuildContext 对象。在渲染一帧的流程中,是先执行widget的Build ,再执行 renderObject 的 Layout 和 Paint)

In response to user input (or other stimuli), an element can become dirty, for example if the developer calls setState() on the associated state object. The framework keeps a list of dirty elements and jumps directly to them during the build phase, skipping over clean elements. During the build phase, information flows unidirectionally down the element tree, which means each element is visited at most once during the build phase. Once cleaned, an element cannot become dirty again because, by induction, all its ancestor elements are also clean.
一旦响应用户输入(或其他刺激),element可能会变为dirty,例如,如果开发人员在关联的state对象上调用setState()。 framework保留了一个dirty element列表,并在build阶段直接跳转到它们,跳过了没有标记为dirty的 clean element( clean是相对于dirty的概念)。 在build阶段,信息单向流向element树,这意味着在构建阶段最多访问一次每个element。 转为clean,某个element就不会再次变dirty,因为通过induction,它的所有原型elements也都是clean状态的(有一种特殊情况,后续的Building widgets on demand --按需build widget进行讨论)。

Because widgets are immutable, if an element has not marked itself as dirty, the element can return immediately from build, cutting off the walk, if the parent rebuilds the element with an identical widget. Moreover, the element need only compare the object identity of the two widget references in order to establish that the new widget is the same as the old widget. Developers exploit this optimization to implement the reprojection pattern, in which a widget includes a prebuilt child widget stored as a member variable in its build.
因为widget是不可变的(immutable),所以在element未将其自身标记为dirty的情况,则该element可以立即从build中return并切断遍历。 此外,如果父级重新build的widget带有标识(key),该element仅需要比较两个widget引用的对象标识(key),即可确定新widget与旧widget 是否相同。 开发人员利用这种优化方案来实现重投影模式(reprojection pattern),其中widget包括一个预先build的子widget,该子widget在其build中存储为成员变量。

During build, Flutter also avoids walking the parent chain using InheritedWidgets. If widgets commonly walked their parent chain, for example to determine the current theme color, the build phase would become O(N²) in the depth of the tree, which can be quite large due to aggressive composition. To avoid these parent walks, the framework pushes information down the element tree by maintaining a hash table of InheritedWidgets at each element. Typically, many elements will reference the same hash table, which changes only at elements that introduce a new InheritedWidget.
在build期间,Flutter还避免使用InheritedWidgets遍历父链。 如果widget像通常那样沿着其父链移动(例如,以确定当前的主题颜色 theme color),则build阶段,树的深度将变为O(N²),由于“激进的组合”,该阶段可能会(时间复杂度)很大。 为了避免这些父项的窜动,framework通过在每个element上维护InheritedWidgets的哈希表,将信息向下推送到element树。 通常,许多element将引用相同的哈希表,该哈希表仅在引入新的InheritedWidget的element上发生更改。

Linear reconciliation --线性对账(算法)

Contrary to popular belief, Flutter does not employ a tree-diffing algorithm. Instead, the framework decides whether to reuse elements by examining the child list for each element independently using an O(N) algorithm. The child list reconciliation algorithm optimizes for the following cases:
与普遍的看法相反,Flutter不采用树形算法。 相反,框架通过使用时间复杂度O(N)算法独立检查每个element的子级列表来决定是否重复使用element。 子列表reconciliation算法针对以下情况进行了优化:

  1. The old child list is empty.
    旧的(element)子列表为空。
  2. The two lists are identical.
    这两个(element)列表是相同的。
  3. There is an insertion or removal of one or more widgets in exactly one place in the list.
    在列表中某个一个位置插入或删除了一个或多个widget。
  4. If each list contains a widget with the same key, the two widgets are matched.
    如果每个列表包含一个具有相同key的widget,则两个widget将匹配。

The general approach is to match up the beginning and end of both child lists by comparing the runtime type and key of each widget, potentially finding a non-empty range in the middle of each list that contains all the unmatched children. The framework then places the children in the range in the old child list into a hash table based on their keys. Next, the framework walks the range in the new child list and queries the hash table by key for matches. Unmatched children are discarded and rebuilt from scratch whereas matched children are rebuilt with their new widgets.
常规方法是通过比较每个widget运行时的type和key,对比两个子列表的开头和结尾,从而有可能在每个列表中找到一个包含所有不匹配子项的非空范围。 然后,framework根据他们的key将旧子项列表中范围内的子项放入哈希表中。 接下来,framework在新子widget列表范围中遍历,并通过key查询哈希表中的匹配项。 不匹配的子项被丢弃并从头匹配范围的终点,不匹配的起始点开始重新build,生成新的widget树衔接上去。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值