我是靠谱客的博主 甜美眼神,最近开发中收集的这篇文章主要介绍Flutter框架分析(七)-- 绘制,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Flutter框架分析分析系列文章:

《Flutter框架分析(一)-- 总览和Window》

《Flutter框架分析(二)-- 初始化》

《Flutter框架分析(三)-- Widget,Element和RenderObject》

《Flutter框架分析(四)-- Flutter框架的运行》

《Flutter框架分析(五)-- 动画》

《Flutter框架分析(六)-- 布局》

《Flutter框架分析(七)-- 绘制》

前言

本篇文章会结合Flutter源码给大家介绍一下渲染流水线最后一步的绘制(paint)阶段。本文涉及的内容可能离大家平时开发Flutter app所需要知道的框架知识相对于前面几章会跟遥远一些。目前可能需要注意的地方就是RepaintBoundary这个Widget,其对应的RenderObjectRenderRepaintBoundary。这个Widget的作用在介绍完渲染流水线的绘制阶段相信大家会有一个更明确的理解。

概述

我们都知道,Flutter框架中render tree负责布局和渲染。在渲染的时候,Flutter会遍历需要重绘的RenderObject子树来逐一绘制。我们在屏幕上看到的Flutter app页面其实是由不同的图层(layers)组合(compsite)而成的。这些图层是以树的形式组织起来的,也就是我们在Flutter中见到的又一个比较重要的树:layer tree。

上图是Flutter框架渲染机制的一个示意图。上方绿色方框里的内容可以认为就是本系列文章的关注所在。也就是Flutter框架渲染流水线运行的地方。可见,整个渲染流水线是运行在UI线程里的,以Vsync信号为驱动,在框架渲染完成之后会输出layer tree。layer tree被送入engine,engine会把layer tree调度到GPU线程,在GPU线程内合成(compsite)layer tree,然后由Skia 2D渲染引擎渲染后送入GPU显示。这里提到layer tree是因为我们即将要分析的渲染流水线绘制阶段最终输出就是这样的layer tree。所以绘制阶段并不是简单的调用 paint()函数这么简单了,而是很多地方都涉及到layer tree的管理。

Layer

Flutter中的图层用类Layer来代表。

abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
@override
ContainerLayer get parent => super.parent;
Layer get nextSibling => _nextSibling;
Layer _nextSibling;
Layer get previousSibling => _previousSibling;
Layer _previousSibling;
}
复制代码

Layer是个抽象类,和RenderObject一样,继承自AbstractNode。表明它也是个树形结构。属性parent代表其父节点,类型是ContainerLayer。这个类继承自Layer。只有ContainerLayer类型及其子类的图层可以拥有孩子,其他类型的Layer子类都是叶子图层。nextSiblingpreviousSibling表示同一图层的前一个和后一个兄弟节点,也就是图层孩子节点们是用双向链表存储的。

class ContainerLayer extends Layer {
Layer _firstChild;
Layer _lastChild;
void append(Layer child) {
adoptChild(child);
child._previousSibling = lastChild;
if (lastChild != null)
lastChild._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
}
void _removeChild(Layer child) {
if (child._previousSibling == null) {
_firstChild = child._nextSibling;
} else {
child._previousSibling._nextSibling = child.nextSibling;
}
if (child._nextSibling == null) {
_lastChild = child.previousSibling;
} else {
child.nextSibling._previousSibling = child.previousSibling;
}
child._previousSibling = null;
child._nextSibling = null;
dropChild(child);
}
void removeAllChildren() {
Layer child = firstChild;
while (child != null) {
final Layer next = child.nextSibling;
child._previousSibling = null;
child._nextSibling = null;
dropChild(child);
child = next;
}
_firstChild = null;
_lastChild = null;
}
}
复制代码

ContainerLayer增加了头和尾两个孩子节点属性,并提供了新增及删除孩子节点的方法。

ContainerLayer的子类有OffsetLayer,ClipRectLayer等等。

叶子类型的图层有TextureLayer,PlatformViewLayer, PerformanceOverlayLayerPictureLayer等等,框架中大部分RenderObject的绘制的目标图层都是PictureLayer

class PictureLayer extends Layer {
final Rect canvasBounds;
ui.Picture _picture;
}
复制代码

属性canvasBounds代表图层画布的边界,但这个属性是建议性质的。 属性picture来自dart:ui库。

