[翻译] Flutter 内幕之 Layout

329 阅读7分钟

Layout

原文地址:www.flutterinternals.org/rendering/l…

什么是 Relayout Boundary?

  • Relayout boundary 是 render tree 中节点之间的逻辑划分点。边界之下的节点永远不会使边界之上的节点的布局失效。
  • Relayout boundary 表示为满足一组条件的 render object(RenderObject._relayoutBoundary),允许其独立于其父节点进行布局。也就是说,以 relayout boundary 为跟节点的子树无法影响树的其他部分。
    • 如果一个 render object 节点的父节点忽略了它的 size(!parentUseSize )、完全定义该节点的 size(sizedByParent )、传入严格约束或它不是一个 render object(例如 RenderView),那该节点就定义为一个 relayout boundary(即无法影响父节点的布局)。
    • 后两种情况表明,无论子树发生什么,子节点都无法更改大小。
    • parentUsesSize 参数可防止 render object 成为 relayout boundary。因此,将 render object 标记为脏时,所有祖先节点(包括父节点)都将标记为脏,直到最接近的 relayout boundary。
  • 当 render object 需要重新布局时,relayout boundary 限制了有多少祖先节点必须标记为脏的 。遍历会在最近的祖先节点 relayout boundary 处停止,因为超出该点的节点不受子孙节点布局的影响。
  • 实际布局的时候,relayout boundary 会被忽略。Render object 始终会 layout 子节点。但是,当子节点没有被标记为脏或接收到相同的约束(约束不变自然不需要 layout)时遍历就会停止。
    • RenderObject.layout 负责执行此逻辑。RenderObject._layoutWithoutResize无条件执行 layout,因为 PipelineOwner.flushLayout 已经跳过了干净的节点。

布局如何标记 dirty?

  • PipelineOwner 维护一个脏节点列表(PipelineOwner._nodeNeedingLayout),通过 PipelineOwner.flushLayout 方法每一帧清空一次该列表。
  • 将 render objects 标记为需要布局是可以批量操作的(即,可以在一次遍历中处理布局,而不必多次布局节点)。
  • 当通过 RenderObject.markNeedsLayout 设置 RenderObject._needsLayout来把一个 render object 标记为脏的时候,在最近的 relayout boundary 之下的所有的祖先节点(包括 relayout boundary)也会被标记为 dirty(通过 RenderObject.markParentNeedsLayout )。
    • 只有最近的闭区间中的 relayout boundary 会被添加到 PipelineOwner._nodeNeedingLayout 中。所有子孙节点会遍历执行布局。同时这也会请求下一帧的回调(通过PipelineOwner.requestVisualUpdate)来让布局变更得以应用(通过 PipelineOwner.flushLayout)。
    • 当处理较早加入 dirty list 的节点的时候,比较晚加入的节点可能会变为 clean; PipelineOwner.flushLayout 忽略 clean 节点。
  • 如果 sizedByParent 标记发生更改,则必须通知 render object 及其父节点(通过 RenderObject.markNeedsLayoutForSizedByParentChange)。更新此标志可以在渲染树中添加或删除 relayout boundary(更改随后由 RenderObject.layout提交)。大多数 render object 不会动态更新此标记。
    • Dirty list 中的节点是使用 RenderObject._layoutWithoutResize 进行布局的,这不会更新 relayout boundary,因此必须把父节点标记为 dirty。因此,由于子节点的状态可能会改变,因此也必须 layout 其父节点。
    • 更改此标志位还意味着子节点将采用新尺寸,从而可能使父节点的布局无效。

约束是如何传递的?

  • 从视图配置(ViewConfiguration.size)读取时,RenderView 提供了最顶层的约束(通过RenderView.performLayout)。这以逻辑像素点来表示整个可视界面的尺寸。
  • 标记为 dirty 的 render object 使用之前缓存的约束(RenderObject.constraints)执行布局。这些约束不能是无效的。否则,该树上方的节点将被标记为脏节点。
  • Render object 的尺寸(由 RenderObject.performResizeRenderObject.performLayout 计算)可能会影响提供给子节点的约束。约束也可能是完全任意的(例如OverflowBox)。这些约束将转发给任何子节点,然后递归地重复该过程。
    • 约束会缓存并用于 RenderObject.layout

