Render Tree 的创建过程

RenderObject 的类型

我们知道 Element 主要分为负责渲染的 RenderObjectElement 和负责组合的 ComponentElement 两大类,而创建 RenderObject 节点的是前者 ​​mount()​​​ 方法中调用的 ​​RenderObjectWidget.createRenderObject()​​ 方法。

该方法是一个抽象方法,需要子类实现,对于不同的布局的 Widget 创建的 RenderObject 类型也不一样,在 Render Tree 中最主要的有两种 RenderObject:

  • 首先是在 RenderObject 注释说明中大量提到了一个类RenderBox,它是大部分的 RenderObjectWidget 所对应的 RenderObject 的抽象类
/// A render object in a 2D Cartesian coordinate system.
/// 一个在 2D 坐标系中的渲染对象
abstract class RenderBox extends RenderObject
  • 以及 Render Tree 的根节点RenderView
/// The root of the render tree.
/// Render Tree 的根节点,处理渲染管道的引导和渲染树的输出
/// 它有一个填充整个输出表面的 RenderBox 类型的唯一子节点
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>

其他的类型的 RenderObject 基本是为了特定布局(如滑动、列表)的实现,但大部分都直接或间接集成自 RenderBox。

通常一个 RenderBox 只有一个子节点(因为它只有一个 child 属性),这使得它整体更像是链表。 Flutter 提供了 ​​ContainerRenderObjectMixin​​ 用来给那些需要存储多个子节点的 RenderBox 进行扩展,多个子节点的组织方式也是采用链表来连接存储,下面列出常见的两种:

  • RenderStack实现了堆栈布局算法
  • RenderFlex实现了Flex布局算法,Column 和 Row 都是属于 Flex 的变体

RenderView 如何创建

既然 Render Tree 的根节点是 RenderView,那么我们看 RenderView 是在哪被创建的。

通过 IDE 的全局搜索我们可以找到对应的创建引用是在 ​​RendererBinding​​ 中。

/// Flutter 引擎和 Render Tree 之间的一个绑定器
mixin RendererBinding on BindingBase, ServicesBinding,
SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable

这个类建立了 Flutter Engine 和 Render Tree 之间的关联,注释中介绍,当 Binding 被创建的时候就会执行 ​​initInstances()​​ 进行初始化并创建 RenderView。

/// RendererBinding

@override
void initInstances() {
// ... 省略了 PipelineOwner 创建和 window 初始化代码
// 创建 RenderView
initRenderView();
}

/// Called automatically when the binding is created.
void initRenderView() {
// ...
renderView = RenderView(
configuration: createViewConfiguration(),
window: window);
// 初始化 RenderView
renderView.prepareInitialFrame();
}

我们回到 Flutter App 启动时调用的函数 runApp。

runApp 会创建 ​​WidgetsFlutterBinding​​​,并执行 ​​ensureInitialized()​​ 方法。

void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized() //初始化
..scheduleAttachRootWidget(app) // 创建其他两棵树的根节点
..scheduleWarmUpFrame();
}

而这个 ​​WidgetsFlutterBinding​​​ 实际上由 7 个 mixin Binding 组合成,其中就包括了 ​​RendererBinding​​​,而调用这几个 mixin Binding 的 ​​initInstances()​​ 都是交给父类 BindingBase 在构造方法中执行。

这种采用 mixin 组合 Binding 的设计可以方便后续接入新的 Binding。

class WidgetsFlutterBinding extends BindingBase
with GestureBinding, SchedulerBinding, ServicesBinding,
PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}

abstract class BindingBase {
/// Default abstract constructor for bindings.
///
/// First calls [initInstances] to have bindings initialize their
/// instance pointers and other state, then calls
/// [initServiceExtensions] to have bindings initialize their
/// observatory service extensions, if any.
BindingBase() {
initInstances();
initServiceExtensions();
developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
developer.Timeline.finishSync();
}
}

三棵树的初始化关联

在​​ensureInitialized()​​​ 方法执行完成得到 Render Tree 根节点之后,就是调用 ​​scheduleAttachRootWidget()​​ 创建其他两棵树的根节点,然后和 Render Tree 进行关联。

@protected
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}

void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(
buildOwner!,
renderViewElement as RenderObjectToWidgetElement<RenderBox>?
);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}

在这里​​attachRootWidget()​​ 创建了 RenderObjectToWidgetAdapter,它的本质其实是 RenderObjectWidget,我们可以看到它声明了对应的 Render Tree 的节点类型为 RenderBox,并且指定了该 RenderBox 的父节点是 RenderView。

最后调用 ​​attachToRenderTree()​​ 将 RenderObjectToWidgetAdapter 转化为 RootRenderObjectElement 并和 Render Tree 进行绑定。


