[翻译] Flutter 内幕之 Compositing

693 阅读16分钟

Compositing

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

如何呈现已合成的视觉效果?

  • Scene 是合成 UI 的非透明、不可变的表示形式。每一帧,渲染管线都会生成一个 scene 来提交给 engine 做光栅化(通过 Window.render 方法)。Scene 也可以被直接光栅化为一个 Image(通过 Scene.toImage 方法)。Scene 里封装了一系列渲染操作(例如,clips,transforms,effects)和图形(例如,pictures,textures,platform views)。
  • SceneBuilder 是 framework 中创建一个 scene 的最底层机制。SceneBuilder 管理着一个存储着渲染操作的栈(通过SceneBuilder.pushClipRect, SceneBuilder.pushOpacity, SceneBuilder.pop 进行操作)及其相关的图形(通过 SceneBuilder.addPicture, SceneBuilder.addTexture, SceneBuilder.addRetained 等等)。当渲染操作应用后,SceneBuilder 生成、追踪、返回一个 EngineLayer 实例。它可能会被 framework 缓存起来以优化后续帧的绘制(比如,更新一个旧的 layer 而不是重新创建它,或者重用一个之前已经光栅化好的 layer)。完成后,通过 SceneBuilder.build 方法得到最终的 Scene
    • 绘制操作累积在一个 Picture(通过 PictureRecorder)中,该 picture 通过 SceneBuilder.addPicture 添加到 scene 中。
    • 外部纹理(由使用平台纹理存储库建立的纹理 ID 表示)通过 SceneBuilder.addTexture 添加到 scene 中。它支持“冻结”参数,以便在暂停动画时可以暂停纹理。
    • 先前经过光栅化处理的 EngineLayers 可以通过 SceneBuilder.addRetained 缓存起来。
  • Picture 是一系列绘制操作的不透明、不可变的表示。 这些操作可以直接添加到“场景”中(通过 SceneBuilder.addPicture),也可以组合到另一张图片中(通过 Canvas.drawPicture)。Pictures 也可以光栅化(通过 Picture.toImage),并提供其内存占用量的估计值。
  • PictureRecorder 是一种不透明的机制,通过该机制可以对绘制操作进行排序并将其转换为不可变的Picture。Picture recorders 通常通过 Canvas 进行管理。录制完成后(通过PictureRecorder.endRecording),将返回最终 Picture,随后 PictureRecorder(和关联的 Canvas)就会失效。

