绘制相关FlutterDart

原文传送门:

上一篇文章介绍了绘制的启动与布局的计算过程,这里接着绘制流程CompositingBitsflushPaintcompositeFrame继续分析。

绘制原理

在开始探索绘制流程之前,我们先看看不使用Flutter FrameworkWidget时,如何渲染出一个图形

import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
  // runApp(MyApp());
  // 一、创建第一个正方形
  // 使用PictureRecorder创建一个画板
  PictureRecorder recorder = PictureRecorder();
  Canvas canvas = Canvas(recorder);

  // canvas绘制
  // 从100,100坐标开始绘制
  Offset offset = Offset(300, 300);
  // 绘制区域是100x100的区域
  Size size = Size(300, 300);
  canvas.drawRect(offset & size, Paint()..color = Colors.red);

  // 通过recorder.endRecording结束节点绘制并返回一个Picture
  Picture picture = recorder.endRecording();

  // 二、创建第二个圆形
  PictureRecorder recorder1 = PictureRecorder();
  Canvas canvas1 = Canvas(recorder1);
  Offset offset1 = Offset(0, 0);
  Size size1 = Size(300, 300);
  canvas1.drawOval(offset1 & size1, Paint()..color = Colors.blue);
  Picture picture1 = recorder1.endRecording();

  // 三、初始化一个SceneBuilder
  SceneBuilder sceneBuilder = SceneBuilder();
  // 通过SceneBuilder上的方法将上诉canvas生成的Picture添加到engine
  sceneBuilder.pushOffset(0, 0);
  sceneBuilder.addPicture(new Offset(0, 0), picture);
  sceneBuilder.addPicture(new Offset(600, 800), picture1);
  sceneBuilder.pop();
  // 四、通过sceneBuilder.build生成scene
  Scene scene = sceneBuilder.build();

  window.onDrawFrame = () {
    // 五、调用window.render, 它只能在onDrawFrame或onBeginFrame中调用
    window.render(scene);
    scene.dispose();
  };
  // 触发一个VSync信号,在下一帧触发onDrawFrame回调
  window.scheduleFrame();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

在这个例子中,我创建了两个图形,一个红色的正方形,一个蓝色的圆形,先看看效果。

在Flutter中,我们的Canvas对象需要通过PictureRecorder,当调用一系列Canvas操作之后(Canvas操作不熟悉的请看我之前的文章Flutter canvas学习之基础知识),需要调用recorder.endRecording()来结束此Canvas操作并返回一个Picture对象,此Picture对象实际上就是一个图层了,然后我们使用SceneBuilder将这个Picture对象添加进去,并生成一个Scene对象,Scene对象就是控制着整个屏幕绘制的类,我们要将它传入window.render()中进行光栅化合成上屏。注意这里的PictureRecorderSceneBuilder都是一次性的,当调用完recorder.endRecordingscene.dispose()之后就不能再使用了。

看完上面流程让我们有个概念,Flutter的PaintComposited流程都是围绕上面的过程来进行的。

compositingBits

当完成layout布局之后,会进行compositingBits过程

compositingBits的作用是将脏合成列表中更新_needsCompositing标记,_needsCompositing会被用于paint过程中确定是否使用新的layer进行绘制,比如裁剪。实际上它本身并不在常见的绘制流程中。

[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]

void flushCompositingBits() {
  // _nodesNeedingCompositingBitsUpdate是一个待重新compositingBits的列表
  _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
  for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
    // 判断节点是否还需要更新
    if (node._needsCompositingBitsUpdate && node.owner == this)
      node._updateCompositingBits();
  }
  // 清空列表
  _nodesNeedingCompositingBitsUpdate.clear();
}
1
2
3
4
5
6
7
8
9
10
11