PipelineOwner 渲染管道管理

目前的 Render Tree 只是一个数据结构,并没有渲染操作。因此我们来研究一下从 Render Tree 到界面是一个什么样的过程。

刚刚提到了 RenderBinding 建立了 Flutter Engine 和 Render Tree 之间的关联,在创建 RenderView 的过程中,我们可以注意到它还创建了一个 PipelineOwner 的对象,并且在设置 renderView 时还将 RenderView 赋值给了它的 rootNode。

/// RendererBinding
@override
void initInstances() {
_pipelineOwner = PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate,
onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
);
}

set renderView(RenderView value) {
_pipelineOwner.rootNode = value;
}
复制代码

PipelineOwner 其实渲染管道的管理者,它在渲染流程中有 3 个主要的方法:

  1. ​flushLayout​​ 更新所有脏节点列表的布局信息
  2. ​flushCompositionBits​​ 对重新计算 needsCompositing 的节点进行更新
  3. ​flushPaint​​ 重绘所有脏节点

这 3 个方法通常是按顺序一起使用的,RenderBiding 会在 ​​drawFrame()​​ 方法中调用这 3 个方法

/// RenderBiding
@protected
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}

那么接下来我们就来研究一下这 3 个方法分别做了什么。

flushLayout

我们知道当 RenderObject 有两个标识:

  • _needsLayout 用于标识是否需要重新 Layout
  • _needsPaint 用于标识是否需要重新绘制

这两个属性是保证 Render Tree 局部重绘的关键属性。

当某个节点需要更新布局信息时,会调用 ​​markNeedsLayout()​​​ 来重置 _needsLayout,但只这个过程还会将当前节点添加到 PipelineOwner 的 _nodesNeedingLayout 中(​​markNeedsPaint​​ 则会添加到 _nodesNeedingPaint)。

// 仅保留主要代码
void markNeedsLayout() {
_needsLayout = true;
if (owner != null) {
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}

​flushLayout()​​​ 会将深度遍历这些节点,调用 RenderObject 的 ​​_layoutWithoutResize()​​​ 方法来重新 Layout,最后将 _needsLayout 置为 false 并调用 ​​markNeedsPaint()​​ 让该节点需要重新绘制。

/// PipelineOwner
void flushLayout() {
// 只保留主要逻辑
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
// 深度遍历
for (RenderObject node in dirtyNodes..sort(
(RenderObject a, RenderObject b) => a.depth - b.depth)
) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
}

/// RenderObject
@pragma('vm:notify-debugger-on-exception')
void _layoutWithoutResize() {
try {
performLayout(); // 布局测量
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
_needsLayout = false;
markNeedsPaint(); // 让节点需要重新绘制
}

Layout 是通过 ​​performLayout()​​ 方法完成的,这个方法是 RenderObject 预留给子类实现自身 Layout 逻辑的抽象方法,例如在 RenderView 中的实现如下

/// RenderView
@override
void performLayout() {
// RenderView 需要占满整个屏幕
// 使用 ViewConfiguration 的 size
_size = configuration.size;

if (child != null)
// 让子节点在父节点的布局约束下进行 Layout
child!.layout(BoxConstraints.tight(_size));
}

要注意的是,自定义的 RenderBox 如果要放在能包含多个子节点的 RenderBox 中,例如 RenderFlex 和 RenderStack,那么需要重写 performLayout() 来确定布局大小,当然我们也可以利用另外一种方式,使用父节点的提供的约束来调整自己的大小:

@override
bool get sizedByParent => true;

@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.smallest;
}

这个方式在我们下面的实验🧪会用到。

flushCompositingBits

在 ​​flushLayout()​​​ 方法后紧接着会被调用的方法是 ​​flushCompositingBits()​​​。这个方法会进行深度遍历更新 _nodesNeedingCompositingBitsUpdate 列表中节点的 needsCompositing,它会调用节点的 ​​_updateCompositingBits()​​ 方法对 RenderObject 节点的一些属性进行更新,包括:

  • _needsCompositing 是否需要合成 layer
  • _needsCompositingBitsUpdate 是否需要更新 _needsCompositing
/// PipelineOwner
void flushCompositingBits() {
// 只保留主要逻辑
_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();
if (!kReleaseMode) {
Timeline.finishSync();
}
}

flushPaint

​flushPaint()​​ 是第 3 个调用的,对 _nodesNeedingPaint 中的节点进行深度遍历,然后调用节点的 PaintingContext 的静态方法 ​​repaintCompositedChild()​​ 重新绘制 RenderObject 的视图。

/// PipelineOwner
void flushPaint() {
// 只保留主要逻辑
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)) {
if (node._needsPaint && node.owner == this) {
if (node._layerHandle.layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
}

该方法中通过层层调用最终会到达,传入节点的 ​​paint()​​​ 方法。​​paint()​​ 方法也是 RenderObject 提供给子类实现绘制逻辑的抽象方法。同样以 RenderView 为例子:

/// RenderView
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child!, offset);
}

