[翻译] Flutter 内幕之 Render Objects

190 阅读15分钟

Render Objects

原文地址:www.flutterinternals.org/data-model/…

Render Object 有哪些组成部分?

  • RenderObject 提供了管理可视 element tree 的基本基础结构。Render object 定义了布局、绘画和合成的一般协议。这个协议在很大程度上是抽象的,由子类来决定布局的输入和输出,如何进行命中测试(尽管所有的 render object 都是HitTestTargets),以及如何对 render object 的层次结构进行建模(RenderObject扩展了AbstractNode,它没有指定一个具体的子节点模型)。
  • Constraints 代表布局的不可变输入。该定义是灵活的,只要约束可以表明它们是否代表单一配置(Constraints.isTight),以规范的形式表达(Constraints.isNormalized),并可以进行相等性比较(通过Constraints.==Constraints.hashCode)。
  • ParentData表示父节点存储在子节点的不透明数据。这种数据通常被认为是 parent data 的实现细节,因此不应该被子节点访问。Parent data 是灵活定义的,只包括一个方法(ParentData.detach,它允许实例对其 render object 在树中的移除作出反应)。

Render objects 如何渲染?

  • RendererBinding 作为 RendererBinding.initInstances 的一部分建立了一个PipelineOwner(即绑定的 "构造函数")。Pipeline owner 作为 render tree 的 AbstractNode.owner (由AbstractNode.attach设置并由AbstractNode.adopChild传递)。
  • PipelineOwner类似于BuildOwner,它跟踪哪些 render object 需要合成、布局、绘画或语义更新。节点通过将自己添加到特定的列表(例如,PipelineOwner._nodesNeedingLayoutPipeplineOwner._nodesNeedingPaint)来标记自己是脏的。之后,所有脏的对象都会被相应的 "flush "方法(如PipelineOwner.flushLayoutPipelineOwner.flushPaint)清理。这些方法启动相应的渲染过程,并根据需要在每一帧中被调用(通过RendererBinding.drawFrameWidgetsBinding.drawFrame)。
    • 通过特定的顺序刷新脏的 render object 来进行渲染。
      • PipelineOwner.flushLayout 布局所有脏的 render objects(根据每个RenderObject.performLayout 的实现,递归地布局脏的子节点)。
      • PipelineOwner.flushCompositingBits 更新缓存以表示子代 render object 是否创建了新的 layer。如果是这样,因为绘制经常是交错进行的(即一个 render object 可能在其子节点之前或之后进行绘制),所以某些操作可能需要以不同的方式来实现。如果所有的子代都绘制到同一个 layer 中,那么就可以使用单一的 painting context 来执行一个操作(比如 clipping)。如果不是,有些效果只能通过插入新的 layer 来实现。这些标示位决定了绘制时将使用哪个选项。
      • PipelineOwner.flushPaint 会绘制所有脏的 render object(根据每个 RenderObject.paint 的实现,递归地绘制脏的子节点)。
      • PipelineOwner.flushSemantics 为所有 dirty 渲染对象编译语义数据。
  • 每当一个对象将自己标记为 dirty 时,它一般还会调用 PipelineOwner.requestVisualUpdate 来调度一帧并最终更新用户界面。请求界面更新会调用由 RendererBinding 构造 PipelineOwner 时绑定的界面更新 handler(PipelineOwner.onNeedVisualUpdate)。
  • 如果在当前帧期间(例如,在下帧回调期间)不能进行界面更新,默认的 handler 会调用 RendererBinding.esureVisualUpdate 来调度一个帧(通过 SchedulerBinding.scheduleFrame)。
  • 当 render object 被附加到 PipelineOwner 时(通过 RenderObject.attach),它将自己添加到所有相关的脏列表中(例如,RenderObject.markNeedsLayoutRenderObject.markNeedsPaint)。
    • 通常情况下,一个新创建的 render object 在此时将是 dirty 的,因为大多数 dirty 位都被初始化为 true(例如 RenderObject._needsLayoutRenderObject._needsPaint)。其他几个位取决于节点的配置(例如,RenderObject._needsCompositing),但如果它们适用,则会为真。
    • 如果 render object 之前已经被分离、改变,现在又被重新连接,那么它也可能是脏的。
    • 作为一个例外,最开始的绘制、布局和语义传递是由一个不同的流程驱动的(通过RenderObject.scheduleInitialLayoutRenderObject.scheduleInitialPaintRenderObject.scheduleInitialSemantics)。这个流程发生在第一次构造 RenderView 的时候(通过RendererBinding.initRenderView)。这个流程调用 RenderView.prepareInitialFrame,它为RenderView 调度初始布局和绘制(语义化遵循一个稍微不同的流程)。将根节点标记为脏,会导致整个树根据需要执行布局、绘制和语义化。
  • 在热重载过程中,渲染器调用 RenderView.reassemble(通过RendererBinding.executeReassemble),它在渲染树上层层递进。默认情况下,render object 将自己的布局、合成、绘制和语义化位标记为 dirty(通过RenderObject.reassemble)。

