原文傳送門:
上一篇文章介紹了繪製的啟動與佈局的計算過程,這裡接著繪製流程CompositingBits
、flushPaint
及compositeFrame
繼續分析。
繪製原理
在開始探索繪製流程之前,我們先看看不使用Flutter Framework
的Widget
時,如何渲染出一個圖形
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()
中進行光柵化合成上屏。注意這裡的PictureRecorder
、SceneBuilder
都是一次性的,當呼叫完recorder.endRecording
和scene.dispose()
之後就不能再使用了。
看完上面流程讓我們有個概念,Flutter的Paint
和Composited
流程都是圍繞上面的過程來進行的。
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中我們使用RenderObject
的isRepaintBoundary
來負責控制圖層。
我們要在子類中重寫isRepaintBoundary=>true
,這樣可以讓父節點重新渲染時不重新渲染自己。
目前2.2.2版本中,所有自帶isRepaintBoundary
屬性的Widget
有TextField
、CupertinoTextSelectionToolbar
、RenderEditable
、SingleChildScrollView
、Flow
、AndroidView
、UiKitView
、PlatformViewSurface
、Texture
、RenderView
、RepaintBoundary
,其中RepaintBoundary
是開放給開發者自行使用的。
flushPaint
Paint過程
通過呼叫一系列canvas
Api來構建一棵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 {
// ...
}
}
複製程式碼
同layout
及compositingBits
類似,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是每個節點繪製的開始的方法,需要子類自行實現繪製目標,它通過傳入一個PaintingContext
和Offset
- PaintingContext: 持有
PictureRecorder
和Canvas
等繪製類,並封裝了一些常用的繪製方法,是繪製核心類 - Offset: 繪製區域,通過父節點計算出當前節點繪製的區域,繪製時在此區域進行繪製
RenderObject
中只定義了一個paint
空方法,需要子類進行實現,例如ColoredBox
元件的_RenderColoredBox
中paint
實現:
[-> 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
進行一系列初始化,此時會先初始化一個PictureLayer
,PictureLayer
是一個能夠具有繪製能力的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
傳入一個Scene
,Scene
只能通過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子類自行實現,通過傳入的SceneBuilder
,SceneBuilder
中有很多方法,會將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
嵌入紋理的使用
我們看看OffsetLayer
的addToScene
方法
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
時通過SceneBuilder
的addPicture
方法傳入。
addToScene
從Layer樹
根節點進行,當遇到有孩子節點Layer物件
時,會呼叫addChildrenToScene
對所有孩子節點呼叫addToScene
,這樣,每一次compositeFrame
流程初始化的SceneBuilder
就會貫穿整個Layer樹
(上面說了SceneBuilder
是一次性的)。
最後在上面的compositeFrame
方法中,通過builder.build
生成Scene
物件並通過window.render
發給GPU,這樣整個頁面就渲染出來了。