1. 前言
在Flutter框架分析(四)-RenderObject一文中,我們簡單介紹了RenderObject中一個重要成員變數:RelayoutBoundary。下面我們簡單回顧下RelayoutBoundary的主要作用。
當一個元件的大小被改變時,其parent的大小可能也會被影響,因此需要通知其父節點。如果這樣迭代上去,需要通知整棵RenderObject Tree重新佈局,必然會影響佈局效率。因此,Flutter通過RelayoutBoundary將RenderObject Tree分段,如果遇到了RelayoutBoundary,則不去通知其父節點重新佈局,因為其大小不會影響父節點的大小。這樣就只需要對RenderObject Tree中的一段重新佈局,提高了佈局效率。
那麼,RelayoutBoundary是怎麼實現將RenderObject Tree分段的呢?本文將通過原始碼來剖析RelayoutBoundary的工作原理。
2. 原始碼解析
在Flutter中,如果Widget有更新,需要重新佈局,Framework會將需要佈局的RenderObject加入PipelineOwner的_nodesNeedingLayout中,然後當下一個VSync訊號來臨時,Framework會遍歷_nodesNeedingLayout,對其中的每一個RenderObject重新進行佈局,遍歷_nodesNeedingLayout的函式原始碼如下:
void flushLayout() {
try {
// TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
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 {
}
}
複製程式碼
其中,_layoutWithoutResize會呼叫RenderObject的performLayout函式,實現該RenderObject的重新佈局。
以上流程的示意圖如下:
由上述邏輯可知,當Widget有更新,需要重新佈局時,加入_nodesNeedingLayout的元素的多少直接關係到需要重新佈局元素的多少,如果能將盡可能少的RenderObject加入_layoutWithoutResize,即可儘可能提高佈局效率。這就是設計RelayoutBoundary的核心思路。
下面我們來看什麼時候會將RenderObject新增進_nodesNeedingLayout。從原始碼可以看到,新增進_nodesNeedingLayout有兩個地方:
- 初始化RenderView的時候,原始碼如下:
void scheduleInitialLayout() {
_relayoutBoundary = this;
owner._nodesNeedingLayout.add(this);
}
複製程式碼
本函式只在Flutter初始化的時候呼叫一次。
- RenderObject標記自己需要重新佈局的時候,原始碼如下:
void markNeedsLayout() {
if (_needsLayout) {
return;
}
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
複製程式碼
那本函式的呼叫時機是什麼呢?主要有以下幾種:
- 子節點變動,例如attach或detach。
- 自身佈局變化,例如Size變化。
當Flutter初始化進行第一次佈局,每個RenderObject均需要佈局,因此無優化空間,本文主要關注對重新佈局的優化,即對markNeedsLayout的呼叫。接下來我們分析markNeedsLayout的呼叫鏈。其流程圖如下:
可見,在一個RenderObject呼叫markNeedsLayout函式後,如果其本身不是_relayoutBoundary,則會通過markParentNeedsLayout函式呼叫到parent的markNeedsLayout函式,從而形成遞迴呼叫,直到找到最近的一個是_relayoutBoundary的上級節點,才會停止遞迴,並將該節點加入_nodesNeedingLayout。因此,通過_relayoutBoundary,Flutter將RenderObject Tree劃分成了數段,當位於某段的RenderObject需要重新佈局時,只會更新該段及其下的RenderObject,而不是整個RenderObject Tree。示意圖如下:
那麼,什麼時候會將RenderObject設定為RelayoutBoundary呢?滿足以下4種情況之一時,會將自身設定為RelayoutBoundary。
- parentUsesSize = false:父節點的佈局不依賴當前節點的大小。
- sizedByParent = true:當前節點大小由父節點決定。
- constraints.isTight:大小為確定的值,即寬高的最大值等於最小值。
- parent is not RenderObject:如果父節點不是RenderObject,子節點layout變化不需要通知父節點更新。
以上條件很好理解,例如parentUsesSize = false,此時父節點的佈局不依賴當前節點的大小,那當前節點佈局更新自然不需要通知父節點,因此可以將其作為一個RelayoutBoundary。
3. 小結
本文首先介紹了RelayoutBoundary的作用,然後結合原始碼分析了RelayoutBoundary的作用原理,其重點如下:
- RelayoutBoundary通過減少待佈局節點列表數量(加入_nodesNeedingLayout)的方式優化節點更新時的佈局效率。
- RelayoutBoundary的設定條件包括以下4種:
- parentUsesSize = false
- sizedByParent = true
- constraints.isTight
- parent is not RenderObjec
4. 參考文件
5. 相關文章
Flutter框架分析(一)--架構總覽
Flutter框架分析(二)-- Widget
Flutter框架分析(三)-- Element
Flutter框架分析(四)-RenderObject
Flutter框架分析(五)-Widget,Element,RenderObject樹
Flutter框架分析(六)-Constraint
Flutter框架分析- Parent Data
Flutter框架分析 -InheritedWidget