How are layers represented?

  • Layer 代表已合成的 scene 中的一个片断。Layers 按层次结构排列,每个节点都有可能影响其下面的节点(通过应用渲染操作或绘制图形)。Layers 是可变的,可以在树上自由移动,但只能使用一次。使用 SceneBuilder将每一个 layer 合成为一个 scene(通过 Layer.addToScene);添加 rootLayer 相当于合成整个树(例如,Layer 添加其子节点)。注意,整个 layer 树有变化时必须重新合成,这里没有脏状态的概念。
    • 当添加一个 layer 时,SceneBuilder 提供一个 EngineLayer 实例。它可以被存储在 Layer.engineLayer 中,以后可以用来优化合成。例如,Layer.addToScene 可能会传递 engine layer (通过 Layer._addToSceneWithRetainedRendering )而不是重新渲染来重用上一帧的光栅化纹理。另外,在执行某些渲染操作时可以指定 engine layer(即"oldLayer"),以允许将这些操作作为内联变换来实现。
    • Layers 支持渲染保留(通过 Layer.markNeedsAddToScene)。这个标志位代表着前一个 engine layer 能不能在合成过程中重用(即,对比上一帧 layer 没有任何变化)。如果是这样,保留的渲染允许整个子树替换为上一个光栅化的位图(通过 SceneBuilder.addRetained)。否则,必须重新对 layer 进行合成。一旦有 layer 被加入到树中,这个标志位就会重置(除非这个 layer 指定它自身必须一直参与合成)。
      • 首先,必须将所有 layer 添加到 scene。添加后,缓存的 engine layer 可能会用于渲染保留。其他 layer 则完全禁用渲染保留(通过 Layer.alwaysNeedsAddToScene)。通常,当某个 layer 发生变化(即通过修改属性或添加子节点)时,必须将其重新添加到 scene 中。
      • 如果一个 layer 必须被添加到 scene,则必须添加所有祖先 layer。保留的渲染允许将子树替换为缓存的位图。如果子孙节点已更改,则该位图将变为无效,因此还必须将祖先 layer 添加到 scene 中(通过 Layer.updateSubtreeNeedsAddToScene)。
        • 请注意,绘制时不会强制使用此属性;仅当 scene 已合成(通过ContainerLayer.buildScene), layer 树才会保持连通。
      • 一个 layer 的父节点接收一个新的 engine layer 时,父节点必须被重新添加到树中。
      • 通常来说,只有 container layer 使用渲染保留。
    • 元数据可以被嵌入 layer 中以便后面检索(通过 Layer.findLayer.findAll)。
  • ContainerLayer 管理着一个子节点列表,并把它们按顺序插入已合成的 scene。Root layer (TransformLayer)是 ContainerLayer 的子类。因此,ContainerLayer 负责组合整个 scene(通过 ContainerLayer.buildScene)。这涉及使保留的渲染状态保持一致(通过 ContainerLayer.updateSubtreeNeedsAddToScene),将所有子节点添加到 scene(通过ContainerLayer.addChildrenToScene),以及将 container 标记为不再需要添加到 scene(通过 Layer._needsAddToScene)。ContainerLayer 实现了许多对子节点的操作(例如,ContainerLayer.appendContainerLayer.removeAllChildren),并且大多数这些操作会对子节点进行遍历(例如,ContainerLayer.find;注意,这是倒叙遍历,即 lastLayer 优先)。
    • ContainerLayer 将为其子代使用保留的渲染,只要它们没有偏移(即位于 layer 的原点)即可。这是 layer 树中大多数保留的渲染的入口。
    • ContainerLayer.applyTransform 转换给定的矩阵以反映在合成过程如何变换给定的子节点。此方法假定所有子节点都位于原点;因此,必须删除所有 layer 偏移并将其转换为一个 transformation(通过SceneBuilder.pushTransformSceneBuilder.pushOffset)。否则,所得矩阵将不准确。
  • OffsetLayerContainerLayer 的子类,它支持高效的重绘(即 repaint boundaries)。被定义作 repaint boundaries 的 Render objects 对应于 Layer 树中的 OffsetLayers。如果 render object 不需要重绘或者只是做了简单的 transform,则可以重用现有的 offset layer。这避免整颗子树被重绘。
    • OffsetLayer 将任何偏移量作为顶级的 transform,这样的后代节点可以在原点进行合成,且可以进行渲染保留。
    • OffsetLayer.toImage 通过 Scene.toImage 将光栅化以此 layer 为根的子树。得到的 image,由原始像素数据组成,且以指定像素比率(忽略设备的屏幕像素密度)渲染为矩形。
  • TransformLayer 是一个 ContainerLayer 的子类,可应用任意 transform;它也恰好(通常来说)是 RenderView 对应的 root layer。任何 layer 的偏移(通过 Layer.addToScene)或显示偏移(通过 OffsetLayer.offset)会被删除并转换成一个 transformation 来确保与 TransformLayer.applyTransform 的表现一致。
  • PhysicalModelLayer 是将物理光影效果集成到已合成 scene 中的核心。这个类将 elevation,clip region,background color 和 shadow color 组合起来,在它的子节点背面投射一个 shadow(通过 SceneBuilder.pushPhysicalShape)。
  • AnnotatedRegionLayer 将元数据合并到一颗给定固定边界的 layer 树中。一个确定了矩形左上角的偏移值(默认到原点)和一个 size 表示它的尺寸。Hit testing 由 AnnotatedRegionLayer.findAnnotatedREgionLayer.findAll 来执行。在树中较深或在子列表中后入的节点优先级更高。
  • FollowerLayerLeaderLayer 支持高效的转换连接(transform linking)(例如,一个 layer 看上去可以与另一 layer 一起滚动)。这些 layers 通过 LayerLink 通讯,将 leader 的 transform (包括 layerOffset)传递给 follwer。在 follower 被渲染的地方,会使用这个 transform 来呈现。CompositedTransformFollowerCompositedTransformTarget widgets 创建和管理这些 layers。
  • 有各种各样的叶子类、内部类组成了 layer 树。
    • 叶节点通常直接继承 LayerPictureLayer 描述了一张会被加入到 scene 的 Picture。类似的, TextureLayer 描述了一个纹理(通过 ID)和一个边界矩形。PlatformViewLayer 几乎相同,它只是应用视图而不是纹理。
    • 非叶子节点通常继承 ContainerLayer 或它的子类。OffsetLayer 是实现高效重绘的关键。ClipPathLayer, ClipRectLayer, ColorFilterLayer, OpacityLayer等通过推入 scene builder 状态,添加所有子节点(通过 ContainerLayer.addChildrenToScene,这可能会利用保留的渲染)来应用相应的效果,然后弹出该状态。
  • EngineLayer 及其各种特殊实现类 (例如, OpacityEngineLayer, OffsetEngineLayer) 代表着后端 layer 的不透明句柄。 SceneBuilder 为其支持的每个操作生成适当的句柄。然后,这些可用于启用渲染保留和内联更新。

