我們前面介紹了StatelessWidget和StatefulWidget,它們只是對其他Widget進行組合,不具備自定義繪製的能力。在需要繪製內容的場景下,我們要使用RenderObjectWidget,因為RenderObjectWidget建立的RenderObject負責佈局和繪製的功能。
本文將以RenderObject為起點,梳理下Flutter的佈局和繪製流程的邏輯。
RenderObject
RenderObject是渲染樹Render Tree的一個節點,主要負責佈局和繪製。
Flutter設計了重要的三棵樹Widget Tree - Element Tree - RenderObject Tree。示例如下: 圖片引用來源
RenderObjectWidget例項化的RenderObjectElement會建立 RenderObject, 所有的RenderObject會組成一顆RenderObject Tree。
abstract class RenderObject extends AbstractNode implements HitTestTarget {}
複製程式碼
RenderObject繼承自AbstractNode, AbstractNode是對樹的節點的抽象:
class AbstractNode {
// 1
int get depth => _depth;
int _depth = 0;
void redepthChild(AbstractNode child) {}
// 2
Object? get owner => _owner;
Object? _owner;
void attach(covariant Object owner) {
_owner = owner;
}
void detach() {
_owner = null;
}
// 3
AbstractNode? get parent => _parent;
AbstractNode? _parent;
void adoptChild(covariant AbstractNode child) {
child._parent = this;
if (attached)
child.attach(_owner!);
redepthChild(child);
}
void dropChild(covariant AbstractNode child) {
child._parent = null;
if (attached)
child.detach();
}
}
複製程式碼
- AbstractNode提供了三個屬性和幾個重要的方法:
- 節點深度
depth
屬性和計算節點深度redepthChild()
方法; owner
和對應的關聯attach()
和取消關聯detach()
方法;parent
父節點;- 掛載子節點
adoptChild()
和解除安裝子節點dropChild()
方法。
abstract class RenderObject extends AbstractNode implements HitTestTarget {
// 1
ParentData? parentData;
// 2
Constraints _constraints;
// 3
RenderObject? _relayoutBoundary;
// 眾多方法...
}
複製程式碼
- RenderObject自身也有幾個重要的屬性:
parentData
父節點的插槽,父節點的一些資訊可以放置在這裡面供子節點使用;_constraints
為父節點提供的約束;_relayoutBoundary
是需要重新佈局的邊界。
- RenderObject的方法和Android的View非常類似:
功能 | RenderObject | View |
---|---|---|
佈局 | performLayout() | measure()/measure() |
繪製 | paint() | draw() |
請求佈局 | markNeedsLayout() | requestLayout() |
請求繪製 | markNeedsPaint() | invalidate() |
父節點/View | parent | getParent() |
新增子節點/View | adoptChild() | addView() |
移除子節點/View | dropChild() | removeView() |
關聯owner/Window | attach() | onAttachedToWindow() |
取消關聯owner/Window | detach() | onDetachedFromWindow() |
事件 | hitTest() | onTouch() |
螢幕旋轉 | rotate() | onConfigurationChanged() |
引數 | parentData | mLayoutParams |
- RenderObject還有一個特點 --- 它定義了佈局/繪製協議,但並沒定義具體佈局/繪製模型。
定義了佈局/繪製協議就是指繼承RenderObject的子類必須要實現一些方法,譬如performLayout
、paint
等;沒定義具體佈局/繪製模型是指沒有限定使用什麼座標系,子節點可以有0個、1個還是多個等。
- Flutter提供了RenderBox和RenderSlive兩個子類,他們分別對應簡單的2D笛卡爾座標模型和滾動模型。
RenderObject子類 | Constraints | ParentData |
---|---|---|
RenderBox | BoxConstraints | BoxParentData |
RenderSlive | SliverConstraints | SliverLogicalParentData |
一般情況下我們不需要直接使用RenderObject,使用RenderBox和RenderSlive這兩個子類就能滿足需求。
SchedulerBinding.handleDrawFrame()
我們介紹這個方法是為了介紹每次重新整理的工作流程,這樣有助於我們更好的理解RenderObject的相關內容。
Flutter啟動流程分析那篇文章中,我們提到過window.scheduleFrame()
向Native platform發起一個重新整理檢視的請求後,Flutter Engine會在適當的時候呼叫SchedulerBinding的_handleDrawFrame
方法。
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
}
}
複製程式碼
handleDrawFrame
中執行了回撥函式陣列persistentCallbacks中所有的回撥函式。其中就包括RendererBinding中的_handlePersistentFrameCallback
方法:
<!-- RendererBinding -->
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
複製程式碼
這裡的drawFrame
方法是呼叫的父類WidgetsBinding的方法:
<!-- WidgetsBinding -->
void drawFrame() {
try {
// 1
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!);
// 2
super.drawFrame();
// 3
buildOwner!.finalizeTree();
} finally {
}
}
複製程式碼
此方法代表的含義:
buildOwner!.buildScope(renderViewElement!)
執行的是Widget的build任務,這其中就包括StatelessWidget和StatefulWidget和RenderObjectWidget;- 呼叫WidgetsBinding的
drawFrame
方法; - 解除安裝非啟用狀態的Element。
WidgetsBinding的drawFrame
方法中則執行了佈局和繪製等操作。
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;
}
}
複製程式碼
上面的buid/layout/paint等都和RenderObject息息相關,我們將在接下來的章節中詳細介紹。
Build
接下來我們就來看看buildScope
方法觸發的RenderObjectWidget的構建過程。
Element inflateWidget(Widget newWidget, dynamic newSlot) {
// 1
final Element newChild = newWidget.createElement();
// 2
newChild.mount(this, newSlot);
return newChild;
}
複製程式碼
inflateWidget
方法主要作用:
- 先通過
createElement
方法根據Widget建立對應的Element; - 然後新建的Element呼叫
mount
方法,將自己掛載到Element Tree上,位置是父Element的newSlot
這個插槽。
createElement
abstract class RenderObjectWidget extends Widget {
@factory
RenderObjectElement createElement();
}
複製程式碼
RenderObjectWidget的createElement
方法是工廠方法,真正的實現方法在子類裡面。
RenderObjectWidget的子類對應的Element總結:
分類 | Widget | Element |
---|---|---|
根節點 | RenderObjectToWidgetAdapter | RootRenderObjectElement |
具有多個子節點 | MultiChildRenderObjectWidget | MultiChildRenderObjectElement |
具有一個子節點點 | SingleChildRenderObjectWidget | SingleChildRenderObjectElement |
葉子節點 | LeafRenderObjectWidget | LeafRenderObjectElement |
程式碼如下:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
@override
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
@override
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
}
複製程式碼
mount
我們來看RenderObjectElement的mount
方法實現:
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
複製程式碼
super.mount
的作用主要是記錄下parent
,slot
和depth
等值;widget.createRenderObject
建立了一個renderObject;attachRenderObject
就是將這個parentData掛載到RenderObject Tree上,並且更新RenderObject的parentData。
void attachRenderObject(dynamic newSlot) {
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
複製程式碼
insertRenderObjectChild
renderObject
通過insertRenderObjectChild
方法掛載到RenderObject Tree上,那具體的實現是如何實現的呢?
能實現掛載RenderObject的只能是SingleChildRenderObjectElement和MultiChildRenderObjectElement。我們分別來看看:
SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
renderObject.child = child;
}
複製程式碼
對child
進行賦值:
set child(ChildType? value) {
if (_child != null)
dropChild(_child!);
_child = value;
if (_child != null)
adoptChild(_child!);
}
複製程式碼
如果已經有_child
先將其從解除安裝,然後將新的Child掛載上。
void dropChild(RenderObject child) {
child._cleanRelayoutBoundary();
child.parentData!.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
複製程式碼
void adoptChild(RenderObject child) {
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
複製程式碼
這兩個方法主要是對_child
和parentData
重新賦值,然後通過markNeedsLayout
,markNeedsCompositingBitsUpdate
和markNeedsSemanticsUpdate
標記需要重新佈局,需要合成和語義的更新。
MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
renderObject.insert(child, after: slot.value?.renderObject);
}
複製程式碼
void insert(ChildType child, { ChildType? after }) {
adoptChild(child);
_insertIntoChildList(child, after: after);
}
複製程式碼
MultiChildRenderObjectElement中的實現方式類似,只是這次不是簡單的賦值,而是將child
新增到Render Tree中去,然後進行各種標記。
_insertIntoChildList
方法新增的邏輯如下:
- 依附的兄弟節點為空,插入在第一個子節點;
- 依附的兄弟節點沒有相關聯的下一個兄弟節點,插入在兄弟節點隊尾;
- 依附的兄弟節點有相關聯的下一個兄弟節點,插入在兄弟節點中間。
inflateWidget遞迴
由於SingleChildRenderObjectWidget和MultiChildRenderObjectWidget含有子節點,所以需要對子Widget進行構建。
<!-- SingleChildRenderObjectWidget -->
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
final Element newChild;
// ... 省略Widget更新的邏輯
newChild = inflateWidget(newWidget, newSlot);
return newChild;
}
複製程式碼
<!-- MultiChildRenderObjectElement -->
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
Element? previousChild;
for (int i = 0; i < children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
children[i] = newChild;
previousChild = newChild;
}
_children = children;
}
複製程式碼
這樣,接下來的操作就進入了遞迴流程了,和上面介紹的流程內容一模一樣了。
流程示意圖:
markNeedsLayout
我們上面看到了,adoptChild
和adoptChild
的方法中都呼叫了markNeedsLayout
的相關內容:
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
bool _needsLayout = true;
RenderObject? _relayoutBoundary;
void markNeedsLayout() {
// 1
if (_needsLayout) {
return;
}
// 2
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
// 3
_needsLayout = true;
if (owner != null) {
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}
}
}
複製程式碼
RenderObject有_needsLayout
屬性,標記是否需要重新佈局,還有一個_relayoutBoundary
佈局邊界屬性,表示開始重新佈局的節點,這樣就不需要每次整個渲染樹的節點都進行重新佈局。
markNeedsLayout
代表的含義:
- 如果已經標記
_needsLayout
,直接返回; - 如果
_relayoutBoundary
佈局邊界不是自身,讓父節點遞迴呼叫markNeedsLayout
方法; - 如果
_relayoutBoundary
佈局邊界是自身,標記_needsLayout
, 並將自身加入到PipelineOwner的_nodesNeedingLayout
列表中,等待PipelineOwner進行重新佈局; - 請求PipelineOwner進行更新。
您可能會有疑問_relayoutBoundary
是在什麼時候賦值的?有兩個地方賦值:
- 第一次佈局的時候,
_relayoutBoundary
會被標記為RenderView,即自身,然後從根節點進行佈局;
void scheduleInitialLayout() {
_relayoutBoundary = this;
owner!._nodesNeedingLayout.add(this);
}
複製程式碼
layout()
方法中RenderObject也會重新標記_relayoutBoundary
,一般情況下也是自身。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// ...
_relayoutBoundary = relayoutBoundary;
}
複製程式碼
markNeedsCompositingBitsUpdate
bool _needsCompositingBitsUpdate = false;
void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
if (owner != null)
owner!._nodesNeedingCompositingBitsUpdate.add(this);
}
複製程式碼
RenderObject有_needsCompositingBitsUpdate
屬性,標記是否需要合成。
markNeedsCompositingBitsUpdate
的邏輯如下:
- 如果已經標記
_needsCompositingBitsUpdate
,直接返回; - 如果未標記
_needsCompositingBitsUpdate
先標記,然後標記父節點或者向父類遞迴呼叫markNeedsCompositingBitsUpdate
直到標記成功為止; - 將自身加入到PipelineOwner的
_nodesNeedingCompositingBitsUpdate
列表中。
結果就是將isRepaintBoundary
這個節點下的所有節點都標記為_needsCompositingBitsUpdate
,然後加入到PipelineOwner的_nodesNeedingCompositingBitsUpdate
列表中。
flushLayout
前面所有的邏輯只能算是buildScope
方法觸發的Build階段。接下來我們就進入了Layout階段了。
<!-- PipelineOwner -->
void flushLayout() {
try {
while (_nodesNeedingLayout.isNotEmpty) {
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
node._layoutWithoutResize();
}
}
} finally {
}
}
複製程式碼
PipelineOwner的flushLayout
其實很簡單,讓_nodesNeedingLayout
中的所有RenderObject按照廣度優先遍歷呼叫_layoutWithoutResize
方法。
<!-- RenderObject -->
void _layoutWithoutResize() {
try {
performLayout();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();
}
複製程式碼
我們來看看_ScaffoldLayout
中的實現:
void performLayout() {
size = _getSize(constraints);
delegate._callPerformLayout(size, firstChild);
}
void _callPerformLayout(Size size, RenderBox? firstChild) {
performLayout(size);
}
void performLayout(Size size) {
layoutChild(_ScaffoldSlot.body, bodyConstraints);
positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
}
Size layoutChild(Object childId, BoxConstraints constraints) {
child!.layout(constraints, parentUsesSize: true);
return child.size;
}
void positionChild(Object childId, Offset offset) {
final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
childParentData.offset = offset;
}
複製程式碼
根據一系列呼叫,生成一個BoxConstraints傳遞給每個子節點,子節點呼叫layout()
進行測量和佈局。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// 1
RenderObject? relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
// 2
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
// 3
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
// 4
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
}
}
try {
// MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
// 5
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();
}
複製程式碼
- 首先根據
!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject
的條件進行_relayoutBoundary
的計算,一般情況下會指向自身;
parentUsesSize表示是否父節點的大小依賴子節點,sizedByParent表示大小由父類決定,constraints.isTight表示大小是固定的。
- 根據
!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary
確實定是否需要重新佈局,不需要直接返回; - 記錄下
_constraints
; - 如果依賴父節點的大小,則根據
_constraints
計算出size
尺寸, ; performLayout
根據根據_constraints
計算出size
尺寸,然後呼叫子類的layout
方法。
總結:
performLayout
的邏輯就是通過layout
方法將Constraints逐步往下傳遞,得到Size逐步向上傳遞,然後父節點通過給parentData賦值確定對子節點的位置擺放。
flushCompositingBits
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();
}
複製程式碼
遍歷_nodesNeedingCompositingBitsUpdate
中的每個RenderObject,然後呼叫_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;
}
複製程式碼
這個方法就是找到isRepaintBoundary為true的節點及其父節點,將它們的_needsCompositing為true
設定為true;
isRepaintBoundary
上面提到的isRepaintBoundary是RenderObject的一個屬性,預設是false。表示的是否需要獨立渲染。
<!-- RenderObject -->
bool get isRepaintBoundary => false;
複製程式碼
如果需要獨立渲染則需要覆蓋這個值為true,例如RenderView的值就為true。
<!-- RenderView -->
@override
bool get isRepaintBoundary => true;
複製程式碼
flushPaint
按照邏輯flushPaint
之前應該先會呼叫markNeedsPaint
,我們回過頭來看看發現確實如此,有很多地方都頻繁的呼叫的markNeedsPaint
,譬如_layoutWithoutResize
,layout
,_updateCompositingBits
等方法中都有出現,只是前面我們特意忽略了這個邏輯。
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
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();
} else {
if (owner != null)
owner!.requestVisualUpdate();
}
}
複製程式碼
- 如果isRepaintBoundary為true, 則加入到
_nodesNeedingPaint
陣列中,然後請求介面更新; - 如果isRepaintBoundary為false,則向父節點遍歷;
- 如果到了根節點,就直接請求介面更新;
我們接下來看看flushPaint的程式碼:
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)) {
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {
}
}
複製程式碼
從下往上遍歷_nodesNeedingPaint
陣列,然後從上往下進行繪製。
接下來我們看看是如何繪製的:
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer!, child.paintBounds);
// 重點
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}
複製程式碼
PaintingContext的類方法repaintCompositedChild
接收了RenderObject物件,最後結果是這個RenderObject物件呼叫_paintWithContext
方法,引數是PaintingContext物件和偏移量Offset。
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
_needsPaint = false;
try {
paint(context, offset);
} catch (e, stack) {
}
}
複製程式碼
<!-- PaintingContext -->
Canvas? _canvas;
複製程式碼
_paintWithContext
方法呼叫的是RenderObject子類物件的paint(PaintingContext context, Offset offset)
進行繪製,繪製在PaintingContext的Canvas上。
總結
本文主要分析了RendObject、Build、Layout、Paint等相關內容,後續繼續分析其他相關內容。