原文传送门:
上一篇文章介绍了绘制的启动与布局的计算过程,这里接着绘制流程CompositingBits
、flushPaint
及compositeFrame
继续分析。
绘制原理
在开始探索绘制流程之前,我们先看看不使用Flutter Framework
的Widget
时,如何渲染出一个图形
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();
}
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()
中进行光栅化合成上屏。注意这里的PictureRecorder
、SceneBuilder
都是一次性的,当调用完recorder.endRecording
和scene.dispose()
之后就不能再使用了。
看完上面流程让我们有个概念,Flutter的Paint
和Composited
流程都是围绕上面的过程来进行的。
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();
}
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;
}
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中我们使用RenderObject
的isRepaintBoundary
来负责控制图层。
我们要在子类中重写isRepaintBoundary=>true
,这样可以让父节点重新渲染时不重新渲染自己。
目前2.2.2版本中,所有自带isRepaintBoundary
属性的Widget
有TextField
、CupertinoTextSelectionToolbar
、RenderEditable
、SingleChildScrollView
、Flow
、AndroidView
、UiKitView
、PlatformViewSurface
、Texture
、RenderView
、RepaintBoundary
,其中RepaintBoundary
是开放给开发者自行使用的。
flushPaint
Paint过程
通过调用一系列canvas
Api来构建一棵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 {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
同layout
及compositingBits
类似,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();
}
}
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;
}
}
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();
}
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) {
}
}
2
3
4
5
6
7
8
9
10
_paintWithContext
在开发阶段会做一些绘制前的检查,同时根据当前_needsPaint
来判断是否需要绘制,最后会调用paint
方法进行绘制。
paint
paint是每个节点绘制的开始的方法,需要子类自行实现绘制目标,它通过传入一个PaintingContext
和Offset
- PaintingContext: 持有
PictureRecorder
和Canvas
等绘制类,并封装了一些常用的绘制方法,是绘制核心类 - Offset: 绘制区域,通过父节点计算出当前节点绘制的区域,绘制时在此区域进行绘制
RenderObject
中只定义了一个paint
空方法,需要子类进行实现,例如ColoredBox
组件的_RenderColoredBox
中paint
实现:
[-> 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);
}
}
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!);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果当前节点的_canvas没有被初始化,那么会调用_startRecording
进行一系列初始化,此时会先初始化一个PictureLayer
,PictureLayer
是一个能够具有绘制能力的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 {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当调用了window.render
就会开始绘制了。window.render
传入一个Scene
,Scene
只能通过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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
addToScene
由Layer子类自行实现,通过传入的SceneBuilder
,SceneBuilder
中有很多方法,会将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';
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
嵌入纹理的使用
我们看看OffsetLayer
的addToScene
方法
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();
}
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);
}
//...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PictureLayer
中持有了一个保存了一块区域绘制信息的Picture
类,在addToScene
时通过SceneBuilder
的addPicture
方法传入。
addToScene
从Layer树
根节点进行,当遇到有孩子节点Layer对象
时,会调用addChildrenToScene
对所有孩子节点调用addToScene
,这样,每一次compositeFrame
流程初始化的SceneBuilder
就会贯穿整个Layer树
(上面说了SceneBuilder
是一次性的)。
最后在上面的compositeFrame
方法中,通过builder.build
生成Scene
对象并通过window.render
发给GPU,这样整个页面就渲染出来了。