Render tree 的结构是什么样的?

  • Render tree 由 RenderObject 实例组成,他们是 AbstractNode 的子类。
    • 每当增加或删除一个子树时,必须分别调用 RenderObject.adopChildRenderObject.dropChild。修改子节点模型意味着子树的布局、绘画、语义和合成位会被标记成脏。
    • 每当一个子节点的深度发生变化时,必须调用 RenderObject.redepthChild
    • Render object 必须附加到 PipelineOwner 上(通过 RenderObject.attach),并在不需要再次渲染时脱离(通过 RenderObject.detach)。
      • PipelineOwner 维护着对根节点的引用(PipelineOwner.rootNode)。当初始化 RendererBinding 时,该字段被设置为 app 的 RenderView(通过RendererBinding.initRenderView 设置 RendererBinding.renderView,并通过一个 setter 设置 PipelineOwner.rootNode)。这将自动分离旧的 render object(如果有的话)并附加新的 object。
      • 子节点被使用或弃用时,会自动附加和分离。
      • 然而,由于父节点可以任意附加和分离,所以 render object 要负责对其子节点调用相应的方法。
    • 引入子模型的子类(如 ContainerRenderObjectMixin)必须覆盖 RenderObject.redepthChildren,在每个子类上调用RenderObject.redepthChild
    • Render object 有一个 parent (RenderObject.parent) 和一个附加的 state (RenderObject.attached),表示它们是否会对渲染有用。
  • Parent data 可以使用 ParentData 的子类来与子节点关联起来;这个数据只能使用 RenderObject.setupParentData 来分配。按照惯例,这个数据对子类来说是不透明的,尽管协议可以自由改变这个规则(但不应该这么早)。Parent data 在之后可以被改变(例如,通过RenderObject.performLayout)。
  • ``ContainerRenderObjectMixin`使用这个字段实现一个链表来存储子节点。
  • 所有 RenderObjects 通过 RenderObject.RenderObject.visitChildren 实现访问者模式,该模式对每个子代调用一次 RenderObjectVisitor

当添加或删除子节点时需要做些什么?

  • 当添加或删除一个子节点时,分别调用 RenderObject.adoptChildRenderObject.dropChild
  • 父节点的 RenderObject.attachRenderObject.detach 方法必须在每个子节点上调用相同的方法。
  • 父节点的 RenderObject.redepthChildrenRenderObject.visitChildren方法必须在每个子节点上分别递归调用 RenderObject.redepthChild 和 visit 参数。

Render tree 有哪些组成部分?

  • RenderObjectWithChildMixin 在 Render object 中存储一个单一的子节点引用。
  • 一个子节点的 setter (RenderObjectWithChildMixin.child) 会适当地使用和弃用子节点(例如,当添加和删除一个子节点时)。
    • 更新子节点代表着 render object 需要布局,因此需要绘制。
    • 附加、分离、重新计算深度和迭代都会被重写以合并子节点。
  • ContainerRenderObjectMixin 使用每个子节点的 parent data(必须实现 ContainerParentDataMixin)来维护子代的双链表(通过ContainerParentDataMixin.nextSiblingContainerParentDataMixin.previousSibling)。
    • 底层的链表支持插入和删除(通过ContainerParentDataMixin._insertIntoChildListContainerParentDataMixin._removeFromChildList)。可以使用一个可选的前节点参数来选择性地插入节点到特定位置。
    • 各种容器类型的方法被暴露出来(例如,ContainerRenderObjectMixin.insertContainerRenderObjectMixin.addContainerRenderObjectMixin.moveContainerRenderObjectMixin.remove)。这些根据需要使用和启用子节点。它们提供了一个移动子节点的操作,以避免不必要地放弃和重新使用一个子节点。
    • 更新(或重新排序)子列表代表着 render object 虚要布局,因此需要绘制。
    • 这些使用、放弃、附加和分离子子节点的操作是恰当的。此外,当子节点列表改变时,RenderObject.markNeedsLayout会被调用(因为这可能会改变布局)。
    • 附加、分离、重新计算深度和迭代都会被重写以合并所有子节点。
    • 第一个子节点(ContainerRenderObjectMixin.firstChild)、最后一个子节点(ContainerRenderObjectMixin.lastChild)和子节点总数(ContainerRenderObjectMixin.childCount)可以通过 render object 访问。

Render object 如何管理布局?

  • Render object 将自己标记为需要布局(通过RenderObject.markNeedsLayout);这将在下一帧期间安排一次布局。
  • 当对应的 dirty 列表(PipelineOwner._nodesNeedingLayout)是非空的时候,布局由 PipelineOwner.flushLayout 触发。
  • RenderObject.layout 是以 Constraints 子类作为输入来调用的;有些子类也会结合其他 out-of-band 输入。layout 方法应用输入来产生几何数据(一般是一个 size,虽然类型是没有限制的)
    • 如果一个协议使用了 out-of-band 输入,并且这个输入发生了变化,那么受影响的 render object 必须被标记为脏(例如,一个使用其子 baseline 来执行布局的父代,每当子代的 baseline 发生变化时,就必须重复布局)。
  • 如果父节点依赖于子节点的布局,它必须将 parentUsesSize 参数传递来布局。
  • RenderObjects如果仅仅使用给定的约束来决定其 size,则将 RenderObject.sizedByParent 设置为 true,并在 RenderObject.performResize 中执行所有 layout。
  • Layout 不能依赖于给定约束以外的任何东西;这包括 render object 的位置(通常由父 render object 决定,并存储在 ParentData 中)。

Render object 是如何被合成进 layer 里的?

  • 合成是将绘制(通过 RenderObject.paint)划分为图层(Layer)然后再光栅化并上传到引擎的过程。有些render object 共享当前 layer ,而有些 render object 则引入一个或多个新 layer。
  • 一个 repaint boundary 对应一个 render object,它总是绘制到一个新的 OffsetLayer(存储在RenderObject.layer中)。这就允许以这个节点为根的子树与它的父节点分开绘制。此外,offset layer 可以被重复使用(和平移),以避免不必要的重新绘制。
    • 其他产生新 layer 的 render object 可以将根 layer 存储在 RenderObject.layer 中,以允许未来的绘制操作更新现有的 layer,而不是创建一个新的 layer。
    • 这是可以做到的,因为 Layer 保留了对任何底层 EngineLayer 的引用,而且 SceneBuilder 在创建新的 scene 时接受一个 oldLayer 参数。
  • 某些图形操作的实现方式是不同的,这取决于它们是在单 layer 内应用还是在多 layer 内应用(如剪切)。
    • 渲染对象经常会将自己的绘画与子代的绘画交错在一起。
    • 因此,为了正确应用这些图形操作,framework 必须跟踪哪些 render object 将新的 layer 引入到 scene 中。
  • needsCompositing 表示以 render object 为根的子树(如,节点及其子代)是否可能引入新的 layer。这向 PaintingContext 表明,某些操作(如剪裁)将需要以不同的方式实现。
    • 总是插入 layer 的 render object(例如一个视频)应该重写 alwaysNeedsCompositing
  • 每当 render obejct 发生改变时,应将合成位标记为脏(通过RenderObject.markNeedsCompositingBitsUpdate);添加的子节点可能会引入 layer,而删除的子节点可能会共享一个 layer。RenderObject.adopChildRenderObject.dropChild 总是调用这个方法。
    • 所有的祖先节点都被标记为脏,直到到达 repaint boundary 或具有一个具有 repaint boundary 父节点的节点。在这种情况下,所有的上游对象都已经有了正确的合成位(即 repaint boundary 状态不能改变;因此,所有的上游合成位在添加 render object 时就已经配置好了)。
  • 当相应的脏列表(PipelineOwner._nodesNeedingCompositingBitesUpdate)是非空的时候,更新由 PipelineOwner.flushCompositingBits 来执行。这个更新必须在绘制之前调用。、
  • 脏的 render object 以深度优先的方式排序,然后更新(通过RenderObject._updateCompositingBits)。这个方法对 render tree 进行深度优先的遍历,如果每个render object 的任何一个子节点需要合成(或者它是一个 repaint boundary 或者 always needs compositing),则将其标记为需要合成。
  • 如果更新了合成位,该节点也会被标记为需要重绘。

Render Object 是如何管理绘制的 ?

  • Render objects 将自己标记为需要绘制 (通过`RenderObject.markNeedsPaint); 这将在下一帧中安排一次绘制。

  • 当对应的 dirty 列表是非空的时候,将通过PipelineOwner.flushPaint进行绘制(PipelineOwner._nodesNeedingPaint)。

  • 脏的 render object 从后到前排序(按照 RenderObject.depth),然后绘制(通过PaintingContext.repaintCompositedChild)。这需要 render object 有一个关联的 layer(只有作为 repaint boundaries 的 render object 才会被添加到这个列表中)。

    • 如果一个 render object 的 layer 没有被关联(即没有被合成),绘制必须等到它被重新关联。Framework 会遍历所有祖先的 render object,以找到最近的拥有 layer 的 repaint boundaries(通过RenderObject._skippedPaintingOnLayer)。每个祖先节点都被标记为脏,以确保它最终被绘制。
  • 如果需要,会为 render object 创建一个新的 PaintingContext,并传递给RenderObject._paintWithContext

  • 该方法确保已经进行了布局,如果是,则使用给定的 context 调用 RenderObject.paint

  • RenderObject.paintBounds 提供了这个 render object 要绘制的区域大小的估计值(作为局部坐标系的Rect)。这不需要与 render object 的布局几何形状匹配。

  • RenderObject.paintBounds provides an estimate of how large a region this render object will paint (as a Rect in local coordinates). This doesn't need to match the render object's layout geometry.

  • Render object 通过 RenderObject.applyPaintTransform 应用相同的 transform 来把子节点转换到给定的矩阵(例如,如果子对象在(x,y)处被绘制,矩阵将由(x,y)转换)。 这允许框架组合一系列转换以在任何两个 render object 的坐标系之间进行映射(通过 RenderObject.getTransformTo)。 此过程允许将局部坐标映射到全局坐标,反之亦然。

