Flutter框架分析(七)-relayoutBoundary

zuoluohust發表於2021-07-05

1. 前言

Flutter框架分析(四)-RenderObject一文中,我們簡單介紹了RenderObject中一個重要成員變數:RelayoutBoundary。下面我們簡單回顧下RelayoutBoundary的主要作用。
當一個元件的大小被改變時,其parent的大小可能也會被影響,因此需要通知其父節點。如果這樣迭代上去,需要通知整棵RenderObject Tree重新佈局,必然會影響佈局效率。因此,Flutter通過RelayoutBoundaryRenderObject 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會呼叫RenderObjectperformLayout函式,實現該RenderObject的重新佈局。
以上流程的示意圖如下:

image.gif

由上述邏輯可知,當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();
    }
  }
}
複製程式碼

那本函式的呼叫時機是什麼呢?主要有以下幾種:

  • 子節點變動,例如attachdetach
  • 自身佈局變化,例如Size變化。

Flutter初始化進行第一次佈局,每個RenderObject均需要佈局,因此無優化空間,本文主要關注對重新佈局的優化,即對markNeedsLayout的呼叫。接下來我們分析markNeedsLayout的呼叫鏈。其流程圖如下:

image.gif

可見,在一個RenderObject呼叫markNeedsLayout函式後,如果其本身不是_relayoutBoundary,則會通過markParentNeedsLayout函式呼叫到parentmarkNeedsLayout函式,從而形成遞迴呼叫,直到找到最近的一個是_relayoutBoundary的上級節點,才會停止遞迴,並將該節點加入_nodesNeedingLayout。因此,通過_relayoutBoundary,FlutterRenderObject Tree劃分成了數段,當位於某段的RenderObject需要重新佈局時,只會更新該段及其下的RenderObject,而不是整個RenderObject Tree。示意圖如下:

image.gif

那麼,什麼時候會將RenderObject設定為RelayoutBoundary呢?滿足以下4種情況之一時,會將自身設定為RelayoutBoundary

  • parentUsesSize = false:父節點的佈局不依賴當前節點的大小。
  • sizedByParent = true:當前節點大小由父節點決定。
  • constraints.isTight:大小為確定的值,即寬高的最大值等於最小值。
  • parent is not RenderObject:如果父節點不是RenderObject,子節點layout變化不需要通知父節點更新。

以上條件很好理解,例如parentUsesSize = false,此時父節點的佈局不依賴當前節點的大小,那當前節點佈局更新自然不需要通知父節點,因此可以將其作為一個RelayoutBoundary

3. 小結

本文首先介紹了RelayoutBoundary的作用,然後結合原始碼分析了RelayoutBoundary的作用原理,其重點如下:

  1. RelayoutBoundary通過減少待佈局節點列表數量(加入_nodesNeedingLayout)的方式優化節點更新時的佈局效率。
  2. RelayoutBoundary的設定條件包括以下4種:
  • parentUsesSize = false
  • sizedByParent = true
  • constraints.isTight
  • parent is not RenderObjec

4. 參考文件

如何在Flutter上實現高效能的動態模板渲染

5. 相關文章

Flutter框架分析(一)--架構總覽
Flutter框架分析(二)-- Widget
Flutter框架分析(三)-- Element
Flutter框架分析(四)-RenderObject
Flutter框架分析(五)-Widget,Element,RenderObject樹
Flutter框架分析(六)-Constraint
Flutter框架分析- Parent Data
Flutter框架分析 -InheritedWidget

相關文章