【Flutter脫髮錄】盤一盤RenderObject

Widget篇中,提到了RenderObject,那麼RenderObject究竟是個啥?我們來盤一盤!node

官方解釋

An object in the render tree.緩存

render樹上的一個對象。bash

Flutter門前有四棵樹,一棵是Widget樹,一棵是Element樹,一棵是Render樹,還有一棵是layer樹。ide

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

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

閱讀源碼

美好的一天應當從閱讀源碼開始!測試

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

能夠看到RenderObject也是鏈表結構,混入了DiagnosticableTreeMixin樹狀結構的特性,而且實現了命中測試抽象類。ui

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

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();
}
複製代碼

劃一下重點:spa

  • 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底層原理,對咱們實現自定義的控件也有幫助。至於具體如何根據佈局繪製到屏幕上,後文會使用實例分析。

搞懂原理真是傷頭髮啊!

相關文章
相關標籤/搜索