Framework是什麼?Flutter Framework 之 RenderObject深入分析

南方吳彥祖_藍斯發表於2021-09-27

本文講對Flutter Framework 之 RenderObject 生命週期中幾個關鍵節點:建立、佈局、繪製等進行了簡要分析。

Overview


可以說,RenderObject 在整個 Flutter Framework 中屬於核心物件,其職責概括起來主要有三點: 『 Layout 』、『 Paint 』、『 Hit Testing 』。 然而,RenderObject 是抽象類,具體工作由子類去完成。

Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖, RenderSliverRenderBoxRenderView以及 RenderAbstractViewport是在 Flutter Framework 中定義的4個子類:

  • RenderSliver,『 Sliver-Widget 』對應的 Base RenderObject;
  • RenderBox,除『 Sliver-Widget 』外幾乎所有常見 Render-Widget 對應的 Base RenderObject;
  • RenderView,是一種特殊的 Render Object,是『 RenderObect Tree 』的根節點;
  • RenderAbstractViewport,主要用於『 Scroll-Widget 』。
Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖,概括了 RenderObject中與 Layout、Paint 相關的主要屬性與方法(也是本文將要討論的主要內容)。 其中,用虛線框起來的是 RenderObject子類需要重寫的方法。 正如上面所說, RenderObject是抽象類,它即沒有明確使用哪種座標系 (Cartesian coordinates or Polar coordinates),也沒有指定使用哪種排版演算法 (width-in-height-out or constraint-in-size-out)。

RenderBox採用笛卡爾座標系、排版演算法是 constraint-in-size-out,即根據父節點傳遞的排版約束來計算 Size。

下面我們從 RenderObject生命週期中幾個關鍵節點展開介紹:建立、佈局、渲染。

本文所示程式碼基於 Flutter 1.12.13,同時對程式碼做了精簡處理,以便突出所要討論的重點。

建立


當 RenderObjectElement 被掛載(mount) 到『 Element Tree 』上時,會建立對應的 Render Object 。 同時,會將其 attach 到『 RenderObject Tree 』上,也就是在『 Element Tree 』建立過程中『 RenderObject Tree 』也被逐步建立出來:

// RenderObjectElementvoid mount(Element parent, dynamic newSlot) {  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
複製程式碼

佈局


當 RenderObject 需要(重新)佈局時呼叫 markNeedsLayout方法,從而被 PipelineOwner收集,並在下一幀重新整理時觸發 Layout 操作。

markNeedsLayout呼叫場景

  • Render Object 被新增到『 RenderObject Tree 』;
  • 子節點 adopt、drop、move;
  • 由子節點的 markNeedsLayout方法傳遞呼叫;
  • Render Object 自身與佈局相關的屬性發生變化,如 RenderFlex的排版方向有變化時:
set direction(Axis value) {  assert(value != null);  if (_direction != value) {
    _direction = value;
    markNeedsLayout();
  }
}
複製程式碼

Relayout Boundary

若某個 Render Object 的佈局變化不會影響到其父節點的佈局,則該 Render Object 就是『 Relayout Boundary 』。 Relayout Boundary 是一項重要的最佳化措施,可以避免不必要的 re-layout。

當某個 Render Object 是 Relayout Boundary 時,會切斷 layout dirty 向父節點傳播,即下一幀重新整理時父節點無需 re-layout。

Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖:

  • RD節點出現 layout dirty,由於其自身、其父節點 RARRoot都不是 Relayout Boundary,最終 layout dirty 傳播到根節點 RenderView,導致整顆『 RenderObject Tree 』重新佈局;
  • RF節點出現 layout dirty,由於其父節點 RB為 Relayout Boundary,layout dirty 傳播到 RB即結束,最終需要重新佈局的只有 RBRF兩個節點;
  • RG節點出現 layout dirty,由於其自身就是 Relayout Boundary,最終需要重新佈局的只有 RG自己。

那麼,具體來說要成為 Relayout Boundary 需要滿足什麼條件呢?

void layout(Constraints constraints, { bool parentUsesSize = false }) {
  RenderObject relayoutBoundary;  if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
    relayoutBoundary = this;
  } 
  else {
    relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
  }  if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {    return;
  }  if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
    visitChildren(_cleanChildRelayoutBoundary);
  }
  _relayoutBoundary = relayoutBoundary;
}
複製程式碼