由于 RenderView 是整颗树的根节点,因此没有什么绘制逻辑,但所有的 RenderObject 都一样,如果有子节点都会通过 PaintingContext 继续调用子节点的 ​​paint()​​ 方法并将 PaintingContext 传递下去,直到整颗树的节点都完成绘制。


场景合成与界面刷新渲染

我们知道 Widget 最终都是通过 Canvas 进行绘制的,因此我们以一个自定义 View 的例子来做分析。

在 《Flutter 实战·第二版》 这本书中,是使用 ​​CustomPainter​​​ 来编写自定义 View,通过重写 ​​void paint(Canvas canvas, Size size);​​ 方法来获得一个 Canvas 对象,因此可以往这个方法的源码翻阅,查看这个 Canvas 对象的来源。

// custom_paint.dart
abstract class CustomPainter extends Listenable

/// Provides a canvas on which to draw during the paint phase.
/// 提供了在绘图阶段要进行绘制的 Canvas
class RenderCustomPaint extends RenderProxyBox {

void _paintWithPainter(Canvas canvas, Offset offset, CustomPainter painter) {
// ...
// 在这里调用 CustomPainter 的 paint,并提供一个 Canvas 对象
painter.paint(canvas, size);
}

@override
void paint(PaintingContext context, Offset offset) {
if (_painter != null) {
// 这里提供 canvas
_paintWithPainter(context.canvas, offset, _painter!);
_setRasterCacheHints(context);
}
super.paint(context, offset);
if (_foregroundPainter != null)
_paintWithPainter(context.canvas, offset, _foregroundPainter!);
_setRasterCacheHints(context);
}
}
}

在这里我们可以看出,我们自定义 View 的绘制操作,是由 RenderCustomPaint 执行,它的本质其实是一个 RenderBox,而其中传入的 Canvas 对象是由它在 ​​paint()​​ 中的 PaintingContext 提供的。

Canvas 与绘制存储

在 PaintingContext 中是采用懒加载的方式来创建 Canvas 对象,PaintingContext 一般创建于 Render Tree 的单独子树开始绘制时,创建时会附带创建另外两个对象:

  • PictureLayer 图层
  • PictureRecorder 图像记录者
// object.dart
class PaintingContext extends ClipContext {
Canvas? _canvas;

/// 获取 Canvas 对象,
/// 当 _canvas 没有创建时调用 [_startRecording] 方法创建
@override
Canvas get canvas {
if (_canvas == null)
_startRecording();
assert(_currentLayer != null);
return _canvas!;
}

/// 创建 Canvas 对象
/// - 创建 PictureLayer 图层对象
/// - 创建 PictureRecorder 图像记录者
/// - 创建 Canvas 对象
/// - 将 PictureLayer 添加到 ContainerLayer 容器层
void _startRecording() {
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
_containerLayer.append(_currentLayer!);
}
}

创建 Canvas 时必须传入一个 PictureRecorder 对象,这个对象会记录 Canvas 的绘制操作,当完成记录时,可通过调用 ​​PictureRecord.endRecording​​ 来结束记录,并得到一个 Picture 对象,由于 Canvas 的绘制是由 Engine 层中的 Skia 引擎提供,因此 Picture 对象也是存储在 Engine 层。

/// PictureRecorder
Picture endRecording() {
if (_canvas == null)
throw StateError('PictureRecorder did not start recording.');
final Picture picture = Picture._();
_endRecording(picture);
_canvas!._recorder = null;
_canvas = null;
return picture;
}

void _endRecording(Picture outPicture) native 'PictureRecorder_endRecording';

Layer Tree

​_startRecording()​​ 除了创建 Canvas 和 PictureRecorder 外,还创建了一个 PictureLayer 对象并将它加入到了 _containerLayer 中。这个 _containerLayer 其实是 RenderObject 中的一个 Layer。

Layer 是用于缓存绘图操作结果(Picture)的图层,图层可以按照规则进行排列得到图像。每个 RenderObject 中会都有一个 Layer,存储在 LayerHandle 中,Render Tree 执行 flushPaint 完成绘制后,会形成一颗 Layer Tree,Layer Tree 的节点数量会比 Render Tree 少,几个 RenderObject 节点只对应一个 Layer 节点。