分析

回到我们熟悉的drawFrame()函数中,pipelineOwner.flushLayout()调用完成以后渲染流水线就进入了绘制(paint)阶段。

void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
复制代码

绘制阶段的第一个调用是pipelineOwner.flushCompositingBits()

pipelineOwner.flushCompositingBits()

这个调用是用来更新render tree 中RenderObject_needsCompositing标志位的。

在介绍这个调用之前我们,我们先来了解一些RenderObject的标志位。

bool _needsCompositing:标志自身或者某个孩子节点有合成层(compositing layer)。如果当前节点需要合成,那么所有祖先节点也都需要合成。

bool _needsCompositingBitsUpdate:标志当前节点是否需要更新_needsCompositing。这个标志位由下方的markNeedsCompositingBitsUpdate()函数设置。

bool get isRepaintBoundary => false;:标志当前节点是否与父节点分开来重绘。当这个标志位为true的时候,父节点重绘的时候子节点不一定也需要重绘,同样的,当自身重绘的时候父节点不一定需要重绘。此标志位为trueRenderObject有render tree的根节点RenderView,有我们熟悉的RenderRepaintBoundaryTextureBox等。

bool get alwaysNeedsCompositing => false;:标志当前节点是否总是需要合成。这个标志位为true的话意味着当前节点绘制的时候总是会新开合成层(composited layer)。例如TextureBox, 以及我们熟悉的显示运行时性能的RenderPerformanceOverlay等。

在渲染流水线的构建阶段,有些情况下render tree里的节点需要重新更新_needsCompositing,比如说render tree里节点的增加,删除。这个标记工作由函数markNeedsCompositingBitsUpdate()完成。

void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent;
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}
复制代码

这个调用会从当前节点往上找,把所有父节点的_needsCompositingBitsUpdate标志位都置位true。直到自己或者父节点的isRepaintBoundarytrue。最后会把自己加入到PipelineOwner_nodesNeedingCompositingBitsUpdate列表里面。而函数调用pipelineOwner.flushCompositingBits()正是用来处理这个列表的。

flushCompositingBits()源码如下:

void flushCompositingBits() {
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
}
复制代码

首先把列表_nodesNeedingCompositingBitsUpdate按照节点在树中的深度排序。然后遍历调用node._updateCompositingBits()


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;
}
复制代码