以上是 RenderObject#layout中與 Relayout Boundary 有關的程式碼, 可知滿足以下 4 個條件之一即可成為 Relayout Boundary:

  • parentUsesSizefalse,即父節點在 layout 時不會使用當前節點的 size 資訊(也就是當前節點的排版資訊對父節點無影響);
  • sizedByParenttrue,即當前節點的 size 完全由父節點的 constraints 決定,即若在兩次 layout 中傳遞下來的 constraints 相同,則兩次 layout 後當前節點的 size 也相同;
  • 傳給當前節點的 constraints 是緊湊型 (Tight),其效果與 sizedByParenttrue是一樣的,即當前節點的 layout 不會改變其 size,size 由 constraints 唯一確定;
  • 父節點不是 RenderObject 型別(主要針對根節點,其父節點為nil)。

每個 Render Object 都有一個 relayoutBoundary屬性,其值要麼等於自己,要麼等於父節點的 relayoutBoundary

markNeedsLayout

  void markNeedsLayout() {    if (_needsLayout) {      return;
    }    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } 
    else {
      _needsLayout = true;      if (owner != null) {
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }
複製程式碼

從上述程式碼可以看到:

  • 若當前 Render Object 不是 Relayout Boundary,則 layout 請求向上傳播給父節點(即 layout 範圍擴大到父節點,這是一個遞迴過程,直到遇到 Relayout Boundary);
  • 若當前 Render Object 是 Relayout Boundary,則 layout 請求到該節點為此,不會傳播到其父節點。

透過 PipelineOwner 收集所有 layout dirty 節點,並在下一幀重新整理時批次處理,而不是實時更新 dirty layout,從而避免不必要的重複 re-layout。

layout

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } 
    else {
      relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
    }    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {      return;
    }
    _constraints = constraints;    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren(_cleanChildRelayoutBoundary);
    }
    _relayoutBoundary = relayoutBoundary;    if (sizedByParent) {
      performResize();
    }
    performLayout();
    markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }
複製程式碼

layout方法是觸發 Render Object 更新佈局資訊的主要入口點。 一般情況下,由父節點呼叫子節點的 layout方法來更新其整體佈局。  RenderObject的子類不應重寫該方法,可按需重寫 performResize或/和 performLayout方法。

當前 Render Object 的佈局受到 layout方法引數 constraints的約束。

Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖,『 Render Object Tree 』的 layout 是一次深度優先遍歷的過程。 優先 layout 子節點,之後 layout 父節點。 父節點向子節點傳遞 layout constraints,子節點在 layout 時需遵守這些約束。 作為子節點 layout 的結果,父節點在 layout 時可以使用子節點的 size。

在上述 layout程式碼第 19~21行,若 sizedByParenttrue,則呼叫 performResize來計算該 Render Object 的 size。

sizedByParenttrue的 Render Object 需重寫 performResize方法,在該方法中僅根據 constraints來計算 size。 如 RenderBox中定義的 performResize的預設行為:取 constraints約束下的最小 size:

  @override
  void performResize() {    // default behavior for subclasses that have sizedByParent = true
    size = constraints.smallest;    assert(size.isFinite);
  }
複製程式碼

若父節點 layout 依賴子節點的 size,在呼叫 layout方法時需將 parentUsesSize引數設為 true。 因為,在這種情況下若子節點 re-layout 導致其 size 發生變化,需要及時通知父節點,父節點也需要 re-layout (即 layout dirty 範圍需要向上傳播)。 這一切都是透過上節介紹過的 Relayout Boundary 來實現。

performLayout

本質上, layout是一個模板方法,具體的佈局工作由 performLayout方法完成。  RenderObject#performLayout是一個抽象方法,子類需重寫。

關於 performLayout有幾點需要注意:

  • 該方法由 layout方法呼叫,在需要 re-layout 時應呼叫 layout方法,而不是 performLayout
  • sizedByParenttrue,則該方法不應改變當前 Render Object 的 size ( 其 size 由 performResize方法計算);
  • sizedByParentfalse,則該方法不僅要執行 layout 操作,還要計算當前 Render Object 的 size;
  • 在該方法中,需對其所有子節點呼叫 layout方法以執行所有子節點的 layout 操作,如果當前 Render Object 依賴子節點的佈局資訊,需將 parentUsesSize引數設為 true
