【Flutter脫髮錄】RenderObject到底是個啥?

愛新覺羅狗剩兒發表於2020-04-12

Widget篇中,提到了RenderObject,那麼RenderObject到底是個啥?我們來捋一捋!

官方解釋

An object in the render tree.

render樹上的一個物件。

Flutter門前有四棵樹,一棵是Widget樹,一棵是Element樹,一棵是Render樹,還有一棵是layer樹。接下來盤一盤這棵Render樹。

Widget樹是一張圖紙,那麼Render樹就是這張圖紙對應的流水線。RenderObject就是這條流水線上的操作員工。

我們在頂層配置好Widget樹後,最終render樹負責在手機螢幕上表現出你想要的畫面。

閱讀原始碼

美好的一天應當從閱讀原始碼開始!

abstract class RenderObject extends AbstractNode 
with DiagnosticableTreeMixin 
implements HitTestTarget {}
複製程式碼

可以看到RenderObject也是連結串列結構,混入了DiagnosticableTreeMixin樹狀結構的特性,並且實現了命中測試抽象類。

去掉debug和assert的程式碼,RenderObject的結構功能清晰可見。

Layout 模組

abstract class RenderObject {
  // LAYOUT 模組
  // 傳遞資訊給child的儲存容器  通常為偏移量Offset
  ParentData parentData;

  // 此方法,在child加入到child列表之前,把parentData傳遞給child
  void setupParentData(covariant RenderObject child) {
    if (child.parentData is! ParentData)
      child.parentData = ParentData();
  }

  // 新增一個render object 作為自己的child
  @override
  void adoptChild(RenderObject child) {
    // 設定parentData
    setupParentData(child);
    // 標記更新
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }

  // 刪除一個render object 作為自己的child
  @override
  void dropChild(RenderObject child) {
    // 清除邊界
    child._cleanRelayoutBoundary();
    // 失去parentdata訪問許可權
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
  }

  // 遍歷children
  void visitChildren(RenderObjectVisitor visitor) { }

  // render tree的管理者
  @override
  PipelineOwner get owner => super.owner;

  // 告訴其owner將其插入render tree 並初次標記需要計算layout並且重繪
  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (_needsLayout && _relayoutBoundary != null) {
      _needsLayout = false;
      markNeedsLayout();
    }
    if (_needsCompositingBitsUpdate) {
      _needsCompositingBitsUpdate = false;
      markNeedsCompositingBitsUpdate();
    }
    if (_needsPaint && _layer != null) {
      _needsPaint = false;
      markNeedsPaint();
    }
    if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
      _needsSemanticsUpdate = false;
      markNeedsSemanticsUpdate();
    }
  }

  // 是否需要佈局
  bool _needsLayout = true;

  // 記錄佈局邊界在哪
  RenderObject _relayoutBoundary;

  // parent的約束
  @protected
  Constraints get constraints => _constraints;
  Constraints _constraints;

  //標記需要重新layout
  void markNeedsLayout() {
    // 如果自己不是邊界,則讓parent.markNeedsLayout處理 一直推到邊界
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      // owner 把自己加入layout的髒表 請求重新整理
      _needsLayout = true;
      if (owner != null) {
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }

  // 遍歷清除非邊界child relayout邊界記錄  當邊界發生變化時,會呼叫此方法清除邊界快取
  void _cleanRelayoutBoundary() {
    if (_relayoutBoundary != this) {
      _relayoutBoundary = null;
      _needsLayout = true;
      visitChildren((RenderObject child) {
        child._cleanRelayoutBoundary();
      });
    }
  }

  // 僅layout 不重新測量大小
  void _layoutWithoutResize() {
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    // 確定邊界
    // 滿足以下四項條件 中的一項  則自己為邊界
    // parentUsesSize parent是否關心自己的大小
    // sizedByParent 由parent確認大小
    // constraints.isTight 受嚴格約束
    // parent 不為 RenderObject 即自己為root節點
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      // 否則使用parent的邊界
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
    _constraints = constraints;
    // 如果邊界發生變化  則遍歷清空所有已記錄的邊界 重新設定
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren((RenderObject child) {
        child._cleanRelayoutBoundary();
      });
    }
    _relayoutBoundary = relayoutBoundary;
    // sizedByParent 為true 時 才會呼叫performResize 確定大小
    // 否則在performLayout中確定size
    if (sizedByParent) {
      performResize();
    }
    // 計算layout
      performLayout();
      markNeedsSemanticsUpdate();
    _needsLayout = false;
    markNeedsPaint();
  }

  // size是否只由parent的約束決定  預設為false  永遠不會錯
  // 當為true時,確定size的工作要在performResize中計算
  @protected
  bool get sizedByParent => false;

  // 子類重寫此方法  根據約束 計算自身的size
  // 不能直接呼叫此方法  而是呼叫layout方法間接呼叫  只有sizedByParent為true時才會執行
  @protected
  void performResize();

  // 子類重寫此方法
  // 不能直接呼叫此方法  而是呼叫layout方法間接呼叫
  // 作用1:當sizedByParent為false時  根據適應child計算自身size
  // 作用2:遍歷呼叫child.layout 確定child的size和offsets
  @protected
  void performLayout();
}
複製程式碼