Compositing 有哪些组成部分?

  • Canvas 提供了在单个 PictureRecorder 上下文中执行绘图操作的接口。也就是说,所有由 Canvas 执行的绘图都被限制为一个单独的 Picture (和 PictureLayer)。PaintingContext 支持跨越多个 layer 的渲染和绘图操作,PaintingContext 根据渲染树的合成需求管理 Canvas 生命周期。

  • PaintingContext.repaintCompositedChild 是一个将 render object 绘制到自己的 layer 中的静态方法。 一旦为了绘制而把节点标记为脏,则会在渲染管道的绘制阶段对其进行处理(通过 PipelineOwner.flushPaint)。这将对所有脏 render object 执行深度优先遍历。请注意,实际上只有 repaint boundaries 被标记为脏;所有其他节点会便利其祖先节点找到最近的 repaint boundary,然后将其标记为脏。检索或创建一个OffsetLayer 来作为子树的容器 layer。创建一个新的 PaintingContext来进行实际的绘制(通过 PaintingContext._paintWithContext)。

  • Layers 可以从渲染管道上挂载或分离。如果在带有已分离 layer 中的 render object 被发现是脏的(通过 PipelineOwner.flushPaint),则遍历祖先节点来找到最近的在一个已挂载(或未创建的)layer 中的 repaint boundary。这个节点会被标记为脏,以便于在重挂载的时候按预期进行绘制(via RenderObject._skippedPaintingOnLayer)。

  • PaintingContext 在 paint 方法和底层 canvas 之间提供了一个中间层,让 render object 适应不断变化的合成需求。也就是说,render object 在某些情况下可能会引入新的 layer,而在其他情况下可能会重用现有的 layer。当这种变化发生时,任何祖先节点都需要适配子孙节点引入新 layer 的可能情况(通过 RenderObject.needsCompositing); 特别是,这需要引入他们自己的 layer来实现某些特殊操作(例如,clip)。PaintingContext 管理该过程,并提供一些封装好的方法(例如,PaintingContext.pushClipPath)。PaintingContext 还可以确保引入 repaint boundary 的子节点被组合到新的 layer 中(通过 PaintingContext.paintChild)。最后, PaintingContext 管理着提供给CanvasPictureRecorder , 每当 recording 完成时添加 picture layers(即 PictureLayer)。

    • 使用ContainerLayer 子类初始化PaintingContext ,以作为通过该 context 生存的 layer 树的根节点。绘制永远不会在此 layer 中进行,而是由新的 PictureLayer 实例捕获,该实例随绘制进行而添加。

    • PaintingContext 可能会因为合成而管理多个 canvas。每个 Canvas 在其生命周期中都与一个 PictureRecorder 和一个 PictureLayer (即当前 layer)相关联。除非必须添加新 layer(例如,为了实现合成效果或遇到 repaint boundary),所有的 render object 会使用相同的 canvas (和 PictureRecorder)。

      • Recording 在第一次访问 Canvas(通过 PaintingContext.canvas)时开始。 这将生成一个新的 picture layer, picture recorder, 和 canvas 实例;一旦创建,picture layer 就会被添加到 container layer。
      • 当直接或间接引入新的 layer 时,Recording 结束;此时,picture 会被存储在当前的 layer 中(通过 PictureRecorder.endRecording),并且清除 canvas 和当前 layer。下次给子节点绘制时,将创建一个新的 canvas,picture recorder 和 picture layer。
      • 使用新的由相应的 OffsetLayer 初始化的 PaintingContext 来绘制已合成的子节点。如果一个已合成的子节点实际上并不需要重绘制(例如,它仅仅是应用了 transform), 那 OffsetLayer可以以一个新的偏移量来重用。
    • PaintingContext 提供了一个简单的接口。如果需要合成,则新的 layer 会被自动推入以实现所需的效果(通过 PaintingContext.pushLayer)。否则,可以直接使用 Canvas 来实现效果(例如,通过 PictureRecorder)。

      • 推入 layer 将结束当前的 recording,添加新的 layer 到 conatiner layer。为这个新的 layer(如果存在,则该 layer 的子节点可能会因为过期而被删除)创建一个 PaintingContext 。如果创建了,则使用新的 conetxt 调用绘制方法;任何绘制都会包含在新的 layer 中。随后使用原始PaintingContext 进行的操作,都将导致新的 canvas,layer 和 picture recorder 被创建。
  • ClipContextPaintingContext 的基类。它的主要用途是在不引入新 layer 的情况下为剪裁提供支持(例如,适用于不需要合成的 render object 的方法)。