Render object 如何管理 hit testing?

  • Render objects 实现了 HitTestTarget,因此可以处理传入的指针事件(通过RenderObject.handleEvent)。然而,render objects 不支持命中测试:Render objects 没有内置的机制来订阅指针。这个能力是由 RenderBox 引入的。
  • GestureBinding 从引擎接收指针事件(通过Window.onPointerDataPacket)。这些事件被排成队列(通过GestureBinding._handlePointerDataPacket) 或当事件被解锁(通过 GestureBinding.unlocked,它被 BaseBinding.lockEvents调用),就会立即被处理(通过GestureBinding._flushPointerEventQueue)。
    • Events are locked during reassembly and the initial warmup frame.
    • 事件在 reassembly 和初始化帧期间被锁定。
  • GestureBinding._handlePointerEvent 使用 hit test result table( GestureBinding._hitTests)和point router(GestureBinding._pointerRouter)依次处理队列中的事件。
    • PointerRouter实例将一个整数指针 ID 映射到一个 event handler callback (PointerRoute,一个接受PointerEvent的函数)。Router 还会获取一个变换矩阵,以从屏幕坐标映射到本地坐标。
    • Hit testing table 将整数指针 ID 映射到所有成功命中的记录中(HitTestResult)。每个 entry(HitTestEntry)记录了接收对象(HitTestEntry.target,一个 HitTestTarget)和从全局坐标映射到局部坐标所需的 transform (HitTestEntry.transform)。
    • HitTestTarget是一个可以处理指针事件的对象(通过HitTestTarget.handleEvent)。Render object 是一个HitTestTarget
  • 初始事件(PointerUpEventPointerSignalEvent)触发命中测试(通过GestureBinding.hitTest)。正在发生的事件(PointerMoveEvent)被转发到与启动事件相关联的路径。终止事件(PointerUpEventPointerCancelEvent)会删除 entry。
    • Hit test results 存储在 hit test table 中,以便可以检索与同一指针相关联的后续事件。命中测试是在 HitTestable 对象上执行的(通过 HitTestable.hitTest)。
    • GestureBinding 定义了一个默认的重写来将自己添加到 HitTestResult中;调用这个也会调用混合到绑定中的任何重写。该绑定处理事件,以使用手势领域(通过GestureBinding.handleEvent)解析手势识别器。
    • RendererBinding 提供了一个重写,首先测试 render view(通过 RenderView.hitTest,由RendererBinding调用)。这是在 render tree 中进行 hit testing 的切入点。
    • RenderView 在将自己添加到 hitTestResult 中之前,会先 test 它的 render box子代(通过RenderBox.hitTest)。
    • RenderViewRenderBox不是HitTestable,但确实提供了一个兼容的接口(由RendererBinding使用)。RenderObject默认不参与命中测试。 - HitTestable 只在 binding 中使用。所有其他的 hit testing 都依赖于实现。
  • 如果命中成功,事件会无条件地按深度优先顺序转发到HitTestResult中的每个HitTestEntry(通过GestureBinding.dispatchEvent)。GestureBinding是一个HitTestDispatcher
    • 路径上的每个 entry 都被映射到一个处理事件的 target 上(通过HitTestTarget.processEvent)。作为HitTestTarget,render object 有资格处理之指针。但是,没有机制让它们参与命中测试。
    • 事件也会通过 pointer router 转发,以支持手势识别,这是一种更有选择性的事件处理机制。