劃一下重點:

  • RenderObject作為Render Tree上的一個物件,但是自己卻不能直接改變這棵大樹。一舉一動都需要傳達給PipelineOwner這位管理者去執行。
  • PipelineOwner管理著幾個汙汙的(dirty)小本本,記錄了需要變化的節點,根據這些小本本去實際更新Render Tree。當RenderObject需要發生改變時,通知owner把自己寫到小本本上(標記為dirty)。可以理解為軍訓過程中,有什麼問題報告給教官,教官統一安排。
  • parent通過setupParentData方法,傳遞parentData,通常為layout offset
  • performResize方法需要子類重寫。計算自身的大小
  • performLayout方法需要子類重寫。
  1. 當sizedByParent為false時,計算size。
  2. 2.遍歷呼叫child.layout,確定child的size和offsets。
  • 核心方法是layout
確定`_relayoutBoundary`佈局邊界
->呼叫`performResize`和`performLayout`方法計算大小和位置
->重繪
複製程式碼

由於這個流程滿足大多數場景,因此當我們真正開發時,只關心重寫performResizeperformLayout的實現,而不會去重寫layout方法。

Paint 模組

abstract class RenderObject {
  // PAINTING 模組
  RenderObject() {
    // 是否需要混合圖層 =  是重繪邊界 || 總是混合圖層
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }

  // 當前是否為重繪邊界
  bool get isRepaintBoundary => false;

  // 是否總是新建圖層然後合併到原圖層
  @protected
  bool get alwaysNeedsCompositing => false;

  // 快取的layer
  ContainerLayer _layer;

  // 是否_needsCompositing的值需要設定
  bool _needsCompositingBitsUpdate = false;

  // 標記_needsCompositing的值需要重新設定
  void markNeedsCompositingBitsUpdate() {
    if (owner != null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }

  // 是否在一個新的圖層繪製然後合併到祖先圖層
  // true:在新圖層繪製 但是新圖層會優先使用快取圖層 以提高效能
  // false:不使用新圖層  此時快取圖層一定要置null
  // 當前為repaintBoundary時 _needsCompositing=true 並且會自動給快取layer賦值為新的OffsetLayer 在此layer上繪製後  合併到祖先圖層
  bool _needsCompositing;

  // 遍歷設定需要使用圖層混合
  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;
  }

  // 是否需要繪製
  bool _needsPaint = true;

  // 標記需要重繪
  void markNeedsPaint() {
    _needsPaint = true;
    // 如果當前為repaintBoundary 則通知owner需要重繪
    if (isRepaintBoundary) {
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    }
    // 如果當前不為root節點  則讓parent判斷
    else if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      parent.markNeedsPaint();
    }
    // 若當前為root節點  則直接通知owner重繪
    else {
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }

  // 根節點初始化  見[RenderView]
  void scheduleInitialPaint(ContainerLayer rootLayer) {
    _layer = rootLayer;
    owner._nodesNeedingPaint.add(this);
  }

  // 替換rootLayer  只有root節點才會呼叫
  // 當裝置的畫素比device pixel ratio變化時  可能會呼叫此方法
  void replaceRootLayer(OffsetLayer rootLayer) {
    _layer.detach();
    _layer = rootLayer;
    markNeedsPaint();
  }