"needs compositing" 标志位做了什么事情?

  • 合成描述了在光栅化过程中引擎将 layer 组合在一起的过程。在 framework 的上下文中,合成通常指的是分配与绘制相关的 render object 到 layer。需要合成并不意味着 render object 将被分配到它自己的 layer;相反,它表明某些效果必须通过引入新的 layer 来实现,而不是修改当前的 layer(例如,应用不透明度或者裁剪)。例如,如果父节点先建立一个 clip 然后绘制其子节点,则必须决定如何实现这个 clip: (1)作为当前 layer 的一部分(即 Canvas.clipPath),或者 (2) 通过推入一个 ContainerLayer (即ClipPathLayer))。"needs compositing" 位管理着这些信息。
    • 通常来说,这是因为这个节点可能最终绘制了一个推入了新 layer 的子节点(例如,因为它是一个 repaint boundary)。因此,任何可能产生非局部后果的绘制都必须以跨 layer 工作的方式来实现(即通过合成)。
    • 请注意,这不同于将 render object 标记为 repaint boundary。这样做会在绘制子节点(通过 PaintingContext.paintChild)时引入一个新的 layer。这使 repaint boundary 的所有祖先节点的 "needs compositing" 位失效。这可能会导致某些祖先节点在绘制时引入新的 layer,但前提是他们使用了任何非局部操作。

"needs compositing" 标志位如何更新?

  • RenderObject.markNeedsCompositingBitsUpdate 把 render object 标记为需要更新 "needs compositing" 位(通过PipelineOwner._nodesNeedingCompositingBitsUpdate)。如果节点被标记为脏节点,则其所有祖先也被标记为脏节点。作为一种优化,如果当前节点或当前节点的父级是 repaint boundary(或父节点已被标记为脏),则可以中断此遍历。

    • 如果节点的合成标志位需要更新,则可能会引入一个新 layer(即,需要合成)。如果是这样,所有祖先节点也需要合成,因为他们可能会绘制引入了新 layer 的后代。如上所述,某些非局部效果将需要通过合成来实现。
    • 由于所有祖先节点都必须先标记为需要合成,所以遍历可能在 repaint boundaries 处停止。
    • 添加或删除子节点(通过 RenderObject.adoptChildRenderObject.dropChild)可能会更改 render 树的合成要求。同样地,更改 RenderObject.alwaysNeedsCompositing 位将更新 "needs composting“ 位。
  • 在渲染过程中,PipelineOwner.flushCompositingBits 更新所有脏的合成位(通过 RenderObject._updateCompositingBits)。给定节点是否需要合成,取决于(1)它是 repaint boundary(通过 RenderObject.isRepaintBoundary)。(2)它被标记为始终需要合成(通过 RenderObject.alwaysNeedsCompositing)。(3)它的任何后代节点需要合成。

    • 这个过程将遍历所有被标记为脏的后代节点,根据上述策略更新他们的 "needs compositing" 标志位。注意,这通常只会遍历将合成标识位设为脏时标记的路径。
    • 如果更改了节点的合成标记位,它会被标记为脏以进行绘制(因为绘制现在可能需要额外的合成)。
  • 如果 render object 始终需要引入一个 layer,它应该切换 alwaysNeedsCompositing为 true。如果这个改变发生了(除了更改子节点时,这会自动调用),markNeedsCompositingBitsUpdate 将被调用。


