哈罗大家好,好久不见,隔了很多天了好久没更新了,最近事情太多了而且状态也不是很好一直没有更新,趁着今天公司年会没有任务,大家都在组队王者荣耀,我就偷个闲,今天我们继续来讲解Flutter运行原理篇的内容,也算是我们新年的第一篇吧,Let ’s go
上一篇讲到了《Flutter运行原理篇之layout布局的过程》最后面的彩蛋我们提到了会讲到paint重绘,所以今天我们就来讲讲《Flutter运行原理篇之paint重绘的原理》,让我们愉快的开始吧
在开始之前我们先要简单的了解几个概念:
Canvas:封装了Flutter Skia各种绘制指令,比如画线、画圆、画矩形等指令,这个与我们在移动端以及Web端的Canvas概率上没有本质的区别,Canvas 绘制完成后,通过 PictureRecorder 获取绘制产物,然后将其保存在 Layer 中
layout:也称作图层,可以分为容器类和绘制类两种;可以理解为绘制就是在图层上面进行的,比如调用 Canvas 的绘制 API 后,相应的绘制产物被保存在 PictureLayer.picture 对象中, 最常见的layout包括:
- OffsetLayer:根 Layer,它继承自ContainerLayer,而ContainerLayer继承自 Layer 类,我们将直接继承自ContainerLayer 类的 Layer 称为容器类Layer,容器类 Layer 可以添加任意多个子Layer。
- PictureLayer:保存绘制产物的 Layer,它直接继承自 Layer 类。我们将可以直接承载(或关联)绘制结果的 Layer 称为绘制类 Layer,相应的绘制产物PictureRecorder被保存在 PictureLayer.picture 对象中
RepaintBoundary:边界节点Widget,顾名思义也就是绘制的边界节点,绘制只能最多影响到离他最近的一个边界节点,边界之外的Widget并不受影响
isRepaintBoundary:bool类型,意思是申明Widget自己是边界节点,作用与上面异曲同工
好了,了解了这几个概念以后我们再来看看paint的步骤,与build和layout的内容一样,我们还是分为两步来讲解paint的重绘的过程,分别是第一次重绘,与更新widget以后的重绘,让我们先来看看第一次重绘的内容:
第一次重绘肯定是从根RenderObject也就是RenderView开始:
RenderView.paint
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child!, offset);
}
PaintingContext.paintChild
void paintChild(RenderObject child, Offset offset) {
assert(() {
if (debugProfilePaintsEnabled)
Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
debugOnProfilePaint?.call(child);
return true;
}());
if (child.isRepaintBoundary) { //判断是否边界节点,因为RenderView肯定是边界节点
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
assert(() {
if (debugProfilePaintsEnabled)
Timeline.finishSync();
return true;
}());
}
RenderView.isRepaintBoundary
@override
bool get isRepaintBoundary => true; //属性为true
看看RenderView的isRepaintBoundary属性,说明RenderView肯定是边界节点,上面If里面判断了是否边界节点,因为RenderView肯定是边界节点,然后往下进行
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
assert(() {
if (debugRepaintRainbowEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(estimatedBounds.deflate(3.0), paint);
}
if (debugPaintLayerBordersEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFF9800);
canvas.drawRect(estimatedBounds, paint);
}
return true;
}());
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
stopRecordingIfNeeded 在下面我讲解,这里先略过
void _compositeChild(RenderObject child, Offset offset) {
assert(!_isRecording);
assert(child.isRepaintBoundary);
assert(_canvas == null || _canvas!.getSaveCount() == 1);
// Create a layer for our child, and paint the child into it.
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: true,
includedChild: false,
);
child._layer!.debugCreator = child.debugCreator ?? child;
return true;
}());
}
assert(child._layer is OffsetLayer);
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!);
}
这个函数里面有3个地方需要注意的:
- 绘制孩子节点时,如果遇到边界节点且当其不需要重绘(_needsPaint 为 false) 时,会直接复用该边界节点的 layer,而无需重绘!这就是边界节点能跨 frame 复用的原理(可以理解为重绘如果遇到的是边界节点可以复用,非边界节点则需要重绘,理解这个非常重要,务必仔细体会)。
- 因为边界节点的layer类型是ContainerLayer,所以是可以给它添加子节点。
- 注意是将当前边界节点的layer添加到父边界节点,而不是父节点。
PaintingContext.repaintCompositedChild
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,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: debugAlsoPaintedParent,
includedChild: true,
);
return true;
}());
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
child._layer = childLayer = OffsetLayer(); //childLayer如果为空的话,就new一个OffsetLayer
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
}
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(child._layer!, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
assert(identical(childLayer, child._layer));
childContext.stopRecordingIfNeeded();
}
childLayer如果为空的话,就new一个OffsetLayer赋值给 child._layer = childLayer,然后把这个构造PaintingContext传给下面child绘制使用
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
RenderObject? debugLastActivePaint;
assert(() {
_debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this;
assert(!isRepaintBoundary || _layer != null);
return true;
}());
_needsPaint = false;
try {
paint(context, offset);
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
assert(() {
debugPaint(context, offset);
_debugActivePaint = debugLastActivePaint;
_debugDoingThisPaint = false;
return true;
}());
}
把PaintingContext传给paint方法,paint方法里面一般重绘完自身以后都会调用PaintingContext.paintChild方法去重绘子类,也会把这个PaintingContext一直传下去,PaintingContext的ContainerLayerd的属性就是OffsetLayer
PaintingContext.paintChild
void paintChild(RenderObject child, Offset offset) {
assert(() {
if (debugProfilePaintsEnabled)
Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
debugOnProfilePaint?.call(child);
return true;
}());
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset); //非边界节点,直接传递this作为child的PaintingContext
}
assert(() {
if (debugProfilePaintsEnabled)
Timeline.finishSync();
return true;
}());
}
PaintingContext.paintChild方法里面也先调用stopRecordingIfNeeded,再调用_compositeChild,这里也就构成了一个递归循环的调用一直到叶子节点的RenderObject的paint方法,如果他不是边界节点的话直接传递this作为child的PaintingContext,这里也就说明了如果没有边界节点的话那么所有的RenderObject都是在一个PaintingContext也就是一个ContainerLayer的图层里面绘制的,如果有边界截点的话那么就会重新申请新的ContainerLayer并且在新的图层上面绘制
感觉内容有点多,我借用网上的例子来给大家说一下,下图左边是 widget 树,右边是最终生成的Layer树,我们看一下生成过程:
- 一开始绘制会从RenderView开始,因为他是一个绘制边界节点(我们上面已经讲了什么是边界节点),在第一次绘制时会为他创建一个 OffsetLayer,我们记为 OffsetLayer1,接下来 OffsetLayer1会传递给child
- 由于 Row 是一个容器类组件且不需要绘制自身,那么接下来他会绘制自己的孩子,它有两个孩子,先绘制第一个孩子Column1,将 OffsetLayer1 传给 Column1,而 Column1 也不需要绘制自身,那么它又会将 OffsetLayer1 传递给第一个子节点Text1。
- Text1 需要绘制文本,他会使用 OffsetLayer1进行绘制,由于 OffsetLayer1 是第一次绘制,所以会新建一个PictureLayer1和一个 Canvas1 ,然后将 Canvas1 和PictureLayer1 绑定,接下来文本内容通过 Canvas1 对象绘制,Text1 绘制完成后,Column1 又会将 OffsetLayer1 传给 Text2 。
- Text2 也需要使用 OffsetLayer1 绘制文本,但是此时 OffsetLayer1 已经不是第一次绘制,所以会复用之前的 Canvas1 和 PictureLayer1,调用 Canvas1来绘制文本。
- Column1 的子节点绘制完成后,PictureLayer1 上承载的是Text1 和 Text2 的绘制产物。
- 接下来 Row 完成了 Column1 的绘制后,开始绘制第二个子节点 RepaintBoundary,Row 会将 OffsetLayer1 传递给 RepaintBoundary,由于它是一个绘制边界节点,且是第一次绘制,则会为它创建一个 OffsetLayer2,接下来 RepaintBoundary 会将 OffsetLayer2 传递给Column2,和 Column1 不同的是,Column2 会使用 OffsetLayer2 去绘制 Text3 和 Text4,绘制过程同Column1,在此不再赘述。
- 当 RepaintBoundary 的子节点绘制完时,要将 RepaintBoundary 的 layer( OffsetLayer2 )添加到父级Layer(OffsetLayer1)中。
整棵组件树绘制完成,生成了一棵右图所示的 Layer 树。需要说名的是 PictureLayer1 和 OffsetLayer2 是兄弟关系,它们都是 OffsetLayer1 的孩子。通过上面的例子我们至少可以发现一点:同一个 Layer 是可以多个组件共享的,比如 Text1 和 Text2 共享 PictureLayer1,共享的话也会导致一个问题,比如 Text1 文本发生变化需要重绘会连带着 Text2 也必须重绘,这个也是为什么有时候我们合理的使用RepaintBoundray可以提高重绘的效率了,就像上图如果Text3的重绘只会影响到Column2而不会再影响到Row
为什么会有共享这种机制了,其实究其原因其实还是为了节省资源,Layer 太多时 Skia 会比较耗资源,所以这其实是一个trade-off。
上面提到了Canvas的使用,(一般来说在paint方法绘制自身的时候回使用到Canvas来进行绘制)所以让我来看看代码他是怎么使用的:
Canvas get canvas {
//如果canvas为空,则是第一次获取;
if (_canvas == null) _startRecording();
return _canvas!;
}
//创建PictureLayer和canvas
void _startRecording() {
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
//将pictureLayer添加到_containerLayer(是绘制边界节点的Layer)中
_containerLayer.append(_currentLayer!);
}
调用get属性的时候会判断为空则创建PictureLayer,以及ui.PictureRecorder(相应的绘制产物PictureRecorder被保存在 PictureLayer.picture 对象中),以及_canvas,将pictureLayer添加到_containerLayer(是绘制边界节点的OffsetLayer)中
我们再来看上面遗留的stopRecordingIfNeeded,这个里面与_startRecording做了相反的操作,只是把ui.PictureRecorder保存下来以后,PictureLayer,ui.PictureRecorder,_canvas都做了置空的操作,这里面有个小细节有朋友可能会问为什么_currentLayer(PictureLayer)置空了,_currentLayer!.picture还能保留下来呢,这个是因为我们在_startRecording的时候把加入到了_containerLayer中了,所以_containerLayer已经保存了对象的引用,而_currentLayer只是另一个引用而已
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
assert(() {
if (debugRepaintRainbowEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(estimatedBounds.deflate(3.0), paint);
}
if (debugPaintLayerBordersEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFF9800);
canvas.drawRect(estimatedBounds, paint);
}
return true;
}());
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
好了,上面是我们从RenderView第一次绘制的时候大概流程,接下来我们再看看更新widget以后的重绘是什么样子的呢:
首先我们还是回顾一下上一篇《Flutter运行原理篇之layout布局的过程》最后面提到的彩蛋(Widget更新会导致build,继而导致layout):
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject? relayoutBoundary;
// 先确定当前组件的布局边界
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
// _needsLayout 表示当前组件是否被标记为需要布局
// _constraints 是上次布局时父组件传递给当前组件的约束
// _relayoutBoundary 为上次布局时当前组件的布局边界
// 所以,当当前组件没有被标记为需要重新布局,且父组件传递的约束没有发生变化,
// 且布局边界也没有发生变化时则不需要重新布局,直接返回即可。
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
// 如果需要布局,缓存约束和布局边界
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
// 后面解释
if (sizedByParent) {
performResize();
}
// 执行布局
performLayout();
// 布局结束后将 _needsLayout 置为 false
_needsLayout = false;
// 将当前组件标记为需要重绘(因为布局发生变化后,需要重新绘制)
markNeedsPaint();
}
Layout 最后对于子节点(不再是容器的RenderObject)最后都会调用markNeedsPaint方法,重这里开始我们就开始paint重绘的过程了,我们先看看这个函数
RenderObject.markNeedsPaint
void markNeedsPaint() {
if (_needsPaint) return;
_needsPaint = true;
if (isRepaintBoundary) { // 如果是当前节点是边界节点
owner!._nodesNeedingPaint.add(this); //将当前节点添加到需要重新绘制的列表中。
owner!.requestVisualUpdate(); // 请求新的frame,该方法最终会调用scheduleFrame()
} else if (parent is RenderObject) { // 若不是边界节点且存在父节点
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint(); // 递归调用父节点的markNeedsPaint
} else {
// 如果是根节点,直接请求新的 frame 即可
if (owner != null)
owner!.requestVisualUpdate();
}
}
下判断是否是边界节点,如果是的话直接加入到了_nodesNeedingPaint上面,如果存在父节点又不是边界的话直接调用parent.markNeedsPaint继续向上递归查找
接着我再帮大家回顾一下整个Widget的更新运行要经过5个步骤:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget
super.drawFrame();
//下面几个是在super.drawFrame()执行的
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“层合成”信息
pipelineOwner.flushPaint(); // 4.重绘
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏,将绘制出的bit数据发送给GPU
}
}
上面的build,layout我们都已经介绍过了,忘记的朋友可以出门左转即可,接下来是合成的内容,这个我们往下先放一放(因为合成的目的就是为了重绘),接下来就到了重绘,也就是我们今天的内容
那我们就先来看看 pipelineOwner.flushPaint(); 函数:
PipelineOwner.flushPaint()
void flushPaint() {
if (!kReleaseMode) {
Timeline.startSync('Paint', arguments: timelineArgumentsIndicatingLandmarkEvent);
}
assert(() {
_debugDoingPaint = true;
return true;
}());
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
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) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
assert(_nodesNeedingPaint.isEmpty);
} finally {
assert(() {
_debugDoingPaint = false;
return true;
}());
if (!kReleaseMode) {
Timeline.finishSync();
}
}
}
这里面会对于dirtyNodes(也就是我们上面添加的_nodesNeedingPaint)里面的每一个node都会执行PaintingContext.repaintCompositedChild(node);方法,其实这个过程我们上面已经分析过了,这部分内容与第一次重绘的过程是一样的:
PaintingContext.repaintCompositedChild
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,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: debugAlsoPaintedParent,
includedChild: true,
);
return true;
}());
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
// Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
child._layer = childLayer = OffsetLayer();
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
}
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(child._layer!, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself).
assert(identical(childLayer, child._layer));
childContext.stopRecordingIfNeeded();
}
现在我们已经说清楚了paint重绘的整个流程,如果你读到这里的话那么你应该清楚了从Widget更新开始经过build,到layout再到paint的大致流程应该清楚了,给大家推一篇博客《绘制原理及Layer》我今天这篇文章部分内容来自这篇博客,说实话如果你有耐心的话我还是推荐你去看完他整篇内容,好了,我们今天的内容就到这里了,新年到来之际还是祝大家新年快乐,升职加薪,最重要的是身体健康,天天开心,下期见···