Layer 节点也有多种,但用的最多的是以下两种:

  • 使用 PictureRecorder 记录绘图操作的节点使用 PictureLayer,PictureLayer 不具有子节点,这是最常用的叶子节点类型
  • 当需要和 Layer 子节点进行叠加来得到图像时,可使用 ContainerLayer,它提供了 append 方法来连接 Layer,以形成一颗 Layer Tree。

ContainerLayer 可以有多个子节点,它们以链表的方式连接在一起,一般不会直接使用 ContainerLayer,而是使用它的子类 OffsetLayer。

使用 ​​prepareInitialFrame()​​ 方法初始化 RenderView 创建的 Layer 类型是 TransformLayer ,它也是 OffsetLayer 的子类。

当创建 PaintingContext 时提供的 Layer 节点不属于 OffsetLayer 时 ,会创建一个 OffsetLayer 来代替原本的 Layer,作为当前子树的根节点。 PaintingContext 创建新的 PictureLayer 时将会使用 append 方法将新的 Layer 节点添加到这个 OffsetLayer 中。

/// PaintingContext
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();
}
// 在这里创建 PaintingContext
childContext ??= PaintingContext(childLayer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
// 完成绘制结束记录
childContext.stopRecordingIfNeeded();
}

上面提到如果节点有孩子,会通过 ​​context.paintChild()​​​ 让子节点也调用 ​​_paintWithContext()​​​ 方法将 PaintingContext 向下传递,继续执行子节点的 ​​paint()​​ 方法进行绘制。

当目前的图层绘制完成时,绘制完成时会调用 ​​stopRecordingIfNeeded()​​ 来结束记录绘制,并将 PictureRecord 生成的 Picture 对象缓存到 PictureLayer 中。

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

/// PictureLayer
set picture(ui.Picture? picture) {
markNeedsAddToScene();
_picture?.dispose();
_picture = picture;
}

节点的绘制分离

Render Tree 的绘制是采用深度遍历自顶向下绘制的,即当前节点绘制完调用子节点的绘制方法。

RenderObject 提供了 isRepaintBoundary 属性来判断当前子树是否需要与父节点分开绘制,该属性默认为 false,并且没有 setter 来进行修改,因此默认情况下一颗 Render Tree 可能只会生成 2 个 Layer 节点(根节点的 TransformLayer 和存储绘制结果的 PictureLayout)。

但其实我们可以在 RenderBox 的子类重写该属性,或者使用 RenderRepaintBoundary(它的 isRepaintBoundary 被重写为 true),来分离父子节点的绘制,从达到分开绘制生成不同 Layer 节点形成一颗 Layer Tree。

该属性在 ​​markNeedsPaint()​​方法中也有使用,相关源码如下:

void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
markNeedsPaintCout++;
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();
}
}
  • 如果 isRepaintBoundary 为 true 则表示和父节点分开绘制,将自己添加到 _nodesNeedingPaint 列表中,在下一次更新时就只会重绘当前子树,不会污染到父节点。
  • 如果 isRepaintBoundary 为 false 则调用父节点的​​markNeedsPaint()​​来让父节点处理,下一次更新由父节点重绘时执行自己的绘制方法进行重绘。

而在绘制流程中,如果子节点的 isRepaintBoundary 为 true,代表需要分开绘制,会结束当前 PictureRecorder 的记录并将生成的 Picture 存到 Layer 中,然后开始子节点的绘制。

子节点绘制时由于 PaintingContext 的 Layer 已经被设置为 null 了,所以会创建新的 PictureLayer 并添加到根 Layer 的子节点列表,如果子节点不需要重新绘制,就直接将子节点的 Layer 添加到根 Layer 的子节点列表。

这里添加时使用的 ​​appendLayer()​​ 会先将当前的 Layer 节点从原本的父节点中移除,再进行添加,因此不用当心会出现重复添加的情况,由于子节点列表的本质是链表,而且创建后添加与再添加之间通常不会有其它 Layer 节点介入,因此也不需要当心该方法添加时的移动和查找效率。

/// PaintingContext
void paintChild(RenderObject child, Offset offset) {
if (child.isRepaintBoundary) {
stopRecordingIfNeeded(); // 结束当前树的绘制
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
}

/// 省略了很多代码
void _compositeChild(RenderObject child, Offset offset) {
// Create a layer for our child, and paint the child into it.
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
}

final OffsetLayer childOffsetLayer = child._layerHandle.layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(childOffsetLayer);
}

@protected
void appendLayer(Layer layer) {
layer.remove(); // 从父节点中移除当前节点
_containerLayer.append(layer);
}

场景渲染

我们回到 RenderBinding 的 ​​drawFrame()​​ 方法中,看一下 Render Tree 完成绘制后,是如何渲染到界面的。

/// RenderBiding
@protected
void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}