_nodesNeedingCompositingBitsUpdate是一个待重新compositingBits的列表,同layout过程类似,它是通过markNeedsCompositingBitsUpdate来将当前节点存入列表。

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void _updateCompositingBits() {
  // 如果节点不需要更新 直接return,用于递归时的子节点
  if (!_needsCompositingBitsUpdate)
    return;
  final bool oldNeedsCompositing = _needsCompositing;
  _needsCompositing = false;
  // 访问子节点
  visitChildren((RenderObject child) {
    child._updateCompositingBits();
    // 如果子节点也需要合成,将当前`_needsCompositing`置为true
    if (child.needsCompositing)
      _needsCompositing = true;
  });
  // 如果当前的节点的isRepaintBoundary或alwaysNeedsCompositing为true,将当前`_needsCompositing`置为true
  if (isRepaintBoundary || alwaysNeedsCompositing)
    _needsCompositing = true;
  // 这里是一个优化,如果oldNeedsCompositing与_needsCompositing不相等 说明当前节点或者子孙节点isRepaintBoundary或alwaysNeedsCompositing值有更新 就调一下markNeedsPaint
  if (oldNeedsCompositing != _needsCompositing)
    markNeedsPaint();
  _needsCompositingBitsUpdate = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

CompositingBits的作用就是将当前节点的_needsCompositing值确定,如果当前isRepaintBoundary或者alwaysNeedsCompositing为true时,或者子孙节点有一个满足isRepaintBoundary或者alwaysNeedsCompositing为true时,那么_needsCompositing也会为true。在这个过程中,它会更新具有脏合成位的任何渲染对象。

Paint

接下来看看渲染非常重要的流程之一——Paint过程

RepaintBoundary

在开始Paint流程之前,我们先确定一个概念RepaintBoundary,我们知道现代的UI系统都会进行界面的图层划分,这样可以进行图层复用,减少绘制量,提升绘制性能。在Flutter中我们使用RenderObjectisRepaintBoundary来负责控制图层。

我们要在子类中重写isRepaintBoundary=>true,这样可以让父节点重新渲染时不重新渲染自己。

目前2.2.2版本中,所有自带isRepaintBoundary属性的WidgetTextFieldCupertinoTextSelectionToolbarRenderEditableSingleChildScrollViewFlowAndroidViewUiKitViewPlatformViewSurfaceTextureRenderViewRepaintBoundary,其中RepaintBoundary是开放给开发者自行使用的。

flushPaint

Paint过程通过调用一系列canvasApi来构建一棵layer树,它是通过调用flushPaint开始的。

[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]

void flushPaint() {
  //...
  try {
    // 待渲染列表
    final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
    _nodesNeedingPaint = <RenderObject>[];
    for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
      assert(node._layer != null);
      if (node._needsPaint && node.owner == this) {
        // 如果节点已经被attached过,说明当前是更新的情况,所以直接更新当前节点及其子节点
        if (node._layer!.attached) {
          // 如果layer已经生成过,调用PaintingContext.repaintCompositedChild
          PaintingContext.repaintCompositedChild(node);
        } else {
          // 否则调用RenderObject上的_skippedPaintingOnLayer方法,递归其父节点将_needsPaint置为true来保证没有所有应该被更新的节点会被更新
          node._skippedPaintingOnLayer();
        }
      }
    }
  } finally {
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

layoutcompositingBits类似,Paint过程也会维护一个叫_nodesNeedingPaint的列表,用于只更新当前需要更新的节点。_nodesNeedingPaint是通过markNeedsPaint方法来添加的。

markNeedsPaint

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void markNeedsPaint() {
  if (_needsPaint)
    return;
  _needsPaint = true;
  // 如果当前节点isRepaintBoundary为true 且owner!=null, 将当前节点加入到_nodesNeedingPaint队列中
  if (isRepaintBoundary) {
    if (owner != null) {
      owner!._nodesNeedingPaint.add(this);
      // 调用window.scheduleFrame()通知需要接收下一帧信息
      owner!.requestVisualUpdate();
    }
  }
  // 父节点是RenderObject则标记父节点
  else if (parent is RenderObject) {
    final RenderObject parent = this.parent! as RenderObject;
    parent.markNeedsPaint();
  } else {
    // 当前为根节点时
    if (owner != null)
      owner!.requestVisualUpdate();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

根据isRepaintBoundary是否为true,如果当前是RepaintBoundary,就将当前节点添加到needingPaint列表中,否则递归调用父节点的markNeedsPaint。其本质就是根据isRepaintBoundary来确定需要绘制的区域。

_skippedPaintingOnLayer

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void _skippedPaintingOnLayer() {
  AbstractNode? node = parent;
  while (node is RenderObject) {
    if (node.isRepaintBoundary) {
      if (node._layer == null)
        break; // 如果这里的子树从未被绘制过,停止递归。
      if (node._layer!.attached)
        break; // 如果layer已经被插入到layer树中,停止递归
      node._needsPaint = true;
    }
    node = node.parent;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

_skippedPaintingOnLayer主要是处理当前节点及父节点在某个时段被detach(从layer树中被移除)了,将所有被移除的节点重新标记为_needsPaint

repaintCompositedChild

[-> packages/flutter/lib/src/rendering/object.dart:PaintingContext]

static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
  assert(child._needsPaint);
  _repaintCompositedChild(
    child,
    debugAlsoPaintedParent: debugAlsoPaintedParent,
  );
}
static void _repaintCompositedChild(
  RenderObject child, {
  bool debugAlsoPaintedParent = false,
  PaintingContext? childContext,
}) {
  //...
  // 拿到当前节点的layer
  OffsetLayer? childLayer = child._layer as OffsetLayer?;
  // 如果layer为空,就初始化layer
  if (childLayer == null) {
    child._layer = childLayer = OffsetLayer();
  } else {
    // 否则从layer树中移除当前layer
    childLayer.removeAllChildren();
  }
  // 初始化当前节点的childContext
  childContext ??= PaintingContext(child._layer!, child.paintBounds);
  // 开始绘制当前节点
  child._paintWithContext(childContext, Offset.zero);
  // 停止绘制
  childContext.stopRecordingIfNeeded();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

_repaintCompositedChild主要将当前节点的layer重新初始化然后调用_paintWithContext进行绘制。

_paintWithContext

[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]

void _paintWithContext(PaintingContext context, Offset offset) {
  if (_needsLayout)
    return;
  _needsPaint = false;
  try {
    // 调用当前节点的paint方法
    paint(context, offset);
  } catch (e, stack) {
  }
}
1
2
3
4
5
6
7
8
9
10

_paintWithContext在开发阶段会做一些绘制前的检查,同时根据当前_needsPaint来判断是否需要绘制,最后会调用paint方法进行绘制。

paint

paint是每个节点绘制的开始的方法,需要子类自行实现绘制目标,它通过传入一个PaintingContextOffset

  • PaintingContext: 持有PictureRecorderCanvas等绘制类,并封装了一些常用的绘制方法,是绘制核心类
  • Offset: 绘制区域,通过父节点计算出当前节点绘制的区域,绘制时在此区域进行绘制

RenderObject中只定义了一个paint空方法,需要子类进行实现,例如ColoredBox组件的_RenderColoredBoxpaint实现:

[-> packages/flutter/lib/src/widgets/basic.dart:_RenderColoredBox]

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);
    }
  }
1
2
3
4
5
6
7
8

ColoredBox对应的RenderObject_RenderColoredBox,其中使用了drawRect进行绘制,并通过Paint()..color设置区域颜色。

这里有个地方还需要注意,当我们调用context.canvas时,会调用当前节点的canvas的getter方法

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

void _startRecording() {
  assert(!_isRecording);
  _currentLayer = PictureLayer(estimatedBounds);
  // 创建画板 当绘制结束,也会调用它的endRecording进行停止
  _recorder = ui.PictureRecorder();
  // 使用_recorder初始化一个canvas
  _canvas = Canvas(_recorder!);
  // 将初始化的PictureLayer添加进当前layer的孩子节点中
  _containerLayer.append(_currentLayer!);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果当前节点的_canvas没有被初始化,那么会调用_startRecording进行一系列初始化,此时会先初始化一个PictureLayerPictureLayer是一个能够具有绘制能力的Layer,我们Flutter中大部分组件都是使用它来绘制

compositeFrame

flushPaint完成后,此时Layer树已经生成,需要把Layer树发送到GPU进行绘制到屏幕上。flushPaint结束之后,会立即调用renderView.compositeFrame()进行合成上屏

[-> packages/flutter/lib/src/rendering/binding.dart:RenderBinding]

void compositeFrame() {
    //...
    try {
      // 初始化一个SceneBuilder类
      final ui.SceneBuilder builder = ui.SceneBuilder();
      // 将SceneBuilder传入layer,此时会递归整棵layer树
      final ui.Scene scene = layer!.buildScene(builder);
      if (automaticSystemUiAdjustment)
        _updateSystemChrome();
      // 调用window.render绘制出画面
      _window.render(scene);
      scene.dispose();
    } finally {
      // ...
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当调用了window.render就会开始绘制了。window.render传入一个SceneScene只能通过SceneBuilder生成。

[-> packages/flutter/lib/src/rendering/layer.dart:ContainerLayer]

ui.Scene buildScene(ui.SceneBuilder builder) {
  List<PictureLayer>? temporaryLayers;
  // ..
  updateSubtreeNeedsAddToScene();
  addToScene(builder);
  _needsAddToScene = false;
  final ui.Scene scene = builder.build();
  // ..
  return scene;
}
// 更新当前节点及孩子节点的_needsAddToScene的值 
void updateSubtreeNeedsAddToScene() {
  super.updateSubtreeNeedsAddToScene();
  Layer? child = firstChild;
  while (child != null) {
    child.updateSubtreeNeedsAddToScene();
    _needsAddToScene = _needsAddToScene || child._needsAddToScene;
    child = child.nextSibling;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

addToScene由Layer子类自行实现,通过传入的SceneBuilderSceneBuilder中有很多方法,会将layer传入到Flutter引擎中,如常用的pushOffset

OffsetEngineLayer pushOffset(double dx, double dy, { OffsetEngineLayer oldLayer }) {
    final OffsetEngineLayer layer = OffsetEngineLayer._(_pushOffset(dx, dy));
    return layer;
  }
  EngineLayer _pushOffset(double dx, double dy) native 'SceneBuilder_pushOffset';
1
2
3
4
5

pushOffset可以用于创建一个偏移图形,它通过传入的位置信息生成一个OffsetEngineLayer

Layer

Layer是Flutter Framework中针对SceneBuilder的一些方法做了一个封装,每种Layer都对应了一个或多个SceneBuilder的方法

我们常用的Layer有很多,这里分为有孩子节点Layer对象无孩子节点Layer对象有孩子节点Layer对象不会执行具体绘制,它会调用一些无孩子节点Layer对象来进行绘制,有孩子节点Layer对象有:

  • 位移类(OffsetLayer/TransformLayer);
  • 透明类(OpacityLayer)
  • 裁剪类(ClipRectLayer/ClipRRectLayer/ClipPathLayer);
  • 阴影类 (PhysicalModelLayer)

无孩子节点Layer对象就是具体绘制类,但它不具备子节点

  • PictureLayer 用于绘制,Flutter上的组件基本用它来绘制的
  • TextureLayer 用于外接纹理,比如视频播放
  • PlatformViewLayer 是用于iOS上的PlatformView嵌入纹理的使用

我们看看OffsetLayeraddToScene方法

void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
  engineLayer = builder.pushOffset(
    layerOffset.dx + offset.dx,
    layerOffset.dy + offset.dy,
    oldLayer: _engineLayer as ui.OffsetEngineLayer?,
  );
  addChildrenToScene(builder);
  builder.pop();
}
1
2
3
4
5
6
7
8
9

我们通过builder.pushOffset将位置偏移量传入,这样后面的子节点的绘制整体都会被应用此偏移。

再来看看用于绘制的PictureLayer

class PictureLayer extends Layer {
  PictureLayer(this.canvasBounds);
  // 用于调试时生成边框的属性
  final Rect canvasBounds;
  // 保存绘制信息的Picture类
  ui.Picture? get picture => _picture;
  ui.Picture? _picture;
  set picture(ui.Picture? picture) {
    markNeedsAddToScene();
    _picture = picture;
  }
  // ...
  
  void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    builder.addPicture(layerOffset, picture!, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
  }
  //...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

PictureLayer中持有了一个保存了一块区域绘制信息的Picture类,在addToScene时通过SceneBuilderaddPicture方法传入。

addToSceneLayer树根节点进行,当遇到有孩子节点Layer对象时,会调用addChildrenToScene对所有孩子节点调用addToScene,这样,每一次compositeFrame流程初始化的SceneBuilder就会贯穿整个Layer树(上面说了SceneBuilder是一次性的)。

最后在上面的compositeFrame方法中,通过builder.build生成Scene对象并通过window.render发给GPU,这样整个页面就渲染出来了。

Last Updated: 8/15/2021, 7:00:32 PM