哈羅大家好钥平,好久不見,隔了很多天了好久沒更新了姊途,最近事情太多了而且狀態(tài)也不是很好一直沒有更新涉瘾,趁著今天公司年會沒有任務(wù),大家都在組隊王者榮耀吭净,我就偷個閑睡汹,今天我們繼續(xù)來講解Flutter運行原理篇的內(nèi)容,也算是我們新年的第一篇吧寂殉,Let ’s go
上一篇講到了《Flutter運行原理篇之layout布局的過程》最后面的彩蛋我們提到了會講到paint重繪囚巴,所以今天我們就來講講《Flutter運行原理篇之paint重繪的原理》,讓我們愉快的開始吧
在開始之前我們先要簡單的了解幾個概念:
Canvas:封裝了Flutter Skia各種繪制指令友扰,比如畫線彤叉、畫圓、畫矩形等指令村怪,這個與我們在移動端以及Web端的Canvas概率上沒有本質(zhì)的區(qū)別秽浇,Canvas 繪制完成后,通過 PictureRecorder 獲取繪制產(chǎn)物甚负,然后將其保存在 Layer 中
layout:也稱作圖層柬焕,可以分為容器類和繪制類兩種;可以理解為繪制就是在圖層上面進行的梭域,比如調(diào)用 Canvas 的繪制 API 后斑举,相應(yīng)的繪制產(chǎn)物被保存在 PictureLayer.picture 對象中, 最常見的layout包括:
- OffsetLayer:根 Layer病涨,它繼承自ContainerLayer富玷,而ContainerLayer繼承自 Layer 類,我們將直接繼承自ContainerLayer 類的 Layer 稱為容器類Layer,容器類 Layer 可以添加任意多個子Layer赎懦。
- PictureLayer:保存繪制產(chǎn)物的 Layer雀鹃,它直接繼承自 Layer 類。我們將可以直接承載(或關(guān)聯(lián))繪制結(jié)果的 Layer 稱為繪制類 Layer励两,相應(yīng)的繪制產(chǎn)物PictureRecorder被保存在 PictureLayer.picture 對象中
RepaintBoundary:邊界節(jié)點Widget黎茎,顧名思義也就是繪制的邊界節(jié)點,繪制只能最多影響到離他最近的一個邊界節(jié)點当悔,邊界之外的Widget并不受影響
isRepaintBoundary:bool類型工三,意思是申明Widget自己是邊界節(jié)點,作用與上面異曲同工
好了先鱼,了解了這幾個概念以后我們再來看看paint的步驟,與build和layout的內(nèi)容一樣奸鬓,我們還是分為兩步來講解paint的重繪的過程焙畔,分別是第一次重繪,與更新widget以后的重繪串远,讓我們先來看看第一次重繪的內(nèi)容:
第一次重繪肯定是從根RenderObject也就是RenderView開始:
RenderView.paint
@override
void paint(PaintingContext context, Offset offset) {
if (child != null)
context.paintChild(child!, offset);
}
PaintingContext.paintChild
void paintChild(RenderObject child, Offset offset) {
assert(() {
if (debugProfilePaintsEnabled)
Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
debugOnProfilePaint?.call(child);
return true;
}());
if (child.isRepaintBoundary) { //判斷是否邊界節(jié)點宏多,因為RenderView肯定是邊界節(jié)點
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset);
}
assert(() {
if (debugProfilePaintsEnabled)
Timeline.finishSync();
return true;
}());
}
RenderView.isRepaintBoundary
@override
bool get isRepaintBoundary => true; //屬性為true
看看RenderView的isRepaintBoundary屬性,說明RenderView肯定是邊界節(jié)點澡罚,上面If里面判斷了是否邊界節(jié)點伸但,因為RenderView肯定是邊界節(jié)點,然后往下進行
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
assert(() {
if (debugRepaintRainbowEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(estimatedBounds.deflate(3.0), paint);
}
if (debugPaintLayerBordersEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFF9800);
canvas.drawRect(estimatedBounds, paint);
}
return true;
}());
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
stopRecordingIfNeeded 在下面我講解留搔,這里先略過
void _compositeChild(RenderObject child, Offset offset) {
assert(!_isRecording);
assert(child.isRepaintBoundary);
assert(_canvas == null || _canvas!.getSaveCount() == 1);
// Create a layer for our child, and paint the child into it.
if (child._needsPaint) {
repaintCompositedChild(child, debugAlsoPaintedParent: true);
} else {
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: true,
includedChild: false,
);
child._layer!.debugCreator = child.debugCreator ?? child;
return true;
}());
}
assert(child._layer is OffsetLayer);
final OffsetLayer childOffsetLayer = child._layer! as OffsetLayer;
childOffsetLayer.offset = offset;
appendLayer(child._layer!);
}
這個函數(shù)里面有3個地方需要注意的:
- 繪制孩子節(jié)點時更胖,如果遇到邊界節(jié)點且當(dāng)其不需要重繪(_needsPaint 為 false) 時,會直接復(fù)用該邊界節(jié)點的 layer隔显,而無需重繪却妨!這就是邊界節(jié)點能跨 frame 復(fù)用的原理(可以理解為重繪如果遇到的是邊界節(jié)點可以復(fù)用,非邊界節(jié)點則需要重繪括眠,理解這個非常重要彪标,務(wù)必仔細(xì)體會)。
- 因為邊界節(jié)點的layer類型是ContainerLayer掷豺,所以是可以給它添加子節(jié)點捞烟。
- 注意是將當(dāng)前邊界節(jié)點的layer添加到父邊界節(jié)點,而不是父節(jié)點当船。
PaintingContext.repaintCompositedChild
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,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: debugAlsoPaintedParent,
includedChild: true,
);
return true;
}());
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
child._layer = childLayer = OffsetLayer(); //childLayer如果為空的話题画,就new一個OffsetLayer
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
}
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(child._layer!, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
assert(identical(childLayer, child._layer));
childContext.stopRecordingIfNeeded();
}
childLayer如果為空的話,就new一個OffsetLayer賦值給 child._layer = childLayer生年,然后把這個構(gòu)造PaintingContext傳給下面child繪制使用
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
RenderObject? debugLastActivePaint;
assert(() {
_debugDoingThisPaint = true;
debugLastActivePaint = _debugActivePaint;
_debugActivePaint = this;
assert(!isRepaintBoundary || _layer != null);
return true;
}());
_needsPaint = false;
try {
paint(context, offset);
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
} catch (e, stack) {
_debugReportException('paint', e, stack);
}
assert(() {
debugPaint(context, offset);
_debugActivePaint = debugLastActivePaint;
_debugDoingThisPaint = false;
return true;
}());
}
把PaintingContext傳給paint方法婴程,paint方法里面一般重繪完自身以后都會調(diào)用PaintingContext.paintChild方法去重繪子類,也會把這個PaintingContext一直傳下去抱婉,PaintingContext的ContainerLayerd的屬性就是OffsetLayer
PaintingContext.paintChild
void paintChild(RenderObject child, Offset offset) {
assert(() {
if (debugProfilePaintsEnabled)
Timeline.startSync('${child.runtimeType}', arguments: timelineArgumentsIndicatingLandmarkEvent);
debugOnProfilePaint?.call(child);
return true;
}());
if (child.isRepaintBoundary) {
stopRecordingIfNeeded();
_compositeChild(child, offset);
} else {
child._paintWithContext(this, offset); //非邊界節(jié)點档叔,直接傳遞this作為child的PaintingContext
}
assert(() {
if (debugProfilePaintsEnabled)
Timeline.finishSync();
return true;
}());
}
PaintingContext.paintChild方法里面也先調(diào)用stopRecordingIfNeeded桌粉,再調(diào)用_compositeChild,這里也就構(gòu)成了一個遞歸循環(huán)的調(diào)用一直到葉子節(jié)點的RenderObject的paint方法衙四,如果他不是邊界節(jié)點的話直接傳遞this作為child的PaintingContext铃肯,這里也就說明了如果沒有邊界節(jié)點的話那么所有的RenderObject都是在一個PaintingContext也就是一個ContainerLayer的圖層里面繪制的,如果有邊界截點的話那么就會重新申請新的ContainerLayer并且在新的圖層上面繪制
感覺內(nèi)容有點多传蹈,我借用網(wǎng)上的例子來給大家說一下押逼,下圖左邊是 widget 樹,右邊是最終生成的Layer樹惦界,我們看一下生成過程:
- 一開始繪制會從RenderView開始挑格,因為他是一個繪制邊界節(jié)點(我們上面已經(jīng)講了什么是邊界節(jié)點),在第一次繪制時會為他創(chuàng)建一個 OffsetLayer沾歪,我們記為 OffsetLayer1漂彤,接下來 OffsetLayer1會傳遞給child
- 由于 Row 是一個容器類組件且不需要繪制自身,那么接下來他會繪制自己的孩子灾搏,它有兩個孩子挫望,先繪制第一個孩子Column1,將 OffsetLayer1 傳給 Column1狂窑,而 Column1 也不需要繪制自身媳板,那么它又會將 OffsetLayer1 傳遞給第一個子節(jié)點Text1。
- Text1 需要繪制文本泉哈,他會使用 OffsetLayer1進行繪制蛉幸,由于 OffsetLayer1 是第一次繪制,所以會新建一個PictureLayer1和一個 Canvas1 丛晦,然后將 Canvas1 和PictureLayer1 綁定巨缘,接下來文本內(nèi)容通過 Canvas1 對象繪制,Text1 繪制完成后采呐,Column1 又會將 OffsetLayer1 傳給 Text2 若锁。
- Text2 也需要使用 OffsetLayer1 繪制文本,但是此時 OffsetLayer1 已經(jīng)不是第一次繪制斧吐,所以會復(fù)用之前的 Canvas1 和 PictureLayer1又固,調(diào)用 Canvas1來繪制文本。
- Column1 的子節(jié)點繪制完成后煤率,PictureLayer1 上承載的是Text1 和 Text2 的繪制產(chǎn)物仰冠。
- 接下來 Row 完成了 Column1 的繪制后,開始繪制第二個子節(jié)點 RepaintBoundary蝶糯,Row 會將 OffsetLayer1 傳遞給 RepaintBoundary洋只,由于它是一個繪制邊界節(jié)點,且是第一次繪制,則會為它創(chuàng)建一個 OffsetLayer2识虚,接下來 RepaintBoundary 會將 OffsetLayer2 傳遞給Column2肢扯,和 Column1 不同的是,Column2 會使用 OffsetLayer2 去繪制 Text3 和 Text4担锤,繪制過程同Column1蔚晨,在此不再贅述。
- 當(dāng) RepaintBoundary 的子節(jié)點繪制完時肛循,要將 RepaintBoundary 的 layer( OffsetLayer2 )添加到父級Layer(OffsetLayer1)中铭腕。
整棵組件樹繪制完成,生成了一棵右圖所示的 Layer 樹多糠。需要說名的是 PictureLayer1 和 OffsetLayer2 是兄弟關(guān)系累舷,它們都是 OffsetLayer1 的孩子。通過上面的例子我們至少可以發(fā)現(xiàn)一點:同一個 Layer 是可以多個組件共享的夹孔,比如 Text1 和 Text2 共享 PictureLayer1笋粟,共享的話也會導(dǎo)致一個問題,比如 Text1 文本發(fā)生變化需要重繪會連帶著 Text2 也必須重繪析蝴,這個也是為什么有時候我們合理的使用RepaintBoundray可以提高重繪的效率了,就像上圖如果Text3的重繪只會影響到Column2而不會再影響到Row
為什么會有共享這種機制了绿淋,其實究其原因其實還是為了節(jié)省資源闷畸,Layer 太多時 Skia 會比較耗資源,所以這其實是一個trade-off吞滞。
上面提到了Canvas的使用佑菩,(一般來說在paint方法繪制自身的時候回使用到Canvas來進行繪制)所以讓我來看看代碼他是怎么使用的:
Canvas get canvas {
//如果canvas為空,則是第一次獲炔迷殿漠;
if (_canvas == null) _startRecording();
return _canvas!;
}
//創(chuàng)建PictureLayer和canvas
void _startRecording() {
_currentLayer = PictureLayer(estimatedBounds);
_recorder = ui.PictureRecorder();
_canvas = Canvas(_recorder!);
//將pictureLayer添加到_containerLayer(是繪制邊界節(jié)點的Layer)中
_containerLayer.append(_currentLayer!);
}
調(diào)用get屬性的時候會判斷為空則創(chuàng)建PictureLayer,以及ui.PictureRecorder(相應(yīng)的繪制產(chǎn)物PictureRecorder被保存在 PictureLayer.picture 對象中)佩捞,以及_canvas绞幌,將pictureLayer添加到_containerLayer(是繪制邊界節(jié)點的OffsetLayer)中
我們再來看上面遺留的stopRecordingIfNeeded,這個里面與_startRecording做了相反的操作一忱,只是把ui.PictureRecorder保存下來以后莲蜘,PictureLayer,ui.PictureRecorder帘营,_canvas都做了置空的操作票渠,這里面有個小細(xì)節(jié)有朋友可能會問為什么_currentLayer(PictureLayer)置空了,_currentLayer!.picture還能保留下來呢芬迄,這個是因為我們在_startRecording的時候把加入到了_containerLayer中了问顷,所以_containerLayer已經(jīng)保存了對象的引用,而_currentLayer只是另一個引用而已
void stopRecordingIfNeeded() {
if (!_isRecording)
return;
assert(() {
if (debugRepaintRainbowEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 6.0
..color = debugCurrentRepaintColor.toColor();
canvas.drawRect(estimatedBounds.deflate(3.0), paint);
}
if (debugPaintLayerBordersEnabled) {
final Paint paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color(0xFFFF9800);
canvas.drawRect(estimatedBounds, paint);
}
return true;
}());
_currentLayer!.picture = _recorder!.endRecording();
_currentLayer = null;
_recorder = null;
_canvas = null;
}
好了,上面是我們從RenderView第一次繪制的時候大概流程杜窄,接下來我們再看看更新widget以后的重繪是什么樣子的呢:
首先我們還是回顧一下上一篇《Flutter運行原理篇之layout布局的過程》最后面提到的彩蛋(Widget更新會導(dǎo)致build肠骆,繼而導(dǎo)致layout):
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject? relayoutBoundary;
// 先確定當(dāng)前組件的布局邊界
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
// _needsLayout 表示當(dāng)前組件是否被標(biāo)記為需要布局
// _constraints 是上次布局時父組件傳遞給當(dāng)前組件的約束
// _relayoutBoundary 為上次布局時當(dāng)前組件的布局邊界
// 所以,當(dāng)當(dāng)前組件沒有被標(biāo)記為需要重新布局羞芍,且父組件傳遞的約束沒有發(fā)生變化哗戈,
// 且布局邊界也沒有發(fā)生變化時則不需要重新布局,直接返回即可荷科。
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
// 如果需要布局唯咬,緩存約束和布局邊界
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
// 后面解釋
if (sizedByParent) {
performResize();
}
// 執(zhí)行布局
performLayout();
// 布局結(jié)束后將 _needsLayout 置為 false
_needsLayout = false;
// 將當(dāng)前組件標(biāo)記為需要重繪(因為布局發(fā)生變化后,需要重新繪制)
markNeedsPaint();
}
Layout 最后對于子節(jié)點(不再是容器的RenderObject)最后都會調(diào)用markNeedsPaint方法畏浆,重這里開始我們就開始paint重繪的過程了胆胰,我們先看看這個函數(shù)
RenderObject.markNeedsPaint
void markNeedsPaint() {
if (_needsPaint) return;
_needsPaint = true;
if (isRepaintBoundary) { // 如果是當(dāng)前節(jié)點是邊界節(jié)點
owner!._nodesNeedingPaint.add(this); //將當(dāng)前節(jié)點添加到需要重新繪制的列表中。
owner!.requestVisualUpdate(); // 請求新的frame刻获,該方法最終會調(diào)用scheduleFrame()
} else if (parent is RenderObject) { // 若不是邊界節(jié)點且存在父節(jié)點
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint(); // 遞歸調(diào)用父節(jié)點的markNeedsPaint
} else {
// 如果是根節(jié)點蜀涨,直接請求新的 frame 即可
if (owner != null)
owner!.requestVisualUpdate();
}
}
下判斷是否是邊界節(jié)點,如果是的話直接加入到了_nodesNeedingPaint上面蝎毡,如果存在父節(jié)點又不是邊界的話直接調(diào)用parent.markNeedsPaint繼續(xù)向上遞歸查找
接著我再幫大家回顧一下整個Widget的更新運行要經(jīng)過5個步驟:
void drawFrame() {
buildOwner!.buildScope(renderViewElement!); // 1.重新構(gòu)建widget
super.drawFrame();
//下面幾個是在super.drawFrame()執(zhí)行的
pipelineOwner.flushLayout(); // 2.更新布局
pipelineOwner.flushCompositingBits(); //3.更新“層合成”信息
pipelineOwner.flushPaint(); // 4.重繪
if (sendFramesToEngine) {
renderView.compositeFrame(); // 5. 上屏厚柳,將繪制出的bit數(shù)據(jù)發(fā)送給GPU
}
}
上面的build,layout我們都已經(jīng)介紹過了沐兵,忘記的朋友可以出門左轉(zhuǎn)即可别垮,接下來是合成的內(nèi)容,這個我們往下先放一放(因為合成的目的就是為了重繪)扎谎,接下來就到了重繪碳想,也就是我們今天的內(nèi)容
那我們就先來看看 pipelineOwner.flushPaint(); 函數(shù):
PipelineOwner.flushPaint()
void flushPaint() {
if (!kReleaseMode) {
Timeline.startSync('Paint', arguments: timelineArgumentsIndicatingLandmarkEvent);
}
assert(() {
_debugDoingPaint = true;
return true;
}());
try {
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)) {
assert(node._layer != null);
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
assert(_nodesNeedingPaint.isEmpty);
} finally {
assert(() {
_debugDoingPaint = false;
return true;
}());
if (!kReleaseMode) {
Timeline.finishSync();
}
}
}
這里面會對于dirtyNodes(也就是我們上面添加的_nodesNeedingPaint)里面的每一個node都會執(zhí)行PaintingContext.repaintCompositedChild(node);方法,其實這個過程我們上面已經(jīng)分析過了毁靶,這部分內(nèi)容與第一次重繪的過程是一樣的:
PaintingContext.repaintCompositedChild
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,
}) {
assert(child.isRepaintBoundary);
assert(() {
// register the call for RepaintBoundary metrics
child.debugRegisterRepaintBoundaryPaint(
includedParent: debugAlsoPaintedParent,
includedChild: true,
);
return true;
}());
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
assert(debugAlsoPaintedParent);
// Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
child._layer = childLayer = OffsetLayer();
} else {
assert(debugAlsoPaintedParent || childLayer.attached);
childLayer.removeAllChildren();
}
assert(identical(childLayer, child._layer));
assert(child._layer is OffsetLayer);
assert(() {
child._layer!.debugCreator = child.debugCreator ?? child.runtimeType;
return true;
}());
childContext ??= PaintingContext(child._layer!, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
// Double-check that the paint method did not replace the layer (the first
// check is done in the [layer] setter itself).
assert(identical(childLayer, child._layer));
childContext.stopRecordingIfNeeded();
}
現(xiàn)在我們已經(jīng)說清楚了paint重繪的整個流程胧奔,如果你讀到這里的話那么你應(yīng)該清楚了從Widget更新開始經(jīng)過build,到layout再到paint的大致流程應(yīng)該清楚了预吆,給大家推一篇博客《繪制原理及Layer》我今天這篇文章部分內(nèi)容來自這篇博客龙填,說實話如果你有耐心的話我還是推薦你去看完他整篇內(nèi)容,好了拐叉,我們今天的內(nèi)容就到這里了觅够,新年到來之際還是祝大家新年快樂,升職加薪巷嚣,最重要的是身體健康喘先,天天開心,下期見···