什么是 repaint boundaries?

  • 某些 render object 在 render 树里引入了 repaint boundaries (通过 RenderObject.isRepaintBoundary)。这些 render object 总是被绘制到一个新的 layer 中,从而使它们与其父节点分开绘制。这有效的解耦以 repaint boundaries 为根的子树与先前绘制的父节点。
    • 当部分 UI 大部分时间保持静态并且部分 UI 频繁更新时,这会很有用。Repaint boundaries 有助于避免重绘 UI 的静态部分。
    • 作为 repaint boundary 的 render object 与 OffsetLayer 相关联,OffsetLayer 是一种特殊类型的 layer,可以更新其位置而无需重新渲染。
  • 作为 repaint boundaries 的 Render objects 在绘制过程中的处理方式有所不同 (通过 PaintingContext.paintChild)。
    • 如果子节点不需要绘制,则跳过整个子树。对应的 layer 将会被更新以适配新的偏移量(通过 PaintContext.paintChild),并将其添加到父 layer。
    • 如果子节点需要绘制,则当前的 offset layer 会被清理或创建(通过PaintingContext._repaintCompositedChild),然后将其用于绘制(通过 RenderObject._paintWithContext)。
    • 否则,绘制将通过 RenderObject._paintWithContext 进行。
  • 由于 repaint boundaries 失踪推入新的 layer,因此必须将所有祖先节点标记为需要合成。任何需要非局部绘制效果的节点也需要引入新的 layer。

可以推入新 layer 的子节点是如何绘制的?

  • PaintingContext.paintChild 通过以下方式管理此过程:(1)结束所有正在进行的 recording。(2)为子节点创建一个新的 layer。(3)使用新的 painting context 在新的 layer 中绘制该子节点。(4)将该 layer 添加到当前的 layer 树。(5)为将来会使用原始 painting context 的绘制创建新的 canvas 和 layer。
  • PaintingContext 中的方法会询问合成标志位,以决定是通过插入 layer 还是更改 canvas (例如,clipping)来实现行为。

Layer 如何使绘制更高效?

  • 所有 render object 都可能与 ContainerLayer 相关联(通过RenderObject.layer )。如果已定义了此 layer,则这是用于绘制 render object 的最后一个 layer(如果使用了多个 layer,则这是最底层的)。
    • Repaint boundaries 会被自动分配一个 OffsetLayer (通过 PaintingContext._repaintCompositedChild)。绘制时,所有其他 layer 都必须专门设置此字段。
    • 不直接推入 layer 的 render object 必须将其设置为 null。
  • ContainerLayer 跟踪任何关联的 EngineLayer (通过 ContainerLayer.engineLayer)。某些操作可以使用此信息来更新 layer 而不是重新创建。
    • 某些方法接收 oldLayer 参数以启用此优化。
    • 已缓存的 engine layers 可以直接被重用(通过 SceneBuilder.addRetained));这是渲染保留。

Widget 树如何手动光栅化?

  • 可以通过 OffsetLayer.toImage 把一个 layer 光栅为一个图像。一种获取整个 UI 图像的便捷方法时在 RenderView 自己的 Layer 上使用此方法。要获得 UI 的局部图像,可以插入适当的 repaint boundary,并对 layer 进行同样的操作。

如何管理 Clips, transforms 和其他效果?

  • 这里有两种路径。不需要合成的 layer 可通过 Canvas 在自己的 rendering context 上处理 transforms 和 clips(因此,效果仅限于该单个 layer)。那些需要合成的 layer 会通过 PaintingContext 将这些效果作为特殊的 ContainerLayers 引入。
    • 包括各种有用的 ContaienrLayersAnnotatedRegionLayer (用于在 layer 的各部分存储元数据),BackDropFilterLayer(用于背景模糊或滤镜),OpacityLayer(允许改变不透明度)和 OffsetLayer(repaint boundary 优化机制的关键)。
  • Canvas.saveLayer / Canvas.restore 允许对绘制命令进行分组,以便一次将诸如 混合(blending)、颜色过滤(color filtering) 和抗锯齿(anti-aliasing)之类的效果应用于该组,而不是进行增量的堆叠(这可能导致无效的渲染)。

外接纹理如何合成?

  • 纹理完全由 engine 管理,并由简单的纹理 ID 引用。 描述纹理的 layer 与此 ID 关联,并在合成后与后端纹理集成。As such, these are painted out-of-band by the engine.
  • 纹理 layers 通过 TextureBox 与 render 树集成在一起。 这是一个跟踪纹理 ID 并绘制相应 TextureLayer 的 render box。 在布局期间,此 box 将展开以满足传入的约束。