【Flutter 專題】102 何為 Flutter RenderObjects ?

阿策小和尚發表於2021-07-07

      小菜前段時間簡單瞭解了一下 WidgetElement,其中 Widget 主要是存放渲染內容以及佈局資訊等,僅作為一個資訊儲存的容器;Element 主要用於存放上下文環境,遍歷 UI View 檢視樹;而小菜今天嘗試學習的 RenderObject 才是 UI View 真正的渲染部分;

RenderObject

      RenderObject 作為渲染樹中的一個物件;其 layout()paint() 是渲染庫核心,負責管理佈局和渲染等;RenderObject 定義了佈局繪製協議,但並沒定義具體佈局繪製模型;

原始碼分析

      RenderObject 可以從多個維度研究,可以通過 layout()paint() 對比 Android 的繪製流程,也可以根據其屬性和互動的物件(parent / owner / child)來學習;小菜從頭開始為了儘可能多的瞭解原始碼,嘗試第二種方式進一步學習;

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
    AbstractNode _rootNode;
    ParentData parentData;
    Constraints _constraints;
    @protected
    Constraints get constraints => _constraints;
    PipelineOwner _owner;
    bool get attached => _owner != null;
    void setupParentData(covariant RenderObject child) {}
    void adoptChild(RenderObject child) {}
    void dropChild(RenderObject child) {}
    void attach(PipelineOwner owner) {}
    void detach() {}
}
複製程式碼

parent 相關

1. ParentData
ParentData parentData;

void setupParentData(covariant RenderObject child) {
    assert(_debugCanPerformMutations);
    if (child.parentData is! ParentData)
      child.parentData = ParentData();
}
複製程式碼

      RenderObject 包括兩個重要屬性 parentParentData 插槽;ParentData 做為一個預留的變數,由 parent 賦值,傳遞資訊給 child 的儲存容器;通常所有和 child 特定的資料都可以儲存在 ParentData 中;

2. Constraints
void layout(Constraints constraints, { bool parentUsesSize = false }) {
    RenderObject relayoutBoundary;
    if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
      relayoutBoundary = this;
    } else {
      final RenderObject parent = this.parent;
      relayoutBoundary = parent._relayoutBoundary;
    }
   
    if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) return;
    _constraints = constraints;
    if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
      visitChildren((RenderObject child) {
        child._cleanRelayoutBoundary();
      });
    }
    _relayoutBoundary = relayoutBoundary;
    
    if (sizedByParent) {
      try {
        performResize();
      } catch (e, stack) {
        _debugReportException('performResize', e, stack);
      }
    }
    RenderObject debugPreviousActiveLayout;
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    _needsLayout = false;
    markNeedsPaint();
}
複製程式碼

      Constraints 作為 RenderObjectparentchild 之間的佈局約束;layout() 作為 RenderObject 的核心方法,需要傳入 Constraints 作為約束,配合 parentUsesSize 判斷 RenderObjectchild 子節點發生變化時,parent 父節點是否需要重新繪製;

3. relayoutBoundary
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
  relayoutBoundary = this;
} else {
  final RenderObject parent = this.parent;
  relayoutBoundary = parent._relayoutBoundary;
}
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
  visitChildren((RenderObject child) {
    child._cleanRelayoutBoundary();
  });
}

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

      layout() 中定義了一個 RenderObject 型別的 relayoutBoundary 佈局邊界,如果佈局邊界發生變化,則遍歷清空所有已記錄的邊界並重新設定;

      markNeedsLayout() 中也需要進行佈局邊界判斷,若 RenderObject 自身不是 relayoutBoundary,則向 parent 父節點查詢,直到找到確定是 relayoutBoundaryRenderObject 並標記為 dirty

      layout() 確定自己是否為邊界需要判斷四個條件,分別是 !parentUsesSize parent 父節點是否關心自己的大小;sizedByParent 是否由 parent 父節點判斷大小;constraints.isTight 是否嚴格約束;parent is! RenderObject 自身是否為 root 根節點;

owner 相關

      PipelineOwner 作為整個渲染流程的管理者;提供用於驅動渲染管道的介面,並儲存在管道的每個階段中已請求訪問渲染物件的狀態等;

1. flushLayout
void flushLayout() {
    if (!kReleaseMode) {
      Timeline.startSync('Layout', arguments: timelineWhitelistArguments);
    }
    try {
      while (_nodesNeedingLayout.isNotEmpty) {
        final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
        _nodesNeedingLayout = <RenderObject>[];
        for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {
      if (!kReleaseMode)  Timeline.finishSync();
    }
}
複製程式碼

      flushLayout() 用於遍歷所有標記為 dirty 的需要重新佈局的 RenderObjects 並重新計算其佈局尺寸和位置等;

2. flushCompositingBits
void flushCompositingBits() {
    if (!kReleaseMode) {
      Timeline.startSync('Compositing bits');
    }
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    if (!kReleaseMode) {
      Timeline.finishSync();
    }
}
複製程式碼

      flushCompositingBits() 用於遍歷所有標記為 dirty 的需要 CompositingBitsUpdate 合併更新的子節點,再次階段,每個 RenderObject 都會了解其子節點是否需要合併更新;

3. flushPaint
void flushPaint() {
    if (!kReleaseMode) {
      Timeline.startSync('Paint', arguments: timelineWhitelistArguments);
    }
    try {
      final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
      _nodesNeedingPaint = <RenderObject>[];
      for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
        if (node._needsPaint && node.owner == this) {
          if (node._layer.attached) {
            PaintingContext.repaintCompositedChild(node);
          } else { node._skippedPaintingOnLayer(); }
        }
      }
    } finally {
      if (!kReleaseMode) { Timeline.finishSync(); }
    }
}
複製程式碼

      flushPaint() 用於遍歷所有標記為 dirty 的需要重新繪製的子節點,並生成 Layer 用於繪製展示;

4. attach / detach
@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();
    }
}
複製程式碼

      layout() 中在 attach()detach() 中也需要 PipelineOwnerattach() 主要通知管理者 owner 將其插入到渲染樹中標記需要計算佈局 layout 並呼叫 markNeedsPaint 重新繪製;detach() 主要是通知管理者取消關聯;

child 相關

      對於 child 子節點,小菜主要學習如下三個方法;

1. adoptChild
@override
void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
}
複製程式碼

      adoptChild() 主要是 RenderObject 新增一個 child 子節點;其中需要通過 setupParentData() 來獲取 ParentData 中的資料並更新;

2. dropChild
@override
void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
}
複製程式碼

      dropChild() 是和 adoptChild() 對應的方法,主要用於 RenderObject 刪除一個 child 子節點;刪除過程中需要 _cleanRelayoutBoundary 清除邊界並刪除 ParentData,之後再更新;

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

      paintChild() 為繪製一個子節點的 RenderObject;如果該子節點有自己合成層,則 child 子節點將被合成到與此繪製相關的上下文相關的 Layer 層中;

RenderBox

      RenderObject 並沒定義具體佈局繪製模型,所以小菜簡單學習了一下 RenderBoxRenderBoxRenderObject 的子類,以螢幕左上角為原點(包括頂部狀態列)座標系;BoxParentData 作為 child 子節點傳輸資料,BoxConstraints 作為其約束條件,通過 Size 記錄其尺寸大小;可以定義具體的佈局繪製模型;


      RenderObject 涉及的方式方法較多,小菜對於原始碼的理解還不夠深入,如有錯誤,請多多指導!

來源: 阿策小和尚

相關文章