相對於React Native
、Weex
等跨平臺框架,Flutter
擁有自己的UI繪製體系,避免了React Native
、Weex
等跨平臺框架與Native
系統的橋接,從而更好的提升了效能。
在Flutter
中,UI都是一幀一幀的繪製,但這繪製的背後都會經過如下階段。
- 動畫與微任務階段,主要是處理動畫及執行一系列微任務。
- 構建階段(build),找出標記為“髒”的節點與佈局邊界之間的所有節點,並做相應的更新。
- 佈局階段,計算
Widget
的大小及位置的確定。 - compositingBits階段,重繪之前的預處理操作,檢查RenderObject是否需要重繪。
- 繪製階段,根據
Widget
大小及位置來繪製UI。 - compositing階段,將UI資料傳送給GPU處理。
- semantics階段,與平臺的輔助功能相關。
- finalization階段,主要是從
Element
樹中移除無用的Element
物件及處理繪製結束回撥。
下面就來分析上述的各個階段
1、動畫與微任務階段
該階段主要是處理動畫及微任務。先來看動畫的處理,在使用動畫時,很多時候都會新增一個回撥函式來進行狀態獲取或資料更新,如通過addListener
、addStatusListener
等函式來新增,而這些回撥函式就會在本階段來執行。具體是在SchedulerBinding
中的handleBeginFrame
函式中實現。
void handleBeginFrame(Duration rawTimeStamp) {
...
try {
// TRANSIENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.transientCallbacks;
//切換為transientCallbacks階段
final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
//清空已註冊的回撥函式
_transientCallbacks = <int, _FrameCallbackEntry>{};
//遍歷所有註冊的回撥方法
callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
if (!_removedIds.contains(id))
//執行已經註冊的回撥函式
_invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
});
_removedIds.clear();
} finally {
//切換為midFrameMicrotasks階段
_schedulerPhase = SchedulerPhase.midFrameMicrotasks;
}
}
複製程式碼
_invokeFrameCallback
就會呼叫在使用動畫時註冊的回撥函式,這裡僅執行一次。如果我們在執行時呼叫_invokeFrameCallback
函式的程式碼註釋調,那麼就無法獲取動畫的狀態,從而影響動畫的正確執行。
當回撥函式執行完畢後,就會進入微任務階段,在該階段會執行一系列微任務,由於這涉及到Flutter
的非同步任務體系,因此這裡就不再敘述。
2、build階段
在上一階段執行完畢後,就進入build階段,在該階段主要是重新構建標記為“髒”的Widget
節點及將需要更新的RenderObject
物件標記為“髒”。
當handleBeginFrame
函式執行完畢後,就會執行handleDrawFrame
函式,該函式在SchedulerBinding
物件初始化時會與Window
相關聯,所以除第一次需要主動呼叫外,其他時候皆是通過Window
來呼叫該函式。
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
Timeline.finishSync(); // end the "Animate" phase
try {
//持久幀回撥,該回撥會一直存在,不會移除
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
//當前幀繪製完成回撥
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
//當執行這裡時,代表當前幀已經繪製完畢
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
//進入空閒狀態
_schedulerPhase = SchedulerPhase.idle;
Timeline.finishSync(); // end the Frame
profile(() {
_profileFrameStopwatch.stop();
_profileFramePostEvent();
});
_currentFrameTimeStamp = null;
}
}
複製程式碼
這裡重點關注持久幀回撥,該回撥也是UI繪製的關鍵函式,是在RendererBinding
物件初始化時註冊的。
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override
void initInstances() {
super.initInstances();
...
//註冊持久幀回撥
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) {
//繪製幀
drawFrame();
}
//繪製幀
void drawFrame() {
//對Widget進行測量、佈局
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
//對Widget進行繪製
pipelineOwner.flushPaint();
//傳送資料給GPU
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
}
複製程式碼
根據函式名可以發現並沒有發現關於構建Widget
的相關函式,那麼在何時構建尼?通過檢視原始碼可以發現,在WidgetsBinding
中重寫了drawFrame
函式。在該函式中會建立新的Widget
物件替換舊的Widget
物件並將不需要的Element
節點從樹中移除。
@override
void drawFrame() {
...
try {
//如果根結點存在,就重新構建Widget
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
//呼叫RendererBinding中的drawFrame函式
super.drawFrame();
//移除不再使用的Element節點
buildOwner.finalizeTree();
} finally {...}
...
}
複製程式碼
2.1、重新build Widget物件
Widget
物件的建立是在buildScope()
函式中實現的,這是一個非常重要的函式,具體實現如下。
void buildScope(Element context, [ VoidCallback callback ]) {
if (callback == null && _dirtyElements.isEmpty)
return;
try {
//“髒”節點列表需要重新排序
_scheduledFlushDirtyElements = true;
...
//將標記為“髒”的Element節點根據深度進行排序
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
//標記為“髒”的Element節點數量
int dirtyCount = _dirtyElements.length;
int index = 0;
//遍歷“髒”節點
while (index < dirtyCount) {
try {
//重新構建Widget,及是否複用當前Element
_dirtyElements[index].rebuild();
} catch (e, stack) {
...
}
index += 1;
//當_dirtyElements集合中的“髒”節點還未處理完畢時,又新增了新的“髒”節點
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
//根據“髒”節點的深度進行排序
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length;
//如果當前節點的深度比新加入的“髒”節點深度要深,則需要將處理座標指向新加入的“髒”節點
while (index > 0 && _dirtyElements[index - 1].dirty) {
index -= 1;
}
}
}
} finally {
//清除_dirtyElements中所有節點的“髒”狀態
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
Timeline.finishSync();
}
}
複製程式碼
_dirtyElements
是一個集合,儲存了所有標記為“髒”的節點。在對其中的“髒”節點進行處理時,需要首先對集合中的“髒”節點進行排序,其排序規則如下。
- 如果“髒”節點的深度不同,則按照深度進行升序排序
- 如果“髒”節點的深度相同,則會將“髒”節點放在集合的右側,“乾淨”節點則在在集合的左側。
在排序完成後,就要遍歷該集合,對其中的“髒”節點進行處理。在這裡呼叫的是rebuild
函式,通過該函式,會重新建立“髒”節點下的所有Widget
物件,並根據新的Widget
物件來判斷是否需要重用Element
物件。一般只要不是增刪Widget
,Element
物件都會被重用,從而也就會重用RenderObject
物件。由於Widget
是一個非常輕量級的資料結構,所以在UI更新時做到了把效能損耗降到最低。
這裡要注意一點的是,如果_dirtyElements
中的“髒”節點還未處理完畢,就又新增了“髒”節點,那麼這時候就會重新排序,保證_dirtyElements
集合的左側永遠是“乾淨”節點,右側永遠是“髒”節點。
由於rebuild
函式比較重要,這裡就重點介紹一下該函式,在rebuild
函式中會呼叫performRebuild
函式,該函式是一個抽象函式,在其子類實現,而標記為“髒”的Element
都是StatefulElement
。所以就來StatefulElement
或者其父類中查詢performRebuild
函式。
abstract class ComponentElement extends Element {
...
@override
void performRebuild() {
Widget built;
try {
//重新建立新的`Widget`物件
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
//當構建Widget物件出錯時展示的預設頁面,可以修改該頁面來使異常介面更友好的顯示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
} finally {
//清除“髒”標記
_dirty = false;
}
try {
//更新子Element對應的Widget物件
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
//當構建Widget物件出錯時展示的預設頁面
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
_child = updateChild(null, built, slot);
}
}
}
複製程式碼
performRebuild
函式做的事很簡單,就是建立新的Widget
物件來替換舊的物件。上面的build
函式呼叫的就是State
類中的build
函式,然後再呼叫Element
的updateChild
函式,該函式在Flutter之Widget層級介紹中進行了簡單的介紹,就是更新Element
對應的Widget
物件。而在updateChild
函式中又會呼叫子Element
的update
函式,從而呼叫子Element
的performRebuild
,然後在呼叫子Element
的updateChild
、update
函式。以此類推,從而更新其所有子Element
的Widget
物件。
updateRenderObject
函式來更新RenderObject
。在更新RenderObject
物件時,會根據情況來對需要重新佈局及重新繪製的RenderObject
物件進行標記。然後等待下一次的Vsync訊號時來重新佈局及繪製UI。
2.2、標記RenderObject
對於RenderObject
物件,可以通過markNeedsLayout
及markNeedsPaint
來標記是否需要重新佈局及重新繪製。但在當前階段只會呼叫markNeedsLayout
來標記需要重新佈局的RenderObject
物件,在下一階段才會標記需要重新繪製的RenderObject
,所以先來看markNeedsLayout
函式。
void markNeedsLayout() {
...
//判斷佈局邊界是否是是當前RenderObject物件
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
//標記當前RenderObject及其子RenderObject物件需要重新佈局
//將當前`RenderObject`新增到集合中。
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
@protected
void markParentNeedsLayout() {
_needsLayout = true;
final RenderObject parent = this.parent;
if (!_doingThisLayoutWithCallback) {
//呼叫父類的markNeedsLayout函式
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
assert(parent == this.parent);
}
複製程式碼
markNeedsLayout
函式的程式碼實現很簡單,就是不斷遍歷父RenderObject
物件,從而找到佈局邊界的RenderObject
物件,並將該RenderObject
物件新增到集合_nodesNeedingLayout
中,然後在下一階段就從該物件開始佈局。
在這裡有個“佈局邊界”的概念,在Flutter
中,可以給任意節點設定佈局邊界,即當邊界內的任何物件發生重新佈局時,不會影響邊界外的物件,反之亦然。
在重新構建build
函式及標記RenderObject
完成後,就進入下一階段,開始佈局。
3、layout階段
在該階段,會確定每個元件的大小及位置,相當於Android中的onMeasure
+onLayout
函式所實現的功能。如果是第一次呼叫該函式,該階段就會遍歷所有的元件,來確定其大小及位置;否則該階段就會遍歷佈局邊界內的所有元件,來確定其大小及位置。
當上一階段中的buildOwner.buildScope(renderViewElement)
函式執行完畢後,就會呼叫RendererBinding
的drawFrame
函式,該函式實現非常簡潔。
//繪製幀
void drawFrame() {
//對指定元件及其子元件進行大小測量及位置確定
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
複製程式碼
其中flushLayout
就是進行元件的大小及位置確定,在該函式中會遍歷集合_nodesNeedingLayout
並呼叫集合中每個物件的_layoutWithoutResize
函式。
void flushLayout() {
try {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
//呼叫RenderObject物件的_layoutWithoutResize函式
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {...}
}
複製程式碼
_layoutWithoutResize
函式是私有的,所以不存在重寫的問題。那麼就直接來看該函式。
void _layoutWithoutResize() {
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {...}
_needsLayout = false;
markNeedsPaint();
}
複製程式碼
_layoutWithoutResize
函式很簡單,就直接呼叫了performLayout
函式。
由於performLayout
是一個抽象函式,需要在子類重寫,但都會在該函式中呼叫layout
函式,然後又在layout
函式中呼叫performLayout
函式。以此類推,從而確定更新UI部分的元件大小及位置,總體流程如下。
RenderObject
物件的size也不是隨便確定的,因為在呼叫RenderObject
的layout
函式時,會傳遞一個繼承自Constraints
的物件。該物件是一個佈局約束,由父傳給子,子會根據該物件來決定自己的大小。
3.1、標記RenderObject
當大小及位置確定後,就又會對RenderObject
進行一次標記,這次跟上一階段的標記大同小異,但這次是標記可繪製的RenderObject
物件,然後在後面對這些物件進行重新繪製。標記可繪製的RenderObject
物件是通過markNeedsPaint
函式來實現的,程式碼如下。
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
//標記需要重新繪製的RenderObject物件
//需要繪製當前圖層
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
} else if (parent is RenderObject) {
//沒有自己的圖層,與父類共用同一圖層
final RenderObject parent = this.parent;
//遍歷其父RenderObject物件
parent.markNeedsPaint();
} else {
//當是RenderView時,需要自己建立新的圖層
if (owner != null)
owner.requestVisualUpdate();
}
}
複製程式碼
markNeedsPaint
函式中涉及到了一個“重繪邊界”的概念。在進入和走出重繪邊界時,Flutter會強制切換新的圖層,這樣就可以避免邊界內外的互相影響。當然重繪邊界也可以在任何節點手動設定,但是一般不需要我們來實現,Flutter提供的控制元件預設會在需要設定的地方自動設定。
4、compositingBits階段
在元件的大小及位置確定後,就會進入當前階段。該階段主要是做一件事,就是將RenderObject
樹上新增及刪除的RenderObject
物件標記為“髒”,方便在下一階段對這些RenderObject
物件進行重繪。具體程式碼實現是在flushCompositingBits
函式中,該函式在Layout
階段後立即呼叫。
void flushCompositingBits() {
...
//將RenderObject物件按照深度進行排序
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
//將RenderObject物件及其子物件標記為“髒”
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
...
}
複製程式碼
_nodesNeedingCompositingBitsUpdate
是一個集合,只有RenderObject
物件的_needsCompositing
為true時,才會新增到該集合中。在RenderObject
物件建立時,_needsCompositing
的值會根據isRepaintBoundary
及alwaysNeedsCompositing
來共同判斷。
RenderObject() {
//isRepaintBoundary決定當前RenderObject是否與父RenderObject分開繪製,預設為false,其值在當前物件的生命週期內無法修改。也就是判斷當前物件是否是繪製邊界
//alwaysNeedsCompositing為true表示當前RenderObject會一直重繪,如視訊播放,預設為false
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}
複製程式碼
然後在向樹中新增或者刪除RenderObject
物件時會呼叫adoptChild
及dropChild
函式,而這兩個函式都會呼叫markNeedsCompositingBitsUpdate
函式,也就在markNeedsCompositingBitsUpdate
函式內完成了將當前物件新增到集合中的操作。
//向樹中新增當前節點
@override
void adoptChild(RenderObject child) {
setupParentData(child);
markNeedsLayout();
//將當前物件的_needsCompositingBitsUpdate值標為true
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
//從樹中移除當前節點
@override
void dropChild(RenderObject child) {
child._cleanRelayoutBoundary();
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
//將當前物件的_needsCompositingBitsUpdate值標為true
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
//
void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent;
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
//將當前物件或者其父物件新增到_nodesNeedingCompositingBitsUpdate集合中
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}
複製程式碼
這樣就會在呼叫flushCompositingBits
函式時,就會呼叫_updateCompositingBits
函式來判斷是否將這些物件及子物件標記為“髒”,然後在下一階段進行繪製。
void _updateCompositingBits() {
//表示已經處理過,
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
//訪問其子物件
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
//如果是繪製邊界或者需要一直重繪
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing) {
//將當前物件標記為“髒”,
markNeedsPaint();
}
_needsCompositingBitsUpdate = false;
}
複製程式碼
5、繪製階段
經過前面的佈局及“髒”RenderObject
物件的標記,現在就可以在圖層上進行UI的繪製。通過呼叫flushPaint
函式就可以重繪已經標記的“髒”RenderObject
物件及其子物件。
void flushPaint() {
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
//根據節點深度進行排序
for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
//當前物件是否與layer進行關聯
if (node._layer.attached) {
//在圖層上繪製UI
PaintingContext.repaintCompositedChild(node);
} else {
//跳過UI繪製,但當前節點為“髒”的狀態不會改變
node._skippedPaintingOnLayer();
}
}
}
} finally {}
}
複製程式碼
flushPaint
函式中,每次遍歷“髒”RenderObject
物件時,都會進行一次排序,避免重複繪製。然後在判斷當前物件是否與Layer
進行關聯,如果沒有關聯,則無法進行繪製,但不會清除“髒”標記。下面來看repaintCompositedChild
函式的實現。
static void repaintCompositedChild(RenderObject child, { bool
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
//拿到Layer物件
OffsetLayer childLayer = child._layer;
if (childLayer == null) {
//建立新的Layer物件
child._layer = childLayer = OffsetLayer();
} else {
//移除Layer物件的後繼節點
childLayer.removeAllChildren();
}
//建立context物件
childContext ??= PaintingContext(child._layer, child.paintBounds);
//呼叫paint函式開始繪製
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}
複製程式碼
在該函式中主要是對Layer
物件的處理,然後呼叫_paintWithContext
函式,在_paintWithContext
函式中就會呼叫paint
這個函式,從而實現UI的繪製。至此,就完成了UI的繪製,下面再來看一個被忽略的物件——Layer
。
5.1、Layer
Layer
是“圖層”意思,在Flutter
中是最容易被忽略但又無比重要的一個類。它非常貼近底層,可以很容易的看到呼叫Native方法。
Layer
跟其他三棵樹一樣,也是一棵樹,有“髒”狀態的標記、更新等操作。不同的是,Layer
是一個雙連結串列結構,在每個Layer
物件中都會指向其前置節點與後置節點(葉子Layer
的後置節點為null)。
abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
//返回父節點
@override
ContainerLayer get parent => super.parent;
//當前節點狀態,為true表示當前節點是“髒”資料。需要重繪
bool _needsAddToScene = true;
//將當前節點標記為“髒”
@protected
@visibleForTesting
void markNeedsAddToScene() {
// Already marked. Short-circuit.
if (_needsAddToScene) {
return;
}
_needsAddToScene = true;
}
@protected
bool get alwaysNeedsAddToScene => false;
//這個是一個非常重要的東西,主要用於節點資料的快取。儲存當前節點的渲染資料,如果當前節點不需要更新,就直接拿儲存的資料使用。
@protected
ui.EngineLayer get engineLayer => _engineLayer;
//更改當前節點的資料
@protected
set engineLayer(ui.EngineLayer value) {
_engineLayer = value;
if (parent != null && !parent.alwaysNeedsAddToScene) {
//將父節點標記需要更新的狀態
parent.markNeedsAddToScene();
}
}
}
ui.EngineLayer _engineLayer;
//更新當前節點狀態,如果_needsAddToScene為true,則將當前節點標記為“髒”
@protected
@visibleForTesting
void updateSubtreeNeedsAddToScene() {
_needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
}
//指向後置節點
Layer get nextSibling => _nextSibling;
Layer _nextSibling;
//指向前置節點
Layer get previousSibling => _previousSibling;
Layer _previousSibling;
//將子節點從Layer樹中移除
@override
void dropChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene();
}
super.dropChild(child);
}
//將當前節點新增到Layer樹中
@override
void adoptChild(AbstractNode child) {
if (!alwaysNeedsAddToScene) {
markNeedsAddToScene();
}
super.adoptChild(child);
}
//將當前節點從Layer樹中移除
@mustCallSuper
void remove() {
parent?._removeChild(this);
}
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
//使用當前節點的快取的資料
if (!_needsAddToScene && _engineLayer != null) {
builder.addRetained(_engineLayer);
return;
}
addToScene(builder);
//將當前節點標記為“乾淨”的
_needsAddToScene = false;
}
}
複製程式碼
5.2、Layer節點的新增
previousSibling
與nextSibling
分別是Layer
的前置節點與後置節點,當向Layer
樹中新增Layer
節點時,也會將當前Layer
設定為父節點的後置節點,父節點設定為當前節點的前置節點。這樣,就形成了一顆樹。
class ContainerLayer extends Layer {
...
//將當前節點及其連結串列上的所有子節點都加入到Layer樹中
@override
void attach(Object owner) {
super.attach(owner);
Layer child = firstChild;
while (child != null) {
child.attach(owner);
child = child.nextSibling;
}
}
//將當前節點及其連結串列上的所有子節點都從Layer樹中移除
@override
void detach() {
super.detach();
Layer child = firstChild;
while (child != null) {
child.detach();
child = child.nextSibling;
}
}
//將child新增到連結串列中
void append(Layer child) {
adoptChild(child);
child._previousSibling = lastChild;
if (lastChild != null)
lastChild._nextSibling = child;
_lastChild = child;
_firstChild ??= child;
}
...
}
複製程式碼
在上述的append
函式中就將子節點新增到Layer
樹並加入到雙連結串列中。在adoptChild
函式中最終會呼叫attach
函式,從而完成Layer
樹的新增。
5.3、Layer的狀態更新
_needsAddToScene
是對Layer
狀態的標記,如果為true,則表示當前Layer
需要重寫進行繪製,否則表示當前Layer
是“乾淨”的,不需要重新繪製,只需要拿Layer
上次的資料與其他Layer
資料一起交給GPU處理即可。從而達到節省資源的目的。
class ContainerLayer extends Layer {
...
//更新Layer節點的狀態。
@override
void updateSubtreeNeedsAddToScene() {
super.updateSubtreeNeedsAddToScene();
Layer child = firstChild;
while (child != null) {
child.updateSubtreeNeedsAddToScene();
_needsAddToScene = _needsAddToScene || child._needsAddToScene;
child = child.nextSibling;
}
}
...
}
複製程式碼
updateSubtreeNeedsAddToScene
函式就是更新Layer
的狀態,可以發現,在更新當前Layer
的狀態時,也會更新其所有子Layer
的狀態。
關於Layer
的更多內容可以去閱讀Flutter Framework 原始碼解析( 2 )—— 圖層詳解這篇文章。
6、其他階段
6.1、compositing階段
該階段主要是將更新後的資料傳遞給GPU。這時候呼叫的是compositeFrame
函式,該函式很簡單,就是呼叫了一個Native函式。
void compositeFrame() {
Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
try {
final ui.SceneBuilder builder = ui.SceneBuilder();
final ui.Scene scene = layer.buildScene(builder);
if (automaticSystemUiAdjustment)
_updateSystemChrome();
//更新後資料交給GPU處理
_window.render(scene);
scene.dispose();
} finally {
Timeline.finishSync();
}
}
複製程式碼
6.2、semantics階段
在向GPU傳送資料後,Flutter
還會呼叫flushSemantics
函式。該函式與系統的輔助功能相關,一般情況下是不做任何處理。
6.3、finalization階段
在該階段,主要是將Element
物件從樹中移除及處理新增在_postFrameCallbacks
集合中的回撥函式。由於該回撥函式是在繪製結束時呼叫,所以在該回撥函式中,context已經建立成功。
7、總結
可以發現,Flutter
的UI繪製還是蠻複雜的,涉及到的東西也比較多,如動畫的處理、輔助功能、非同步任務等。但整體上還是通過Widget
、Element
、RenderObject
這三棵樹來操作layer
樹實現的UI的繪製。
熟悉了這四棵樹,也就會對Flutter
的UI繪製有一個清晰的認識。