Render object 如何处理 transformations?

  • Visual transforms are implemented in RenderObject.paint. The same transform is applied logically (i.e., when hit testing, computing semantics, mapping coordinates, etc.) via RenderObject.applyPaintTransform. This method applies the child transformation to the provided matrix (providedMatrix * childTransform).
  • 视觉上的 transform 在RenderObject.paint中实现。通过RenderObject.applyPaintTransform 在逻辑上(即在命中测试、计算语义、映射坐标等时)应用 transform。该方法将子节点的 transformation 应用于给定的矩阵(providedMatrix * childTransform)。
  • RenderObject.transformTo链式地绘制从当前坐标空间映射到祖先节点的坐标空间的变换矩阵,。
  • RenderObject.transformTo chains paint transformation matrices mapping from the current coordinate space to an ancestor’s coordinate space.
    • 如果没有给定祖先节点,则 transformation 将映射到全局坐标(即,位于RenderView下方的 render object 的坐标;为了获得物理屏幕坐标,这些坐标必须通过 RenderView.applyPaintTransform 进一步变换)。

视图是如何影响渲染的?

  • RenderView 从 platform 接收一个 ViewConfiguration,它指定了屏幕的尺寸(ViewConfiguration.size)和像素深度(ViewConfiguration.devicePixelRatio)。

  • RenderView.performLayout 采用这个尺寸(RenderView.size),如果存在的话,就按照屏幕的尺寸(ViewConfiguration.size),以严格的约束条件布局 render box 子节点。

  • RenderView.paintRenderView.hitTest会委托给子代,如果存在的话;Render view 总是将自己加入到 hit test result 中。

  • 当设置配置时(通过RenderView.configuration),layer tree 的根部被一个TransformLayer(通过RenderView._updateMatricesAndCreateNewRootLayer)替换,它将物理像素转换为逻辑像素。这将所有子代 render object 与屏幕分辨率的变化隔离开来。

  • RenderView.applyPaintTransform 应用分辨率转换(RenderView._rootTransform)将逻辑全局坐标映射到物理屏幕坐标。

  • RenderView.compositeFrame 实现了渲染管道的最后阶段,将所有绘制的 layer 合成,并将生成的 scene 上传到引擎进行光栅化(通过Window.render)。

    • 如果启用了系统 UI 自定义(RenderView.automaticSystemUiAdjustment),RenderView._updateSystemChrome将查询 layer 树,以找到与屏幕顶部和底部相关联的 SystemUiOverlayStyle 实例(这是用于确定如何着色系统 UI 的机制,如状态栏或 Android 的底部导航栏)。如果找到了,适当的设置就会传达给对应 platform(使用SystemChrome platform 方法)。