// RenderFlexvoid performLayout() {
  RenderBox child = firstChild;  while (child != null) {    final FlexParentData childParentData = child.parentData;
    BoxConstraints innerConstraints = BoxConstraints(minHeight: constraints.maxHeight, maxHeight: constraints.maxHeight);
    child.layout(innerConstraints, parentUsesSize: true);
    child = childParentData.nextSibling;
  }
  size = constraints.constrain(Size(idealSize, crossSize));
  child = firstChild;  while (child != null) {    final FlexParentData childParentData = child.parentData;    double childCrossPosition = crossSize / 2.0 - _getCrossSize(child) / 2.0;
    childParentData.offset = Offset(childMainPosition, childCrossPosition);
    child = childParentData.nextSibling;
  }
}
複製程式碼

上述程式碼片段擷取自 RenderFlex,可以看到它大概做了3件事:

  • 對所有子節點逐個呼叫 layout方法;
  • 計算當前 Render Object 的 size;
  • 將與子節點佈局有關的資訊儲存到相應子節點的 parentData中。

RenderFlex繼承自 RenderBox,是常用的 RowColumn對應的 Render Object。

繪製


markNeedsLayout相似,當 Render Object 需要重新繪製 (paint dirty) 時透過 markNeedsPaint方法上報給 PipelineOwner

markNeedsPaint

  void markNeedsPaint() {    if (isRepaintBoundary) {      assert(_layer is OffsetLayer);      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } 
    else if (parent is RenderObject) {      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    } 
    else {      if (owner != null)
        owner.requestVisualUpdate();
    }
  }
複製程式碼

markNeedsPaint內部邏輯與 markNeedsLayout都非常相似:

  • 若當前 Render Object 是 Repaint Boundary,則將其新增到 PipelineOwner#_nodesNeedingPaint中,Paint request 也隨之結束;
  • 否則,Paint request 向父節點傳播,即需要 re-paint 的範圍擴大到父節點(這是一個遞迴的過程);
  • 有一個特例,那就是『 Render Object Tree 』的根節點,即 RenderView,它的父節點為 nil,此時只需呼叫 PipelineOwner#requestVisualUpdate即可。

PipelineOwner#_nodesNeedingPaint收集的所有 Render Object 都是 Repaint Boundary。

Repaint Boundary

對 Repaint Boundary 從上面 markNeedsPaint的實現可略知一二, 若某 Render Object 是 Repaint Boundary,其會切斷 re-Paint request 向父節點傳播。

更直白點,Repaint Boundary 使得 Render Object 可以獨立於父節點進行繪製, 否則當前 Render Object 會與父節點繪製在同一個 layer 上。 總結一下,Repaint Boundary 有以下特點:

  • 每個 Repaint Boundary 都有一個獨屬於自己的 OffsetLayer (ContainerLayer),其自身及子孫節點的繪製結果都將 attach 到以該 layer 為根節點的子樹上;
  • 每個 Repaint Boundary 都有一個獨屬於自己的 PaintingContext (包括背後的 Canvas),從而使得其繪製與父節點完全隔離開。
Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖,由於 Root/ RA/ RC/ RG/ RI是 Repaint Boundary,所以它們都有對應的 OffsetLayer。 同時,由於每個 Repaint Boundary 都有屬於自己的 PaintingContext,所以它們都有對應的 PictureLayer,用於呈現具體的繪製結果。 對於那些不是 Repaint Boundary 的節點,將會繪製到最近的 Repaint Boundary 祖先節點提供的 PictureLayer 上。

Repaint Boundary 會影響兄弟節點的繪製,如由於 RC是 Repaint Boundary,導致 RBRD被繪製到不同的 PictureLayer 上。

實現中,『 Layer Tree 』往往會比上圖所示更復雜,由於每個 Render Object 在繪製過程中都可以自主引入更多的 layer。

Repaint Boundary 的目標是最佳化效能,但從上面的討論我們也可以看出 Repaint Boundary 會增加『 Layer Tree 』的複雜度。 因此,Repaint Boundary 並不是越多越好。 只適用於那些需要頻繁重繪的場景,如影片。

Flutter Framework 為開發者預定義了 RepaintBoundary widget,其繼承自 SingleChildRenderObjectWidget,在有需要時我們可以透過 RepaintBoundary widget 來新增 Repaint Boundary。

Paint

void paint(PaintingContext context, Offset offset) { }
複製程式碼