Layout 是如何执行的?

  • PipelineOwner.flushLayout 会按照深度递增的顺序触发 dirty list(PipelineOwner._nodesNeedingLayout)中的所有节点进行 layout。此列表中的节点相当于 relayout boundary, 它们调用 RenderObject._layoutWithoutResize 而不是 RenderObject.layout 进行 layout。
    • Dirty List 中的节点永远不需要调用 RenderObject.performResize。考虑一个由父节点确定 size 的节点。
      • 如果父节点不是脏的,传入的 constraints 没有变更。因此,resize 是不必要的。
      • 如果父节点被标记为脏,由于节点是从上到下进行清理的,因此将首先对其进行布局。这最终将布局原来的节点(现在可能会重新计算大小),从而有效地将其从脏列表中删除。
    • RenderObject._layoutWithoutResize 不会更新缓存的 constraints(RenderObject.constraints)或者 relayout boundary (RenderObject._relayoutBoundary)。
      • 由于祖先节点尚未标记为脏,因此传入的约束不会改变。
      • 由于此节点和父节点之间的关系没有改变,因此 Relayout boundary 状态不会变更。如果改变了,父节点也会被标记为脏(通过 RenderObject.markNeedsLayoutForSizedByParentChange)。
  • 当一个子节点被标记为脏、接收到新约束或刚刚成为 relayout boundary 时,将执行布局(通过RenderObject.layout)。否则,它将标记为 clean 并中断遍历。
    • 注意,布局优化是在标记阶段而不是布局阶段中应用的。当一个子代被标记为脏的节点时,Relayout boundaries 提供了一个限制但没有其他直接的影响。
    • RenderObject.layout 缓存新的约束 (RenderObject.constraints) ,并将 relayout boundary 字段 (RenderObject._relayoutBoundary) 的任何变更传递给受影响的子孙节点 (通过 RenderObject._cleanChildRelayoutBoundary)。
  • 由其父节点调整 size(RenderObject.sizedByParent)的 Render objects 仅通过传入的约束来确定新的 size(通过 RenderObject.performResize)。所有 render objects(包括已调整 size 的)都继续进行 layout (通过 RenderObject.performLayout).
    • 这个方法负责布局所有子节点(通过 RenderObject.layout)和更新所有子节点的 parent data。
    • 子节点有可能在父节点之前或之后进行布局,这取决于父节点如何执行 layout(例如,当父节点的 size 被子节点影响 size 时,必须首先布局它的子节点 )。
    • 那些不是由父节点决定 size 的 render object 必须在 RenderObject.performLayout中确定一个新的 size。
  • 完成后,RenderObject.layout 将节点标记为需要语义化(semantics)和绘制(painting),但不再需要布局。

如何优化布局?

  • 某些条件下可以避免布局子节点:
    • 如果子节点不是脏的且约束条件不变,则无需布局,因此可中断遍历。
    • 如果父节点没有使用子节点的 size,则它无需于子节点一起布局;子节点必然符合原始传入的约束,因此夫节点不会发生任何改变。
    • 如果父节点传递了严格(tight)约束给子节点,则它无需于子节点一起布局;子节点不能改变大小。
    • 如果子节点为 SizeByParent(给定的约束完全决定其 size),并且再次传递相同的约束,则父节点无需与子节点一起布局;子节点不能改变大小。
  • 沿着树向下布局时会维护最近的上游 relayout boundary。执行布局时(通过 RenderObject.performLayout),render object 确定其是否满足上述任何优化标准。如果符合,它将自身设置为最近的 relayout boundary。否则,它将采用父节点的 relayout boundary。
    • 更新 relayout boundary 后会继续进行布局,即使节点是 clean 的且传入的约束不变。
    • 所有子节点必须更新它们所知道的最近 relayout boundary。这是通过递归访问以清除 relayout boundary 并标记 render object 为需要进行布局(通过 RenderObject._cleanRelayoutBOundary)来实现的。遍历会在他们自己划定的 relayout boundary 的后代处停止(即 RenderObject._relayoutBoundary == this)。
    • 布局可能已无效的节点(即,可能导致布局被跳过的假设不再成立)必须被标记为脏。布局还负责沿受影响的 render object 树向下传递 relayout boundary。

如何构建 UI 以响应布局?

  • RenderObject.invokeLayoutCallback 允许在布局过程中调用 buidler callback。只能对仍然为脏的子树进行操作,以确保将节点精确地布局一次。这样,可以在布局过程中按需构建子节点(viewports 的需要)和移动节点(进行 global key 重定)。
  • 以下几个不变量确保这是有效的:
    • 构建是单向的 -- 信息仅沿树向下流动,一次访问一个节点。
    • 布局一次性访问每个脏节点。
    • 当前子节点下方的构建不能使先前的布局失效;构建信息只会沿着树向下流动。
    • 在布局节点后进行够能不能使该节点无效;节点永远不会被重新访问。
    • 因此,在以给定的 render object 为根的未访问子树中进行构建是安全的。