Flutter绘制流程及Layer

650 阅读12分钟

Flutter绘制流程

1⃣️.请求重绘(markNeedsPaint

RenderObject需要绘制时需要调用markNeedsPaint,该方法会从当前节点一直往上查找,直到找到一个边界节点(RepaintBoundary ),然后会将该节点加入到PipelineOwner#_nodesNeedingPaint中,随后请求新的一帧owner!.requestVisualUpdate();

  void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      if (owner != null) {
        owner!._nodesNeedingPaint.add(this);
        owner!.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      final RenderObject parent = this.parent! as RenderObject;
      parent.markNeedsPaint();
    } else {
      if (owner != null)
        owner!.requestVisualUpdate();
    }
  }

2⃣️.新帧到来(DrawFrame

新的一帧到来,在RendererBinding#drawFrame中调用pipelineOwner.flushPaint(),该方法在将边界节点按照深度优先的顺序重排后,调用PaintingContext#repaintCompositedChild(node),最终调用到PaintingContext#_repaintCompositedChild的静态方法,该方法做了下面几件事:

  1. 判断当前边界节点是否有OffsetLayer
    • 如果没有,创建新的OffsetLayer并赋值给边界节点
    • 如果有,清空当前边界节点layer上的所有子layer
  2. 创建PaintingContext并绑定当前layer
    • 从这里可以看出一个边界节点对应了一个PaintingContext
    • 此时的canvas还并没有创建
  3. 调用child._paintWithContext()开始绘制节点
    • 从这里开始就直接进入3.开始绘制(paint)
  4. 调用childContext.stopRecordingIfNeeded()
    • 绘制结束,对应4.绘制结束

3⃣️.开始绘制(paint

在第2步中调用child._paintWithContext()后,会调用paint(context, offset)直接开始child节点的绘制,该方法会接受PaintingContext作为参数。

RenderObject的绘制可分为以下几种:

  • 容器类RenderObject,比如Center,它的RenderObject是继承自RenderShiftedBox ,而它的绘制方法只是调用了context.paintChild()绘制child
  • 容器类+绘制类RenderObject,比如ColorBox,它的RenderObject_RenderColoredBox,绘制方法会先通过context.canvas.drawRect()绘制自身,再通过context.paintChild()绘制child
  • 绘制类RenderObject,比如RawImage,它的RenderObjectRenderImage,它的绘制方法只是绘制了它自身(另外,像这样的绘制RenderObject一定是叶子结点)

context.paintChild

请查看PaintingContext篇⬇️

paint()

该方法由RenderObject子类自己实现;

不涉及到变换的绘制只需要通过Canvas绘制即可;如果涉及到变换,有两种实现方式:

  1. 使用合成Layer的方式
  2. 使用Canvas的方式

4⃣️.绘制结束(stopRecordingIfNeeded

再回到第2步中,在当前边界节点的所有子节点完成绘制后,就表示当前边界节点绘制结束了,会调用context.stopRecordingIfNeeded()方法,保存绘制内容,并且置空PictureLayer、canvas....

void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

到此,到生成Layer位置的绘制流程就完成了,当然后面还有添加到Sence的场景,这里不讨论。

PaintingContext

绘制上下文,直接持有画布(Canvas)和PictureLayer;

一个边界节点对应一个PaintingContext,一个PaintingContext对应一个Canvas、PictureLayer;

作用:

  • 持有Canvas
  • 持有PictureLayer
  • 合成Layer

合成Layer的概念

创建一个新的ContainerLayer,然后将ContainerLayer传递给新节点,这样后代节点的Layer必然属于ContainerLayer(那么给这个 ContainerLayer 做变换就会对其全部的子孙节点生效)的过程叫做Layer的合成

_repaintCompositedChild

静态方法,重绘并合成子Layer,在绘制流程第2⃣️步的方法pipelineOwner.flushPaint()中调用;

  • 该方法会直接对childLayer进行合成;
  • 随后开始调用_paintWithContext对该边界节点的child进行绘制
static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext? childContext,
  }) {
    OffsetLayer? childLayer = child._layerHandle.layer as OffsetLayer?;
    if (childLayer == null) {
      final OffsetLayer layer = OffsetLayer();
      child._layerHandle.layer = childLayer = layer;
    } else {
      childLayer.removeAllChildren();
    }
   
    childContext ??= PaintingContext(childLayer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);

  
    childContext.stopRecordingIfNeeded();
  }

_paintWithContext

这个方法就非常简单了,调用RenderObject的paint()方法,由子类自行实行,对于需要绘制孩子节点的RenderObject,需要调用paintChild方法

void _paintWithContext(PaintingContext context, Offset offset) {
  if (_needsLayout)
    return;
  RenderObject? debugLastActivePaint;
  _needsPaint = false;
  try {
    paint(context, offset);
  } catch (e, stack) {
    _debugReportException('paint', e, stack);
  }
}

paintChild

该方法是绘制流程第2⃣️步的由父节点绘制子节点时调用的context.paintChild实现逻辑如下:

void paintChild(RenderObject child, Offset offset) {
  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);
  }
}
  • 如果child是边界节点(RepaintBoundary),则会重新开始合成,_compositeChild()最终还是调用的repaintCompositedChild()方法
    • 会先调用context.stopRecordingIfNeeded(),该方法会结束当前context的绘制,并且保存绘制结果,再调_cpmpositeChild()生成的新的OffsetLayer
    • 从这里可以看到,只要是RepaintBoundary,就一定会新增一层OffsetLayer
    • 从第2步的边界节点开始,一直往下传递PaintingContext,如果子节点没有遇到RepaintBoundary,就会一直复用当前PaintingContext的PictureLayer,直到出现RepaintBoundary,会立即停止当前的context的绘制,重新开启新的OffsetLayer,并生成新的PaintingContext(这个过程就是合成Layer);这就引申了一个新问题,原本在Flex控件内多个孩子组件是共用一个PictureLayer的,但是如果在中间的某个孩子包裹一层RepaintBoundary会怎么样?
  • 如果非边界节点,直接绘制⬇️