抽象基類 RenderObject中的 paint是個空方法,需要子類重寫。  paint方法主要有2項任務:

  • 當前 Render Object 本身的繪製,如: RenderImage,其 paint方法主要職責就是 image 的渲染
  void paint(PaintingContext context, Offset offset) {
    paintImage(
      canvas: context.canvas,
      rect: offset & size,
      image: _image,
      ...
    );
  }
複製程式碼
  • 繪製子節點,如: RenderTable,其 paint方法主要職責是依次對每個子節點呼叫 PaintingContext#paintChild方法進行繪製:
  void paint(PaintingContext context, Offset offset) {    for (int index = 0; index < _children.length; index += 1) {      final RenderBox child = _children[index];      if (child != null) {        final BoxParentData childParentData = child.parentData;
        context.paintChild(child, childParentData.offset + offset);
      }
    }
  }
複製程式碼

串起來

Framework是什麼?Flutter Framework 之 RenderObject深入分析

下面我們將整個繪製流程串一串,如上圖:

PipelineOwner#flushPaint

當新一幀開始時,會觸發 PipelineOwner#flushPaint方法,進而對收集的所有『 paint-dirty Render Obejcts 』進行 re-paint:

void flushPaint() {    try {      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];      // Sort the dirty nodes in reverse order (deepest first).
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {        assert(node._layer != null);        if (node._needsPaint && node.owner == this) {
          PaintingContext.repaintCompositedChild(node);
        }
      }
    } 
  }
複製程式碼

PaintingContext#repaintCompositedChild

PaintingContext#repaintCompositedChild是一個非常重要的方法,一是為『 paint-dirty Render Obejcts 』建立 Layer (若沒有)、二是為 RenderObject 的繪製準備 context併發起繪製流程:

  static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext childContext,
  }) {    assert(child.isRepaintBoundary);
    OffsetLayer childLayer = child._layer;    if (childLayer == null) {
      child._layer = childLayer = OffsetLayer();
    } 
    else {      assert(childLayer is OffsetLayer);
      childLayer.removeAllChildren();
    }    // 在正常的繪製流程中透過引數傳遞過來的 childContext 都是 null
    // 因此,此處總是會建立新的 PaintingContext
    //
    childContext ??= PaintingContext(child._layer, child.paintBounds);
    child._paintWithContext(childContext, Offset.zero);
    childContext.stopRecordingIfNeeded();
  }
複製程式碼

RenderObject#_paintWithContext

RenderObject#_paintWithContext邏輯比較簡單,主要就是呼叫 paint方法;

  void _paintWithContext(PaintingContext context, Offset offset) {    if (_needsLayout)      return;
    _needsPaint = false;
    paint(context, offset);
  }
複製程式碼

paint方法正如上節所講,透過 Canvas#draw***系列方法進行具體的繪製操作,對於子節點(若有)呼叫 PaintingContext#paintChild方法;

PaintingContext#paintChild

對於當前繪製子節點,若是 Repaint Boundary,則需要在獨立的 layer 上進行繪製, 否則直接呼叫子節點的 _paintWithContext方法在當前上下文(paint context)中繪製:

  void paintChild(RenderObject child, Offset offset) {    if (child.isRepaintBoundary) {
      stopRecordingIfNeeded();
      _compositeChild(child, offset);
    } 
    else {
      child._paintWithContext(this, offset);
    }
  }
複製程式碼

下面重點分析對 Repaint Boundary 的處理,如上 paintChild所示,首先會呼叫 PaintingContext#stopRecordingIfNeeded來停止當前的繪製工作:

  void stopRecordingIfNeeded() {    if (!_isRecording)      return;
    _currentLayer.picture = _recorder.endRecording();
    _currentLayer = null;
    _recorder = null;
    _canvas = null;
  }
複製程式碼

stopRecordingIfNeeded首先將當前繪製結果儲存到 _currentLayer.picture中,之後對上下文做一些清理。 將 _currentLayer置為 null,初看感覺難於理解,儲存在 _currentLayer.picture中的繪製結果且不是就丟了? 其實, _currentLayer早在 _startRecording方法中就被新增到『 Layer Tree 』上,這裡只是將 PaintingContext中的引用置為 null 而以。

  void _startRecording() {    assert(!_isRecording);
    _currentLayer = PictureLayer(estimatedBounds);
    _recorder = ui.PictureRecorder();
    _canvas = Canvas(_recorder);
    _containerLayer.append(_currentLayer);
  }
複製程式碼

