< 返回

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

2022年1月11日 FlutterAndroid

# 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


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 进行关联。


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

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

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
('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

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

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

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


bool get sizedByParent => true;


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

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

  
  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] 方法创建
  
  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


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


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

# 场景渲染

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

/// RenderBiding

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 启动的时候这个方法的注册调用链如下:

注册调用链.png

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


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.onDrawFramewindow.onBeginFrame注册的回调,从而刷新界面。

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

drawFrame调用链

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


# 整理图

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

yuque_diagram_1


# Framework 项目代码实验

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

首先根据官方说明配置一个 Framework 开发环境,然后进入到 hello_world 项目中: https://github.com/flutter/flutter/wiki/Setting-up-the-Framework-development-environment (opens new window)

实验项目和平时开发一样依旧采用 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);
}

03.png

上面的代码会在界面绘制一条白线,由于这里只 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);
}

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

04.gif

接下来我们将上面的直线封装为一个自定义的 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);
  }

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

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

  
  bool get sizedByParent => true;

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

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

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

接着是 main 方法中:

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

05.gif

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

06.png

我们发现两个节点的绘制次数都是 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;

  
  bool get isRepaintBoundary => _isRepaintBoundary;

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

我们先来演示两种情况:

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

07.png

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

08.png

可以看到 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 图示如下:

yuque_diagram_2

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

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

09.png

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

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

10.png

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

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

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 层的异同点会有很大帮助。

由于涉及到的东西特别多,因此并没办法讲的很全面,本文所要介绍的内容也可能存在遗漏,但按照本文讲解的流程去阅读源码进行扩展,并不难弄懂,有问题可在评论区留言。