/// RenderView
void compositeFrame() {
final ui.SceneBuilder builder = ui.SceneBuilder();
// 将图层添加到 scene
final ui.Scene scene = layer!.buildScene(builder);
// 发送 scene 给 GPU 进行渲染
_window.render(scene);
scene.dispose();
}

/// Layer
ui.Scene buildScene(ui.SceneBuilder builder) {
updateSubtreeNeedsAddToScene();
addToScene(builder); // 抽象方法,由子类实现
_needsAddToScene = false;
final ui.Scene scene = builder.build();
return scene;
}

当需要发送帧图像给 GPU 时,会调用 ​​compositeFrame()​​​ 方法,在这个方法中会构建一个 SceneBuilder,然后通过 ​​ContainerLayer.buildScene()​​ 将 Layer Tree 的 Picture 合成一个 Scene。

Scene 可理解为场景,是存储 GPU 绘制的像素信息的图像对象,当添加的是 OffsetLayer 会设置图层的偏移量,当添加的是 ContanierLayer 时会遍历子节点进行添加,当添加的是 PictureLayer 会调用 native 方法在 Engine 添加 Picture 到图像中,当我们调用 build 方法时也是从 Engine 得到 Scene 对象。

void _addPicture(double dx, double dy, Picture picture, int hints)
native 'SceneBuilder_addPicture';

void _build(Scene outScene) native 'SceneBuilder_build';

Layer 中有两个属性 _needsAddToScene 和 _subtreeNeedsAddToScene 来表示自己和子树是否需要被添加到 Scene 中,当 Layer 被脏了则需要合成到 Scene,一个 Layer 或者其子树被合成到 Scene 后,对应的属性会被设置为 false。

Scene 合成完成后,接着调用 render 方法将 Scene 发送给 GUP 渲染到界面上。

/// FlutterView
void render(Scene scene) => _render(scene, this);
void _render(Scene scene, FlutterView view) native 'PlatformConfiguration_render';

界面刷新

现在我们知道 Flutter 是调用 ​​drawFrame()​​​ 方法,来做 Render Tree 的绘制,那么 ​​drawFrame()​​ 什么时候执行呢?我们阅读一下这个方法的注释。

/// This method is called by [handleDrawFrame], which itself is called
/// automatically by the engine when it is time to lay out and paint a frame.

注释中说明 ​​drawFrame()​​​ 会在 Engine 需要提供一帧新图像时,自动被 ​​handleDrawFrame()​​ 方法调用,实际上在 RenderBinding 初始化的时候,会把这个方法添加到 persistentCallbacks 回调列表中。

/// RenderBinding
void initInstances() {
// window 的初始化时会设置一些状态改变的回调
window
..onMetricsChanged = handleMetricsChanged
..onTextScaleFactorChanged = handleTextScaleFactorChanged
..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
// RenderView 初始化创建
initRenderView();
// 在这里添加了一个回调
addPersistentFrameCallback(_handlePersistentFrameCallback);
}

void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame(); // 在这个回调里调用帧绘制
_scheduleMouseTrackerUpdate();
}

/// SchedulerBinding

/// 该列表中的回调方法会被 handleDrawFrame 依次拿出来执行
final List<FrameCallback> _persistentCallbacks = <FrameCallback>[];

/// 将回调添加到 _persistentCallbacks 中
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}

​handleDrawFrame()​​​ 被执行时,会从回调列表里面取出这个回调,从而屏幕刷新的时候都会调用 ​​drawFrame()​​ 将 Render Tree 绘制到界面上。

/// Engine 调用这个方法来提供新的一帧图像
void handleDrawFrame() {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
// ... 只保留关键代码
}

也就是说,我们界面刷新时,相关的回调工作会交给 ​​handleDrawFrame()​​​ 去执行,而这个方法除了在 APP 启动的时候,会先在 ​​scheduleWarmUpFrame()​​​ 的定时器中执行一次进行首次展示外,在 ​​scheduleAttachRootWidget()​​​ 方法执行的时候,就会被注册到 ​​window.onDrawFrame​​了作为界面刷新的回调了。 我们采用断点调试的方式,可以看到 APP 启动的时候这个方法的注册调用链如下:

Flutter 绘制流程分析与代码实践_flutter

void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app) // 提前注册回调
..scheduleWarmUpFrame();
}

void attachRootWidget(Widget rootWidget) {
// 如果是引导帧,则进行视觉更新
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}

void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); // <- 帧任务
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}

/// 以下都是 SchedulerBinding 中的方法
void scheduleFrame() {
ensureFrameCallbacksRegistered(); // <- 确定回调的注册
window.scheduleFrame(); // 请求回调的执行,进行界面更新
_hasScheduledFrame = true;
}