由於已經將 _canvas置為 null 了,下次使用時會觸發對 _startRecording方法的呼叫:

  Canvas get canvas {    if (_canvas == null)
      _startRecording();    return _canvas;
  }
複製程式碼

PaintingContext#_compositeChild

_compositeChild中,透過 repaintCompositedChild對子節點發起新一輪的繪製,並將繪製結果( child._layer)新增到『 Layer Tree 』中:

  void _compositeChild(RenderObject child, Offset offset) {    assert(!_isRecording);    assert(child.isRepaintBoundary);    assert(_canvas == null || _canvas.getSaveCount() == 1);
    repaintCompositedChild(child, debugAlsoPaintedParent: true);    final OffsetLayer childOffsetLayer = child._layer;
    childOffsetLayer.offset = offset;
    appendLayer(child._layer);
  }
複製程式碼

小結

整個繪製過程其實就是對『 RenderObject Tree 』進行深度遍歷的過程; Repaint Boundary 會獨立於父節點進行繪製,因此需要獨立的 ContainerLayer(OffsetLayer) 以及 PaintingContext。

跑起來


下面我們簡單的分析一下,一個 Flutter App 是怎麼跑起來的。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}
複製程式碼

runApp是 Flutter 專案的入口點,完成 Binding 的初始化、attach root widget、安排首幀的排程。

Framework是什麼?Flutter Framework 之 RenderObject深入分析

如上圖,在 RendererBinding#initInstances中會建立『 RenderObject Tree 』的根節點: RenderView

如下程式碼,在 RenderView初始化過程中為首幀渲染作準備,『 Layer Tree 』根節點也是在此過程中建立的。

  // RenderView
  //
  void prepareInitialFrame() {
    scheduleInitialLayout();
    scheduleInitialPaint(_updateMatricesAndCreateNewRootLayer());
  }  Layer _updateMatricesAndCreateNewRootLayer() {
    _rootTransform = configuration.toMatrix();    final ContainerLayer rootLayer = TransformLayer(transform: _rootTransform);
    rootLayer.attach(this);    return rootLayer;
  }
複製程式碼
  // RenderObject
  //
  void scheduleInitialLayout() {
    _relayoutBoundary = this;
    owner._nodesNeedingLayout.add(this);
  }  void scheduleInitialPaint(ContainerLayer rootLayer) {
    _layer = rootLayer;
    owner._nodesNeedingPaint.add(this);
  }
複製程式碼

如下圖,在 WidgetsFlutterBinding#scheduleAttachRootWidget-> WidgetsFlutterBinding#attachRootWidget呼叫鏈上會建立 Root Widget: RenderObjectToWidgetAdapter

Framework是什麼?Flutter Framework 之 RenderObject深入分析

RenderObjectToWidgetAdapter#attachToRenderTree方法中『 Element Tree 』的根節點 RenderObjectToWidgetElement被建立出來。 至此:

  • Root Widget( RenderObjectToWidgetAdapter)
  • 『 Element Tree 』的根節點( RenderObjectToWidgetElement)
  • 『 RenderObject Tree 』的根節點( RenderView)
  • 『 Layer Tree 』的根節點( TransformLayer)

建立完成。

  // RenderObjectToWidgetAdapter
  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
      owner.lockState(() {
        element = createElement();
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null);
      });      // This is most likely the first time the framework is ready to produce
      // a frame. Ensure that we are asked for one.
      SchedulerBinding.instance.ensureVisualUpdate();    return element;
  }  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
複製程式碼

上述程式碼第 9行,對 Root Element 進行掛載(mount)。 隨之,『 Element Tree 』被逐步建立出來。

Framework是什麼?Flutter Framework 之 RenderObject深入分析
  // RenderObjectToWidgetAdapter
  void _rebuild() {
    _child = updateChild(_child, widget.child, _rootChildSlot);
  }
複製程式碼

隨著『 Element Tree 』的構建,『 RenderObject Tree 』也被建立出來。

  // RenderObjectElement
  void mount(Element parent, dynamic newSlot) {    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
複製程式碼

小結


本文對 RenderObject 生命週期中幾個重要節點:建立、佈局、繪製等作了簡要介紹。 對一些重要概念,如:Relayout Boundary、Repaint Boundary 也進行了較詳細的分析。
更多Android技術分享可以關注@我,也可以加入QQ群號:1078469822,學習交流Android開發技能。

作者:峰之巔
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2794046/,如需轉載,請註明出處,否則將追究法律責任。

相關文章