  // 子類重寫此方法 在對應的offset完成真正的繪製操作
  // 不可直接呼叫此方法  而是用markNeedsPaint標記 讓owner去處理
  // 如果只想繪製一個child 則用PaintingContext.paintChild 介面的方式去操作  避免直接操作render object
  void paint(PaintingContext context, Offset offset) { }
}
複製程式碼

本模組中最重要的一點就是needsCompositing這個變數。這個變數決定是否在新的layer上繪製。在構造器中可以看到,它由isRepaintBoundaryalwaysNeedsCompositing決定。isRepaintBoundary這個可以通過RepaintBoundary包裹修改其為truealwaysNeedsCompositing可以由子類修改。

因此我們可以通過使用RepaintBoundary這個控制元件達到區域性重繪的目的,以提高效能。

SEMANTICS 語義化模組 和 HIT TESTING 命中測試模組

語義化即Semantics,主要是提供給讀屏軟體的介面,也是實現輔助功能的基礎,通過語義化介面可以讓機器理解頁面上的內容,對於有視力障礙使用者可以使用讀屏軟體來理解UI內容。除非有特殊的需求,一般接觸不到。

通過以下方法,處理命中測試結果。

// 重寫此方法處理事件
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
複製程式碼

RenderObject中還有兩個比較重要的方法,也提一下。

  // 是否重灌 只在debug模式下生效 也就是開發中的hot reload效果
  void reassemble() {
    // 標記為需要重新layout
    markNeedsLayout();
    // 標記重新設定是否使用新圖層
    markNeedsCompositingBitsUpdate();
    // 標記需要重繪
    markNeedsPaint();
    // 標記語義化需要更新
    markNeedsSemanticsUpdate();
    // 遍歷child的reassemble方法 全部標記一遍
    visitChildren((RenderObject child) {
      child.reassemble();
    });
  }
  
  // 是否顯示在螢幕上  viewport中會使用到
  void showOnScreen({
    RenderObject descendant,
    Rect rect,
    Duration duration = Duration.zero,
    Curve curve = Curves.ease,
  }) {
    if (parent is RenderObject) {
      final RenderObject renderParent = parent;
      renderParent.showOnScreen(
        descendant: descendant ?? this,
        rect: rect,
        duration: duration,
        curve: curve,
      );
    }
  }
複製程式碼

PipelineOwner

The pipeline owner manages the rendering pipeline.

整個渲染流程的管理者,具體表現在如下幾個方法:

  1. flushLayout

遍歷需要relayout的render object節點,呼叫_layoutWithoutResize()重寫計算佈局

  1. flushCompositingBits

遍歷需要CompositingBitsUpdate的節點,呼叫_updateCompositingBits()方法更新needsCompositing。通常在flushLayoutflushPaint之間執行。

  1. flushPaint

遍歷需要repaint的節點,通過PaintingContext呼叫子render object_paintWithContext方法觸發paint繪製。

  1. flushSemantics

更新語義化

RenderBox

RenderBoxRenderObject的子類,是在2D笛卡爾座標系下對RenderObject的進一步封裝。它主要封裝瞭如下幾個功能點:

  1. parentdata是BoxParentData 只有offset屬性 預設為Offset.zero
  2. 使用BoxConstraints作為其約束
  3. 使用size記錄其大小
  4. 測量自身最大最小寬高
  5. 測量基線
  6. 實現預設的命中測試方案
  7. 混入RenderObjectWithChildMixin單個child的實現->SingleChildRenderObjectWidget
  8. 混入ContainerRenderObjectMixin多個children的實現->MultiChildRenderObjectWidget

開發App在笛卡爾座標系上進行繪製,所以繼承RenderBox就可以滿足大部分場景了。

總結

本節主要記錄了RenderObject主要的功能和方法,理解這些內容可以幫助我們更好的理解Flutter UI底層原理,對我們實現自定義的控制元件也有幫助。至於具體如何根據佈局繪製到螢幕上,後文會使用例項分析。

搞懂原理真是傷頭髮啊!

相關文章