@protected
void ensureFrameCallbacksRegistered() {
window.onBeginFrame ??= _handleBeginFrame;
window.onDrawFrame ??= _handleDrawFrame; // <- 注册回调
}

注册的这个回调其实就是对 handleDrawFrame 包了一层壳。

void _handleDrawFrame() {
if (_rescheduleAfterWarmUpFrame) {
_rescheduleAfterWarmUpFrame = false;
addPostFrameCallback((Duration timeStamp) {
_hasScheduledFrame = false;
scheduleFrame();
});
return;
}
handleDrawFrame();
}

​window.scheduleFrame()​​​ 会向 Engine 层发起一个请求,在下一次合适的时机调用​​window.onDrawFrame​​​和 ​​window.onBeginFrame​​注册的回调,从而刷新界面。

最后我们采用断点调试的方式,看界面刷新时 drawFrame 的完整调用链是什么样,绿框中的就是我们刚刚所讲到的那些方法了。

Flutter 绘制流程分析与代码实践_子节点_02

到这里,知识就串起来了~


整理图

我们画张图整理一下,为了让图更加简单易看,我们省略掉亿点点细节🤏。

Flutter 绘制流程分析与代码实践_子节点_03


Framework 项目代码实验

当然了解完相关流程,我们直接在 Flutter Framework 的项目中进行实验,按照流程自己写一遍从 Render Tree 到界面刷新的代码,证明、也是熟悉这个流程。

首先根据官方说明配置一个 Framework 开发环境,然后进入到 hello_world 项目中: ​​github.com/flutter/flu…​

实验项目和平时开发一样依旧采用 Flutter APP 的方式启动,但不同的是我们不调用 ​​runApp()​​ 方法,而是直接创建一颗 Render Tree 和使用 Canvas,采用上面讲的流程来执行我们的 APP。

我们先尝试使用 Canvas 绘制一条直线,然后生成 Picture 添加到 Sence 中,然后发送给 GPU 进行渲染。

import 'dart:ui';
import 'package:flutter/material.dart';

void main() {

final PictureRecorder pictureRecorder = PictureRecorder();
drawLine(pictureRecorder);
final Picture picture = pictureRecorder.endRecording();

final SceneBuilder sceneBuilder = SceneBuilder();
sceneBuilder.addPicture(Offset.zero, picture);
final Scene scene = sceneBuilder.build();
window.render(scene);
}

void drawLine(PictureRecorder recorder) {
final Canvas canvas = Canvas(recorder);

final Paint paint = Paint()
..color = Colors.white
..strokeWidth = 10;

canvas.drawLine(Offset(300, 300), Offset(800, 300), paint);
}

Flutter 绘制流程分析与代码实践_子节点_04

上面的代码会在界面绘制一条白线,由于这里只 render 了一次,因此在绘制完这条白线后,界面就不会有任何变化了。 现在我们尝试让线条动起来,通过上面的讲解,我们知道 Flutter 是使用 ​​window.scheduleFrame()​​​来请求屏幕的刷新,因此我们将渲染放到 ​​window.onDrawFrame​​中,并不断改变线条位置。

import 'dart:ui';
import 'package:flutter/material.dart';

void main() {
double dy = 300.0;

window.onDrawFrame = () {
final PictureRecorder pictureRecorder = PictureRecorder();
drawLine(pictureRecorder, dy);
if (dy < 800)
dy++;

final Picture picture = pictureRecorder.endRecording();

final SceneBuilder sceneBuilder = SceneBuilder();
sceneBuilder.addPicture(Offset.zero, picture);
final Scene scene = sceneBuilder.build();

// 不断刷新界面
window.render(scene);
window.scheduleFrame();
};

window.scheduleFrame();
}

void drawLine(PictureRecorder recorder, double dy) {
final Canvas canvas = Canvas(recorder);

final Paint paint = Paint()
..color = Colors.white
..strokeWidth = 10;

canvas.drawLine(Offset(300, dy), Offset(800, dy), paint);
}

这样就得到了一条会移动的直线。

Flutter 绘制流程分析与代码实践_ide_05

接下来我们将上面的直线封装为一个自定义的 RenderObject,然后自己创建一颗 Render Tree,并使用 ​​drawFrame()​​ 方法中的流程:使用 PipelineOwner 来重新绘制被污染的节点。

void main() {
// 构建根节点
final PipelineOwner pipelineOwner = PipelineOwner();
final RenderView renderView =
RenderView(configuration: const ViewConfiguration(), window: window);
pipelineOwner.rootNode = renderView;
// 初始化
renderView.prepareInitialFrame();

renderView.child = MyRenderNode();

window.onDrawFrame = () {
callFlush(pipelineOwner);
renderView.compositeFrame();
window.scheduleFrame();
};
window.scheduleFrame();
}

