Flutter渲染之繪製上屏

JSShou發表於2021-08-04

原文傳送門:

上一篇文章介紹了繪製的啟動與佈局的計算過程,這裡接著繪製流程CompositingBitsflushPaintcompositeFrame繼續分析。

繪製原理

在開始探索繪製流程之前,我們先看看不使用Flutter FrameworkWidget時,如何渲染出一個圖形

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();
}
複製程式碼

在這個例子中,我建立了兩個圖形,一個紅色的正方形,一個藍色的圓形,先看看效果。

在Flutter中,我們的Canvas物件需要通過PictureRecorder,當呼叫一系列Canvas操作之後(Canvas操作不熟悉的請看我之前的文章Flutter canvas學習之基礎知識),需要呼叫recorder.endRecording()來結束此Canvas操作並返回一個Picture物件,此Picture物件實際上就是一個圖層了,然後我們使用SceneBuilder將這個Picture物件新增進去,並生成一個Scene物件,Scene物件就是控制著整個螢幕繪製的類,我們要將它傳入window.render()中進行光柵化合成上屏。注意這裡的PictureRecorderSceneBuilder都是一次性的,當呼叫完recorder.endRecordingscene.dispose()之後就不能再使用了。

看完上面流程讓我們有個概念,Flutter的PaintComposited流程都是圍繞上面的過程來進行的。

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();
}
複製程式碼

_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;
}
複製程式碼

CompositingBits的作用就是將當前節點的_needsCompositing值確定,如果當前isRepaintBoundary或者alwaysNeedsCompositing為true時,或者子孫節點有一個滿足isRepaintBoundary或者alwaysNeedsCompositing為true時,那麼_needsCompositing也會為true。在這個過程中,它會更新具有髒合成位的任何渲染物件。

Paint

接下來看看渲染非常重要的流程之一——Paint過程

RepaintBoundary

在開始Paint流程之前,我們先確定一個概念RepaintBoundary,我們知道現代的UI系統都會進行介面的圖層劃分,這樣可以進行圖層複用,減少繪製量,提升繪製效能。在Flutter中我們使用RenderObjectisRepaintBoundary來負責控制圖層。

我們要在子類中重寫isRepaintBoundary=>true,這樣可以讓父節點重新渲染時不重新渲染自己。

目前2.2.2版本中,所有自帶isRepaintBoundary屬性的WidgetTextFieldCupertinoTextSelectionToolbarRenderEditableSingleChildScrollViewFlowAndroidViewUiKitViewPlatformViewSurfaceTextureRenderViewRepaintBoundary,其中RepaintBoundary是開放給開發者自行使用的。

flushPaint

Paint過程通過呼叫一系列canvasApi來構建一棵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 {
    // ...
  }
}
複製程式碼

layoutcompositingBits類似,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();
  }
}
複製程式碼

根據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;
  }
}
複製程式碼

_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();
}
複製程式碼

_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) {
  }
}
複製程式碼

_paintWithContext在開發階段會做一些繪製前的檢查,同時根據當前_needsPaint來判斷是否需要繪製,最後會呼叫paint方法進行繪製。

paint

paint是每個節點繪製的開始的方法,需要子類自行實現繪製目標,它通過傳入一個PaintingContextOffset

  • PaintingContext: 持有PictureRecorderCanvas等繪製類,並封裝了一些常用的繪製方法,是繪製核心類
  • Offset: 繪製區域,通過父節點計算出當前節點繪製的區域,繪製時在此區域進行繪製

RenderObject中只定義了一個paint空方法,需要子類進行實現,例如ColoredBox元件的_RenderColoredBoxpaint實現:

[-> 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);
    }
  }
複製程式碼

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!);
}
複製程式碼

如果當前節點的_canvas沒有被初始化,那麼會呼叫_startRecording進行一系列初始化,此時會先初始化一個PictureLayerPictureLayer是一個能夠具有繪製能力的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 {
      // ...
    }
  }
複製程式碼

當呼叫了window.render就會開始繪製了。window.render傳入一個SceneScene只能通過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;
  }
}
複製程式碼

addToScene由Layer子類自行實現,通過傳入的SceneBuilderSceneBuilder中有很多方法,會將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';
複製程式碼

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嵌入紋理的使用

我們看看OffsetLayeraddToScene方法

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();
}
複製程式碼

我們通過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;
  }
  // ...
  @override
  void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
    builder.addPicture(layerOffset, picture!, isComplexHint: isComplexHint, willChangeHint: willChangeHint);
  }
  //...
}
複製程式碼

PictureLayer中持有了一個儲存了一塊區域繪製資訊的Picture類,在addToScene時通過SceneBuilderaddPicture方法傳入。

addToSceneLayer樹根節點進行,當遇到有孩子節點Layer物件時,會呼叫addChildrenToScene對所有孩子節點呼叫addToScene,這樣,每一次compositeFrame流程初始化的SceneBuilder就會貫穿整個Layer樹(上面說了SceneBuilder是一次性的)。

最後在上面的compositeFrame方法中,通過builder.build生成Scene物件並通過window.render發給GPU,這樣整個頁面就渲染出來了。

相關文章