context.canvas,_startRecording

在创建PaintingContext时并不会立即创建canvas,而是在子节点第一次需要绘制时创建的context.canvas.drawxxx,此时创建canvas并且创建PictureLayer,并且添加到ContainerLayer上,这样就可以记录接下来的context.canvas的绘制内容了;

@override
Canvas get canvas {
  if (_canvas == null)
    _startRecording();
  return _canvas!;
}

void _startRecording() {
  _currentLayer = PictureLayer(estimatedBounds);
  _recorder = ui.PictureRecorder();
  _canvas = Canvas(_recorder!);
  _containerLayer.append(_currentLayer!);
}

比如_RenderColoredBox#paint(),调用context.canvas.drawRect绘制自身,调用context.paintChild绘制子节点⬇️:

@override
void paint(PaintingContext context, Offset offset) {
  if (size > Size.zero) {
    context.canvas.drawRect(offset & size, Paint()..color = color);
  }
  if (child != null) {
    context.paintChild(child!, offset);
  }
}

stopRecordingIfNeeded

在上面的_repaintCompositedChild方法最后调用了这个方法,从字面意思就能理解这个方法的意义了,结束当前的绘制记录,并且保存绘制内容。

该方法还会清空当前canvas和PictureLayer;

@protected
@mustCallSuper
void stopRecordingIfNeeded() {
  if (!_isRecording)
    return;
  _currentLayer!.picture = _recorder!.endRecording();
  _currentLayer = null;
  _recorder = null;
  _canvas = null;
}

到这里,一次标准重绘合成Layer的流程就结束了;

而对于直接合成方法来说一次标准的合成是这样的⬇️,可以查看PaintingContext的添加Layer合成Layer的源码,无一例外的都是这两个方法一起调用的:

stopRecordingIfNeeded();
appendLayer(childLayer);

pushLayer

除了repaintCompositedChild的合成方式,PaintingContext还提供了直接合成的方法⬇️:

image.png

这些方法会除了pushOpacity()以外,会根据needsCompositing参数选择是合成Layer的方式还是直接通过canvas的API进行绘制; 如果是needsCompositing则会调用pushLayer进行合成⬇️:

void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect? childPaintBounds }) {
  if (childLayer.hasChildren) {
    childLayer.removeAllChildren();
  }
  stopRecordingIfNeeded();
  appendLayer(childLayer);
  final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);

  painter(childContext, offset);
  childContext.stopRecordingIfNeeded();
}

关于needsCompositing是什么,请查看Layer篇的关于什么时候需要合成Layer

Layer

分类

容器类Layer

继承自ContainerLayer的一系列Layer;

容器Layer可以添加任意Layer;

作用

  • 容器类Layer可以绘制结构组成一棵树
  • 可以对多个Layer整体应用一些变换效果

绘制类Layer

PictureLayer,保存绘制产物的Layer,可以直接承载(或关联)绘制结果的 Layer 称为绘制类 Layer;

Canvas绘制的结果是Picture对象,PictureLayer保存了Picture对象,比如下面代码⬇️:

//1.创建绘制记录器及Canvas
  PictureRecorder recorder = PictureRecorder();
  Canvas canvas = Canvas(recorder);
  //2.在指定位置区域绘制
  var rect = const Rect.fromLTWH(30, 200, 300, 300);
  drawChessboard(canvas, rect);//画棋盘
  drawPieces(canvas, rect);//画棋子
  //3.创建layer,将绘制的产物保存在layer中
  PictureLayer pictureLayer = PictureLayer(rect);
  //recorder.enRecording()获取绘制产物
  pictureLayer.picture = recorder.endRecording();
  OffsetLayer rootLayer = OffsetLayer();
  rootLayer.append(pictureLayer);
  //4.上屏,将绘制的内容显示在屏幕上
  final SceneBuilder builder = SceneBuilder();
  final Scene scene = rootLayer.buildScene(builder);
  window.render(scene);

Layer的价值

类似ps,通过添加layer(图层)实现效果;

  • 通过 OpacityLayer 可以调整某一层的透明度
  • 通过 ColorFilterLayer 可以调整某一层的滤色效果
  • 通过 ImageFilterLayer 可以调整某一层的图片滤色效果
  • 通过 BackdropFilterLayer 可以调整某一层的图片滤色效果
  • 通过 ShaderMaskLayer 可以调整某一层的着色遮罩效果
  • 通过 TransformLayer 可以调整某一层的变换效果
  • 通过 ClipXXXLayer 可以调整某一层的裁剪效果
  • ....

变换效果的选择

两种选择方式:

  • 使用ContainerLayer的子类实现
  • 使用Canvas的API直接进行变换

第一种会造成Layer的合成,但是会对所有的子Layer生效; 第二种不会造成Layer的合成,但是只会对当前的Layer生效;

Layer合成(Compositing)

创建一个新的ContainerLayer,然后将ContainerLayer传递给新节点,这样后代节点的Layer必然属于ContainerLayer(那么给这个 ContainerLayer 做变换就会对其全部的子孙节点生效)的过程叫做Layer的合成;

除了前面说的被动合成的过程(遇边界节点就合成),PaintingContext还有可以主动合成的方法⬇️

Layer的名称PaintingContext对应的方法Widget
ClipPathLayerpushClipPathClipPath
OpacityLayerpushOpacityOpacity
ClipRRectLayerpushClipRRectClipRRect
ClipRectLayerpushClipRectClipRect
TransformLayerpushTransformRotatedBox、Transform

关于什么时候需要合成Layer

book.flutterchina.club/chapter14/c…

简而言之:当后代节点会向 layer 树中添加新的绘制类Layer时,则父级的变换类组件中就需要合成 Layer

why?

在绘制流程中第2⃣️步,新帧到来方法drawFrame()中,执行绘制的方法是flushPaint(),在这之前还有个方法是flushCompositingBits(),这个方法维护了RenderObject一个needsCompositing的标志位;

  • 十分有趣的是祖先的needsCompositing是根据孩子的needsCompositing决定,结合上面考虑一下为什么?
  • isRepaintBoundaryalwaysNeedsCompositing决定了needsCompositing的值,isRepaintBoundary前面说过很多次了,边界节点需要合成;alwaysNeedsCompositing是一个只有get方法的值,如果需要更改只能用子类重写(eg:_ColorFilterRenderObject)