void callFlush(PipelineOwner pipelineOwner) {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
}

class MyRenderNode extends RenderBox {

double _dy = 300;

final Paint _paint = Paint()
..color = Colors.white
..strokeWidth = 10;

void _drawLines(Canvas canvas, double dy) {
canvas.drawLine(Offset(300, dy), Offset(800, dy), _paint);
}

@override
void paint(PaintingContext context, Offset offset) {
_drawLines(context.canvas, _dy);
_dy++;
markNeedsLayout();
}
}

这份代码运行的效果和上面的是一样的,但只有一个节点并不能看出转化为 Layer Tree 的优势,我们来构建一颗多个节点的 Render Tree。我们采用 RenderFlex 来存储多个节点,并和上面讲解 ​​flushLayout()​​时所说的一样交由父节点来决定布局大小。

void main() {
// 构建根节点
final PipelineOwner pipelineOwner = PipelineOwner();
final RenderView renderView =
RenderView(configuration: const ViewConfiguration(), window: window);
pipelineOwner.rootNode = renderView;
// 初始化
renderView.prepareInitialFrame();

final RenderFlex flex = RenderFlex(textDirection: TextDirection.ltr);

// 从 301 开始移动到 500 一共绘制了 200 次
double dy = 301;

// 创建两个叶子节点
final MyRenderNode node1 = MyRenderNode(dy, Colors.white);
final MyRenderNode node2 = MyRenderNode(dy, Colors.blue);

renderView.child = flex;
// 注意这里是往前插入
flex.insert(node1);
flex.insert(node2);

window.onDrawFrame = () {
callFlush(pipelineOwner);
renderView.compositeFrame();
if (dy < 500) {
node1.dy = ++dy;
window.scheduleFrame();
} else {
print('node1 paint count: ${node1.paintCount}');
print('node2 paint count: ${node2.paintCount}');
}
};

window.scheduleFrame();
}

void callFlush(PipelineOwner pipelineOwner) {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
}

class MyRenderNode extends RenderBox {
MyRenderNode(this._dy, Color color) {
_paint = Paint()
..color = color
..strokeWidth = 10;
}

double _dy;
int paintCount = 0;

set dy(double dy) {
_dy = dy;
markNeedsLayout();
}

double get dy => _dy;

late Paint _paint;

void _drawLines(Canvas canvas, double dy) {
canvas.drawLine(Offset(300, dy), Offset(800, dy), _paint);
}

@override
void paint(PaintingContext context, Offset offset) {
_drawLines(context.canvas, dy);
paintCount++;
}

@override
bool get sizedByParent => true;

@override
Size computeDryLayout(BoxConstraints constraints) {
return constraints.smallest;
}
}

这份代码比较长,对于 ​​MyRenderNode​​的修改:

  • 首先我们重写了​​sizedByParent​​​和​​computeDryLayout()​​,用于在布局测量时决定大小
  • ​_dy​​​ 属性添加了 setter 方法,在每次修改​​_dy​​​ 的值时调用​​markNeedsLayout()​​ 来让下一次界面刷新时重新绘制节点
  • 另外我们还添加了一个​​piantCount​​ 属性来记录节点绘制了几次

接着是 main 方法中:

  • 使用 RenderFlex 作为 RenderView 的子节点
  • 创建了两个子节点插入到 RenderFlex 中
  • 每次渲染时,都会修改 node1 的 dy,让他进行重绘,node2 则不做修改
  • 当 dy 的值达到了 500 的时候停止界面刷新并打印两个节点的绘制次数

效果如上,会有一根不动的蓝线,和一根移动的白线。 我们再看看控制台打印的信息。

Flutter 绘制流程分析与代码实践_ide_06

我们发现两个节点的绘制次数都是 200,这意味着每次渲染两个节点都进行了重新绘制,根据上面我们讲到的 PaintingContext 和 Layer 的特点,我们可以很快判断出,这是由于 node1 和 node2 没有分开绘制,使用同一个 Layer 节点所造成的。

由于 node1 被污染后也会调用父节点 flex 的 ​​markNeedsPaint()​​,因此绘制操作时由父节点向下绘制的,而 node2 也是 flex 的子节点,整棵子树都会重新绘制,这就是 node2 污染时 node1 也跟着重绘的原因。 ​

我们在自定义的 RenderBox 里重写 isRepaintBoundary 属性,并在 framework 层为 ContainerLayer 添加一个节点计数方法。

/// ContainerLayer