这里做的事情是从当前节点往下找,如果某个子节点isRepaintBoundarytruealwaysNeedsCompositingtrue则设置_needsCompositingtrue。子节点这个标志位为true的话,那么父节点的该标志位也会被设置为true。如果_needsCompositing发生了变化,那么会调用markNeedsPaint()通知渲染流水线本RenderObject需要重绘了。为啥要重绘呢?原因是本``RenderObject`所在的图层(layer)可能发生了变化。

pipelineOwner.flushPaint()

函数flushPaint()处理的是之前加入到列表_nodesNeedingPaint里的节点。当某个RenderObject需要被重绘的时候会调用markNeedsPaint()

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;
parent.markNeedsPaint();
} else {
if (owner != null)
owner.requestVisualUpdate();
}
}
复制代码

函数markNeedsPaint()首先做的是把自己的标志位_needsPaint设置为true。然后会向上查找最近的一个isRepaintBoundarytrue的祖先节点。直到找到这样的节点,才会把这个节点加入到_nodesNeedingPaint列表中,也就是说,并不是任意一个需要重绘的RenderObject就会被加入这个列表,而是往上找直到找到最近的一个isRepaintBoundarytrue才会放入这个列表,换句话说,这个列表里只有isRepaintBoundarytrue这种类型的节点。也就是说重绘的起点是从“重绘边界”开始的。


void flushPaint() {
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
// Sort the dirty nodes in reverse order (deepest first).
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {
...
}
}
复制代码

在处理需要重绘的节点的时候,会先给这些节点做个排序,这里需要注意的是,和之前flushLayout()里的排序不同,这里的排序是深度度深的节点在前。在循环体里,会判断当前节点的_layer属性是否处于attached的状态。如果_layer.attachedtrue的话调用PaintingContext.repaintCompositedChild(node);去做绘制,否则的话调用node._skippedPaintingOnLayer()将自身以及到上层绘制边界之间的节点的_needsPaint全部置为true。这样在下次_layer.attached变为true的时候会直接绘制。

从上述代码也可以看出,重绘边界相当于把Flutter的绘制做了分块处理,重绘的从上层重绘边界开始,到下层重绘边界为止,在此之间的RenderObject都需要重绘,而边界之外的就可能不需要重绘,这也是一个性能上的考虑,尽量避免不必要的绘制。所以如何合理安排RepaintBoundary是我们在做Flutter app的性能优化时候需要考虑的一个方向。

这里的_layer属性就是我们之前说的图层,这个属性只有绘制边界的RenderObject才会有值。一般的RenderObject这个属性是null


static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
if (child._layer == null) {
child._layer = OffsetLayer();
} else {
child._layer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}
复制代码

函数_repaintCompositedChild()会先检查RenderObject的图层属性,为空则新建一个OffsetLayer实例。如果图层已经存在的话就把孩子清空。

如果没有PaintingContext的话会新建一个,然后让开始绘制。我们先来看一下PaintingContext这个类:

class PaintingContext extends ClipContext {
@protected
PaintingContext(this._containerLayer, this.estimatedBounds)
final ContainerLayer _containerLayer;
final Rect estimatedBounds;
PictureLayer _currentLayer;
ui.PictureRecorder _recorder;
Canvas _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);
}
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
_currentLayer.picture = _recorder.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
复制代码

PaintingContext字面意思是绘制上下文,其属性_containerLayer是容器图层,来自构造时的入参。也就是说PaintingContext是和容器图层关联的。接下来还有PictureLayer类型的_currentLayer属性, ui.PictureRecorder类型的_recorder属性和我们熟悉的Canvas类型的属性_canvas。函数_startRecording() 实例化了这几个属性。_recorder用来录制绘制命令,_canvas绑定一个录制器。最后,_currentLayer会作为子节点加入到_containerLayer中。有开始那么就会有结束,stopRecordingIfNeeded()用来结束当前绘制的录制。结束时会把绘制完毕的Picture赋值给当前的PictureLayer.picture

有了PaintingContext以后,就可以调用RenderObject._paintWithContext()开始绘制了,这个函数会直接调用到我们熟悉的RenderObject.paint(context, offset),我们知道函数paint()RenderObject子类自己实现。从之前的源码分析我们知道绘制起点都是“绘制边界”。这里我们就拿我们熟悉的一个“绘制边界”,RenderRepaintBoundary,为例来走一下绘制流程,它的绘制函数的实现在RenderProxyBoxMixin类中:


@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child, offset);
}
复制代码

这个调用又回到了PaintingContextpaintChild()方法:


void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
}
复制代码

这里会检查子节点是不是绘制边界,如果不是的话,就是普通的绘制了,接着往下调用_paintWithContext(),继续往当前的PictureLayer上绘制。如果是的话就把当前的绘制先停掉。然后调用_compositeChild(child, offset);


void _compositeChild(RenderObject child, Offset offset) {
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
}
child._layer.offset = offset;
appendLayer(child._layer);
}
复制代码

如果这个子绘制边界被标记为需要重绘的话,那么就调用repaintCompositedChild()来重新生成图层然后重绘。如果这个子绘制边界没有被标记为需要重绘的话,就跳过了重新生成图层和重绘。最后只需要把子图层加入到当前容器图层中就行了。

上面说的是子节点是绘制边界的时候的绘制流程,那如果子节点是普通的一个RenderObject呢?这里就拿Flutter app出错控件的绘制做个例子:

void paint(PaintingContext context, Offset offset) {
try {
context.canvas.drawRect(offset & size, Paint() .. color = backgroundColor);
double width;
if (_paragraph != null) {
// See the comment in the RenderErrorBox constructor. This is not the
// code you want to be copying and pasting. :-)
if (parent is RenderBox) {
final RenderBox parentBox = parent;
width = parentBox.size.width;
} else {
width = size.width;
}
_paragraph.layout(ui.ParagraphConstraints(width: width));
context.canvas.drawParagraph(_paragraph, offset);
}
} catch (e) {
// Intentionally left empty.
}
}
复制代码

这看起来就像个正常的绘制了,我们会用来自PaintingContext的画布canvas来绘制矩形,绘制文本等等。从前面的分析也可以看出,这里的绘制都是在一个PictureLayer的图层上所做的。

至此 pipelineOwner.flushPaint();这个函数的调用就跑完了,通过分析我们可以知道,绘制工作其实主要是在这个函数中完成的。接下来我们再来看一下绘制流程的最后一个重要的函数调用:

renderView.compositeFrame()

这里的renderView就是我们之前说的render tree的根节点。这个函数调用主要是把整个layer tree生成scene送到engine去显示。

void compositeFrame() {
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}
复制代码

ui.SceneBuilder()最终调用Native方法SceneBuilder_constructor。也就是说ui.SceneBuilder实例是由engine创建的。接下来就是调用layer.buildScene(builder)方法,这个方法会返回一个ui.Scene实例。由于方法compositeFrame()的调用者是renderView。所以这里这个layer是来自renderView的属性,我们前面说过只有绘制边界节点才有layer。所以可见render tree的根节点renderView也是一个绘制边界。那么这个layer是从哪里来的呢?在文章《Flutter框架分析(二)-- 初始化》我们讲过,框架初始化的过程中renderView会调度开天辟地的第一帧:

void scheduleInitialFrame() {
scheduleInitialLayout();
scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
owner.requestVisualUpdate();
}
Layer _updateMatricesAndCreateNewRootLayer() {
_rootTransform = configuration.toMatrix();
final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
rootLayer.attach(this);
return rootLayer;
}
void scheduleInitialPaint(ContainerLayer rootLayer) {
_layer = rootLayer;
owner._nodesNeedingPaint.add(this);
}
复制代码

在方法_updateMatricesAndCreateNewRootLayer()中,我们看到这里实例化了一个TransformLayerTransformLayer继承自OffsetLayer。构造时需要传入Matrix4类型的参数transform。这个Matrix4其实和我们在Android中见到的Matrix是一回事。代表着矩阵变换。这里的transform来自我们之前讲过的ViewConfiguration,它就是把设备像素比例转化成了矩阵的形式。最终这个layer关联上了renderView。所以这里这个TransformLayer其实也是layer tree的根节点了。

回到我们的绘制流程。layer.buildScene(builder);这个调用我们自然是去 TransformLayer里找了,但这个方法是在其父类OffsetLayer内,从这个调用开始就都是对图层进行操作,最终把layer tree转换为场景scene

ui.Scene buildScene(ui.SceneBuilder builder) {
List<PictureLayer> temporaryLayers;
updateSubtreeNeedsAddToScene();
addToScene(builder);
final ui.Scene scene = builder.build();
return scene;
}
复制代码

函数调用updateSubtreeNeedsAddToScene();会遍历layer tree来设置_subtreeNeedsAddToScene标志位,如果有任意子图层的添加、删除操作,则该子图层及其祖先图层都会被置上_subtreeNeedsAddToScene标志位。然后会调用addToScene(builder);


@override
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
_lastEffectiveTransform = transform;
final Offset totalOffset = offset + layerOffset;
if (totalOffset != Offset.zero) {
_lastEffectiveTransform = Matrix4.translationValues(totalOffset.dx, totalOffset.dy, 0.0)
..multiply(_lastEffectiveTransform);
}
builder.pushTransform(_lastEffectiveTransform.storage);
addChildrenToScene(builder);
builder.pop();
return null; // this does not return an engine layer yet.
}
复制代码

builder.pushTransform会调用到engine层。相当于告诉engine这里我要加一个变换图层。然后调用ddChildrenToScene(builder)将子图层加入场景中,完了还要把之前压栈的变换图层出栈。

void addChildrenToScene(ui.SceneBuilder builder, [ Offset childOffset = Offset.zero ]) {
Layer child = firstChild;
while (child != null) {
if (childOffset == Offset.zero) {
child._addToSceneWithRetainedRendering(builder);
} else {
child.addToScene(builder, childOffset);
}
child = child.nextSibling;
}
}
复制代码

这就是遍历添加子图层的调用。主要还是逐层向下的调用addToScene()。这个方法不同的图层会有不同的实现,对于容器类图层而言,主要就是做三件事:1.添加自己图层的效果然后入栈,2.添加子图层,3. 出栈。

在所有图层都处理完成之后。回到renderView.compositeFrame(),可见最后会把处理完得到的场景通过_window.render(scene);调用送入engine去显示了。

至此渲染流水线的绘制(paint)阶段就算是跑完了。

等等,好像缺了点什么,在分析绘制的过程中我们看到有个主要的调用pipelineOwner.flushCompositingBits()是在更新render tree里节点的_needsCompositing标志位的。但是我们这都把流程说完了,貌似没有看到这个标志位在哪里用到啊。这个标志位肯定在哪里被用到了,否则我们费这么大劲更新有啥用呢?回去再研究一下代码......

这个标志位某些RenderObject在其paint()函数中会用到,作用呢,就体现在PaintingContext的这几个函数的调用上了:


void pushClipRect(bool needsCompositing, Offset offset, Rect clipRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.hardEdge }) {
final Rect offsetClipRect = clipRect.shift(offset);
if (needsCompositing) {
pushLayer(ClipRectLayer(clipRect: offsetClipRect, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetClipRect);
} else {
clipRectAndPaint(offsetClipRect, clipBehavior, offsetClipRect, () => painter(this, offset));
}
}
void pushClipRRect(bool needsCompositing, Offset offset, Rect bounds, RRect clipRRect, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) {
final Rect offsetBounds = bounds.shift(offset);
final RRect offsetClipRRect = clipRRect.shift(offset);
if (needsCompositing) {
pushLayer(ClipRRectLayer(clipRRect: offsetClipRRect, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetBounds);
} else {
clipRRectAndPaint(offsetClipRRect, clipBehavior, offsetBounds, () => painter(this, offset));
}
}
void pushClipPath(bool needsCompositing, Offset offset, Rect bounds, Path clipPath, PaintingContextCallback painter, { Clip clipBehavior = Clip.antiAlias }) {
final Rect offsetBounds = bounds.shift(offset);
final Path offsetClipPath = clipPath.shift(offset);
if (needsCompositing) {
pushLayer(ClipPathLayer(clipPath: offsetClipPath, clipBehavior: clipBehavior), painter, offset, childPaintBounds: offsetBounds);
} else {
clipPathAndPaint(offsetClipPath, clipBehavior, offsetBounds, () => painter(this, offset));
}
}
void pushTransform(bool needsCompositing, Offset offset, Matrix4 transform, PaintingContextCallback painter) {
final Matrix4 effectiveTransform = Matrix4.translationValues(offset.dx, offset.dy, 0.0)
..multiply(transform)..translate(-offset.dx, -offset.dy);
if (needsCompositing) {
pushLayer(
TransformLayer(transform: effectiveTransform),
painter,
offset,
childPaintBounds: MatrixUtils.inverseTransformRect(effectiveTransform, estimatedBounds),
);
} else {
canvas
..save()
..transform(effectiveTransform.storage);
painter(this, offset);
canvas
..restore();
}
}
复制代码

needsCompositing作为这几个函数的入参,从代码可见其作用主要是控制这几种特殊的绘制操作的具体实现方式,如果needsCompositingtrue的话,则会调用pushLayer,参数我们之前见过的各种图层


void pushLayer(ContainerLayer childLayer, PaintingContextCallback painter, Offset offset, { Rect childPaintBounds }) {
stopRecordingIfNeeded();
appendLayer(childLayer);
final PaintingContext childContext = createChildContext(childLayer, childPaintBounds ?? estimatedBounds);
painter(childContext, offset);
childContext.stopRecordingIfNeeded();
}
@protected
PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
return PaintingContext(childLayer, bounds);
}
复制代码

流程基本上和我们之前看到的重绘的时候新增一个图层的操作是一样的。

而如果needsCompositingfalse的话则走的是canvas的各种变换了。大家感兴趣的话可以去看一下源码,这里就不细说了。

总结

至此Flutter框架渲染流水线的绘制(paint)阶段就分析完了。绘制流程并不像之前的构建,布局流程那样直接,只要遍历element tree或者render tree就行了。渲染阶段会出现另一个树,图层树,layer tree。整个绘制流程就是在把render tree转化为适合的layer tree,最后再生成场景(scene)的一个过程。

最后,在了解渲染过程的基础上,推荐大家再看一下这个来自Google工程师的视频:深入了解 Flutter 的高性能图形渲染。相信大家在看过这个视频之后,会对Flutter框架的渲染,以及可能遇到的一些性能问题会有进一步的理解。

最后

以上就是甜美眼神为你收集整理的Flutter框架分析(七)-- 绘制的全部内容,希望文章能够帮你解决Flutter框架分析(七)-- 绘制所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部