void _updateCompositingBits() {
  if (!_needsCompositingBitsUpdate)
    return;
  final bool oldNeedsCompositing = _needsCompositing;
  _needsCompositing = false;
  visitChildren((RenderObject child) {
    child._updateCompositingBits();
    if (child.needsCompositing)
      _needsCompositing = true;
  });
  if (isRepaintBoundary || alwaysNeedsCompositing)
    _needsCompositing = true;
  if (oldNeedsCompositing != _needsCompositing)
    markNeedsPaint();
  _needsCompositingBitsUpdate = false;
}

在回答这个问题前先看下Clip剪裁组件的绘制方法(简化后):

if (needsCompositing) {
  context.pushLayer(
    OpacityLayer(alpha: disabledColorAlpha),
    (PaintingContext context, Offset offset) {
      context.paintChild(child, _boxParentData(child).offset + offset);
    },
    offset,
  );
} else {
  final Rect childRect = _boxRect(child).shift(offset);
  context.canvas.saveLayer(childRect.inflate(20.0), Paint()..color = _disabledColor);
  context.paintChild(child, _boxParentData(child).offset + offset);
  context.canvas.restore();
}

很显然,Clip组件会通过needsCompositing决定采用那种方式做裁剪;前面说了context.pushLayer一定会合成一次Layer(可以查看源码,这里为了篇幅不放代码了);

现在回答一下上面的问题:为什么子节点要决定祖先节点的needsCompositing?

答:因为当祖先节点是变换组件时(ClipXXXLayer、TransformLayer...),如果子组件需要合成而变换组件不合成的话就会存在变换效果的隔离(因为每个PictureLayer是相互隔离的),所以如果子节点需要合成,那么祖先节点变换组件也需要合成,从而保证变换效果的正常;

用一个Clip组件套和不套RepaintBoundary测试一下,可以用张老师的代码,明显看到的是,在没有包裹RepaintBoundary的情况下,Layer树并没有ClipPathLayer,包裹了RepaintBoundary后Layer树出现了ClipPathLayer,所以关于Clip组件的性能问题,只要注意这个细节,并不会有问题;

另外对于Opacity组件也是类似,只有当child为空和alpha=0时才不会合成Layer,否则一定合成Layer,但是就像张老师所说的不要人云亦云,以为使用这些组件就会造成性能问题,Flutter设计了这种方案就一定是可以使用的,只不过使用的时候要注意;

Flex组件内的PictureLayer

为了更好的说明PaintingContext是如何工作的,这里使用Flex组件做个测试;

为什么要拿Flex组件测试,上面说了在遇到一个RepaintBoundary后会立马停止当前Layer的绘制,重新合成一个Layer,并且在当前边界节点绘制完成后结束;

不包裹RepaintBoundary

测试开始,我们首先用一个Row包裹三个子组件,并且打印他们的Layer树⬇️:

Row(
  children: [
    Box(),
    Box(),
    Box(),
  ],
)

Layer树⬇️:

flutter: TransformLayer#3cb57
flutter:   owner: RenderView#cdfb2
flutter:   creator: [root]
flutter:   engine layer: TransformEngineLayer#4b879
flutter:   handles: 1
flutter:   offset: Offset(0.0, 0.0)
flutter:   transform:
flutter:     [0] 1.0,0.0,0.0,0.0
flutter:     [1] 0.0,1.0,0.0,0.0
flutter:     [2] 0.0,0.0,1.0,0.0
flutter:     [3] 0.0,0.0,0.0,1.0
flutter:  
flutter:  └─child 1: PictureLayer#7019b
flutter:      handles: 1
flutter:      paint bounds: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
flutter:      picture: Picture#5c715
flutter:      raster cache hints: isComplex = false, willChange = false
flutter: 

可以看到并没有合成多余的Layer;

包裹RepaintBoundary

现在把中间的Box组件用RepaintBoundary包裹下⬇️:

Row(
  children: [
    Box(),
    RepaintBoundary(
      child: Box(),
    ),
    Box(),
  ],
),

再打印Layer树⬇️:

flutter: TransformLayer#001ac
flutter:   owner: RenderView#a4019
flutter:   creator: [root]
flutter:   engine layer: TransformEngineLayer#3c92e
flutter:   handles: 1
flutter:   offset: Offset(0.0, 0.0)
flutter:   transform:
flutter:     [0] 1.0,0.0,0.0,0.0
flutter:     [1] 0.0,1.0,0.0,0.0
flutter:     [2] 0.0,0.0,1.0,0.0
flutter:     [3] 0.0,0.0,0.0,1.0
flutter:  
flutter:  ├─child 1: PictureLayer#07366
flutter:     handles: 1
flutter:     paint bounds: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
flutter:     picture: Picture#4d597
flutter:     raster cache hints: isComplex = false, willChange = false
flutter:  
flutter:  ├─child 2: OffsetLayer#0c657
flutter:    creator: RepaintBoundary  Row  Directionality  [root]
flutter:    engine layer: OffsetEngineLayer#da762
flutter:    handles: 2
flutter:    offset: Offset(96.0, 237.0)
flutter:   
flutter:   └─child 1: PictureLayer#3ce47
flutter:       handles: 1
flutter:       paint bounds: Rect.fromLTRB(0.0, 0.0, 96.0, 126.0)
flutter:       picture: Picture#984d9
flutter:       raster cache hints: isComplex = false, willChange = false
flutter:  
flutter:  └─child 3: PictureLayer#c0670
flutter:      handles: 1
flutter:      paint bounds: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
flutter:      picture: Picture#f652f
flutter:      raster cache hints: isComplex = false, willChange = false

可以看到由于增加了RepaintBoundary,所以新增了一个OffsetLayer,在RepaintBoundary绘制完后又新增了一个PictureLayer绘制第三个孩子组件,这里为什么不能复用第一个?,先看下代码的实现,是如何新增的⬇️:

RenderFlex混入了RenderBoxContainerDefaultsMixin,所以绘制方法⬇️:

void defaultPaint(PaintingContext context, Offset offset) {
  ChildType? child = firstChild;
  while (child != null) {
    final ParentDataType childParentData = child.parentData! as ParentDataType;
    context.paintChild(child, childParentData.offset + offset);
    child = childParentData.nextSibling;
  }
}

可以看到遍历了子节点并且调用PaintingContext#paintChild方法,再放一下这个方法的代码:

void paintChild(RenderObject child, Offset offset) {
  if (child.isRepaintBoundary) {
    stopRecordingIfNeeded();
    _compositeChild(child, offset);
  } else {
    child._paintWithContext(this, offset);
  }
}
  1. 首先PaintingContext创建canvas1和PictureLayer1并且绘制记录child1,绘制完成后,开始绘制child2
  2. 发现child2是RepaintBoundary,PaintingContext需要对其重新合成,所以结束当前的PictureLayer1,开始合成新的Layer(OffsetLayer2),继续绘制child2,child2绘制完后,PaintingContext也结束当前OffsetLayer2的PictureLayer,开始绘制child3
  3. 调用context.canvas开始绘制child3,发现canvas是空的(因为前面stopRecordingIfNeeded置空了之前PictureLayer和canvas),所以会创建新的canvas和PictureLayer,形成了PictureLayer3

看到这里不免有疑问,为什么第3步还要生成一个新的PictureLayer3,直接复用PictureLayer1部更好吗? 在这个文章里做了很好的回答,简单概述一下就是如果这个控件是Stack,如果复用PictureLayer,就会造成Child3被Child2遮住;

最后

关于Layer的下一步还有Scene 的渲染,这里就不详细的说了,下面的文章张老师讲的很详细,之前说的Clip组件和Opacity性能问题归根结底就是ContainerLayer添加的问题,通过打印我们也得知ContainerLayer最后在addToScene流程的时候会生成一个engineLayer,这个engineLayer和我们这里的Layer是不同的概念,这是要传给Flutter engine渲染的东西,所以减少它的生成自然会提高性能。但是平常的开发是完全不太需要关心这个的。写这篇文章的目的是为了加深对Flutter绘制渲染流程的理解。

这篇文章写的很糙,更多的是自己做笔记,不如下面这两个文章详细⬇️

#参考资料

book.flutterchina.club/chapter14/c…

juejin.cn/book/708413…