int layerCount() {
int count = 1; // 算上当前节点
Layer? child = firstChild;
while (child != null) {
if(child is OffsetLayer)
count += child.layerCount();
else
count += 1;
child = child.nextSibling;
}
return count;
}
void main() {
window.onDrawFrame = () {
if (dy < 500) {
node1.dy = ++dy;
window.scheduleFrame();
} else {
print('node1 paint count: ${node1.paintCount}');
print('node2 paint count: ${node2.paintCount}');
// 在结束时打印 Layer 的数量
print('layer count: ${renderView.layer?.layerCount()}');
}
};
}

class MyRenderNode extends RenderBox {
bool _isRepaintBoundary = false;

@override
bool get isRepaintBoundary => _isRepaintBoundary;

/// 添加设置方法
set isRepaintBoundary(bool v) {
_isRepaintBoundary = v;
}
}

我们先来演示两种情况:

  1. 不对两个叶子节点的 isRepaintBoundary 进行修改

Flutter 绘制流程分析与代码实践_ide_07

  1. 将 node1 单独绘制:​​node1.isRepaintBoundary = false;​

Flutter 绘制流程分析与代码实践_ide_08

可以看到 node1 的 isRepaintBoundary 设置为 true 时, node2 只绘制了 1 次,现在 node2 的污染就不会导致 node1 重绘制了。

另外我们看到第二种情况的 Layer 节点数量分是 4,为什么会是 4 呢?

回想一下介绍 PaintingContext 创建时提供 Layout 的要求:

当提供给 PaintingContext 的 Layer 节点不属于 OffsetLayer 时 ,会创建一个 OffsetLayer 来代替原本的 Layer,作为当前子树的根节点。

如果我们对程序进行调试,就可以发现,虽然是以 node1、node2 的顺序插入,但实际 ​​insert()​​ 方法是往前插入,在 flex 中 node2 是处于 node1 的前面,因此 node2 会先进行绘制。

由于 node2 并没有设置单独绘制,因此会按照正常流程和 flex 绘制在同一个 PictureRecorder 中生成一个 PictureLayer 并添加到 TransformLayer 中。

node2 绘制完成之后开始绘制 node1。由于我们将 node1 设置为单独绘制,那么绘制 node1 的时候将会作为一个子树重新开始绘制,这时会重新调用 ​​_repaintCompositedChild()​​方法,创建一个新的 PaintingContext 来传递,此时由于 node1 是一个叶子结点,本身并不附带 OffsetLayer 节点,因此会创建一个新的 OffsetLayer 给 PaintingConext,再进行绘制。

绘制 node 1 时生成的 PictureLayer 添加到这个 OffsetLayout 中,完成绘制之后再将 OffsetLayout 添加到 RenderView 的 TransformLayer 中。

因此第 2 种情况会得到 4 个 Layer 节点,对应的 Layer 图示如下:

Flutter 绘制流程分析与代码实践_ide_09

我们修改一下计数方法,让它打印当前遍历的层次和节点类型。

int layerCount() {
int deep = 0;
print('$deep ==> root is [${this.runtimeType}]');
return _layerCount(deep + 1);
}

int _layerCount(int deep) {
int count = 1; // 算上当前节点
Layer? child = firstChild;
while (child != null) {
print('$deep ==> child is [${child.runtimeType}]');
if(child is OffsetLayer)
count += child._layerCount(deep + 1);
else
count += 1;
child = child.nextSibling;
}
return count;
}

Flutter 绘制流程分析与代码实践_flutter_10

可以看到和我们画的转化图是一样的。如果我们将 node1 和 node2 交换一下,先添加 node2 再添加 node1,使 node1 先进行绘制,那么结果会是什么样呢?

flex.insert(node2);
flex.insert(node1);

Flutter 绘制流程分析与代码实践_flutter_11

可以看到依旧是 4 个 Layer 节点,node2 生成的 PictureLayer 依旧是 TransformLayer 的子节点。

我们看一下 RenderFlex 是如何绘制子节点的,我们通过调试进入 RenderFlex 的 ​​paint()​​​ 方法,可以看到它调用的是 ​​paintDefault()​​,也就是进行遍历依次调用当前 PaintingContext 的 ​​paintChild()​​ 绘制子节点。

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;
}
}

RenderFlex 循环绘制时,父节点和下面的子节点用的都是同一个 PaintingContex。由于 node1 是单独绘制,因此会创建一个新的 PaintingContext 和 OffsetLayer,但绘制 node2 时还是使用父节点的 PaintingContext,所以 flex 和 node2 会一起生成一个 PictureLayer 添加到根节点中。

结语

本文到这里就结束了,我们现在可以看到,Flutter 性能高的一个很重要的原因,就是它在资源复用和避免不必要计算等方面,做了很多思考。

之所以研究这部分,是因为这部分是 Flutter Framework 层最贴近 Engine 层的内容,对以后研究 Flutter 和 Android 两者在 Engine 层的异同点会有很大帮助。