Flutter的繪製流程簡述

大逗大人發表於2019-11-24

相對於React NativeWeex等跨平臺框架,Flutter擁有自己的UI繪製體系,避免了React NativeWeex等跨平臺框架與Native系統的橋接,從而更好的提升了效能。

Flutter中,UI都是一幀一幀的繪製,但這繪製的背後都會經過如下階段。

  1. 動畫與微任務階段,主要是處理動畫及執行一系列微任務。
  2. 構建階段(build),找出標記為“髒”的節點與佈局邊界之間的所有節點,並做相應的更新。
  3. 佈局階段,計算Widget的大小及位置的確定。
  4. compositingBits階段,重繪之前的預處理操作,檢查RenderObject是否需要重繪。
  5. 繪製階段,根據Widget大小及位置來繪製UI。
  6. compositing階段,將UI資料傳送給GPU處理。
  7. semantics階段,與平臺的輔助功能相關。
  8. finalization階段,主要是從Element樹中移除無用的Element物件及處理繪製結束回撥。

下面就來分析上述的各個階段

1、動畫與微任務階段

該階段主要是處理動畫及微任務。先來看動畫的處理,在使用動畫時,很多時候都會新增一個回撥函式來進行狀態獲取或資料更新,如通過addListeneraddStatusListener等函式來新增,而這些回撥函式就會在本階段來執行。具體是在SchedulerBinding中的handleBeginFrame函式中實現。

  void handleBeginFrame(Duration rawTimeStamp) {
    ...
    try {
      // TRANSIENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.transientCallbacks;
      //切換為transientCallbacks階段
      final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
      //清空已註冊的回撥函式
      _transientCallbacks = <int, _FrameCallbackEntry>{};
      //遍歷所有註冊的回撥方法
      callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
        if (!_removedIds.contains(id))
          //執行已經註冊的回撥函式
          _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp, callbackEntry.debugStack);
      });
      _removedIds.clear();
    } finally {
      //切換為midFrameMicrotasks階段
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
  }
複製程式碼

_invokeFrameCallback就會呼叫在使用動畫時註冊的回撥函式,這裡僅執行一次。如果我們在執行時呼叫_invokeFrameCallback函式的程式碼註釋調,那麼就無法獲取動畫的狀態,從而影響動畫的正確執行。

當回撥函式執行完畢後,就會進入微任務階段,在該階段會執行一系列微任務,由於這涉及到Flutter的非同步任務體系,因此這裡就不再敘述。

2、build階段

在上一階段執行完畢後,就進入build階段,在該階段主要是重新構建標記為“髒”的Widget節點及將需要更新的RenderObject物件標記為“髒”。

handleBeginFrame函式執行完畢後,就會執行handleDrawFrame函式,該函式在SchedulerBinding物件初始化時會與Window相關聯,所以除第一次需要主動呼叫外,其他時候皆是通過Window來呼叫該函式。

  void handleDrawFrame() {
    assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
    Timeline.finishSync(); // end the "Animate" phase
    try {
      //持久幀回撥,該回撥會一直存在,不會移除
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);

      //當前幀繪製完成回撥
      _schedulerPhase = SchedulerPhase.postFrameCallbacks;
      final List<FrameCallback> localPostFrameCallbacks =
          List<FrameCallback>.from(_postFrameCallbacks);
      _postFrameCallbacks.clear();
      //當執行這裡時,代表當前幀已經繪製完畢
      for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
    } finally {
      //進入空閒狀態
      _schedulerPhase = SchedulerPhase.idle;
      Timeline.finishSync(); // end the Frame
      profile(() {
        _profileFrameStopwatch.stop();
        _profileFramePostEvent();
      });
      _currentFrameTimeStamp = null;
    }
  }
複製程式碼

這裡重點關注持久幀回撥,該回撥也是UI繪製的關鍵函式,是在RendererBinding物件初始化時註冊的。

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    ...
    //註冊持久幀回撥
    addPersistentFrameCallback(_handlePersistentFrameCallback);
  }
  void _handlePersistentFrameCallback(Duration timeStamp) {
    //繪製幀
    drawFrame();
  }
  //繪製幀
  void drawFrame() {
    //對Widget進行測量、佈局
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    //對Widget進行繪製
    pipelineOwner.flushPaint();
    //傳送資料給GPU
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
}
複製程式碼

根據函式名可以發現並沒有發現關於構建Widget的相關函式,那麼在何時構建尼?通過檢視原始碼可以發現,在WidgetsBinding中重寫了drawFrame函式。在該函式中會建立新的Widget物件替換舊的Widget物件並將不需要的Element節點從樹中移除。

  @override
  void drawFrame() {
    ...
    try {
      //如果根結點存在,就重新構建Widget
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      //呼叫RendererBinding中的drawFrame函式
      super.drawFrame();
      //移除不再使用的Element節點
      buildOwner.finalizeTree();
    } finally {...}
    ...
  }
複製程式碼

2.1、重新build Widget物件

Widget物件的建立是在buildScope()函式中實現的,這是一個非常重要的函式,具體實現如下。

  void buildScope(Element context, [ VoidCallback callback ]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    try {
      //“髒”節點列表需要重新排序
      _scheduledFlushDirtyElements = true;
      ...
      //將標記為“髒”的Element節點根據深度進行排序
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      //標記為“髒”的Element節點數量
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      //遍歷“髒”節點
      while (index < dirtyCount) {
        try {
          //重新構建Widget,及是否複用當前Element
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ...
        }
        index += 1;
        //當_dirtyElements集合中的“髒”節點還未處理完畢時,又新增了新的“髒”節點
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          //根據“髒”節點的深度進行排序
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          //如果當前節點的深度比新加入的“髒”節點深度要深,則需要將處理座標指向新加入的“髒”節點
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            index -= 1;
          }
        }
      }
    } finally {
      //清除_dirtyElements中所有節點的“髒”狀態
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
    }
  }
複製程式碼

_dirtyElements是一個集合,儲存了所有標記為“髒”的節點。在對其中的“髒”節點進行處理時,需要首先對集合中的“髒”節點進行排序,其排序規則如下。

  • 如果“髒”節點的深度不同,則按照深度進行升序排序
  • 如果“髒”節點的深度相同,則會將“髒”節點放在集合的右側,“乾淨”節點則在在集合的左側。

在排序完成後,就要遍歷該集合,對其中的“髒”節點進行處理。在這裡呼叫的是rebuild函式,通過該函式,會重新建立“髒”節點下的所有Widget物件,並根據新的Widget物件來判斷是否需要重用Element物件。一般只要不是增刪WidgetElement物件都會被重用,從而也就會重用RenderObject物件。由於Widget是一個非常輕量級的資料結構,所以在UI更新時做到了把效能損耗降到最低。

這裡要注意一點的是,如果_dirtyElements中的“髒”節點還未處理完畢,就又新增了“髒”節點,那麼這時候就會重新排序,保證_dirtyElements集合的左側永遠是“乾淨”節點,右側永遠是“髒”節點。

由於rebuild函式比較重要,這裡就重點介紹一下該函式,在rebuild函式中會呼叫performRebuild函式,該函式是一個抽象函式,在其子類實現,而標記為“髒”的Element都是StatefulElement。所以就來StatefulElement或者其父類中查詢performRebuild函式。

abstract class ComponentElement extends Element {
  ...
  @override
  void performRebuild() {
    Widget built;
    try {
      //重新建立新的`Widget`物件
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      //當構建Widget物件出錯時展示的預設頁面,可以修改該頁面來使異常介面更友好的顯示
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
    } finally {
      //清除“髒”標記
      _dirty = false;
    }
    try {
      //更新子Element對應的Widget物件
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      //當構建Widget物件出錯時展示的預設頁面
      built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
      _child = updateChild(null, built, slot);
    }
  }
}
複製程式碼

performRebuild函式做的事很簡單,就是建立新的Widget物件來替換舊的物件。上面的build函式呼叫的就是State類中的build函式,然後再呼叫ElementupdateChild函式,該函式在Flutter之Widget層級介紹中進行了簡單的介紹,就是更新Element對應的Widget物件。而在updateChild函式中又會呼叫子Elementupdate函式,從而呼叫子ElementperformRebuild,然後在呼叫子ElementupdateChildupdate函式。以此類推,從而更新其所有子ElementWidget物件。

Flutter的繪製流程簡述
最後就是呼叫葉子節點的updateRenderObject函式來更新RenderObject。在更新RenderObject物件時,會根據情況來對需要重新佈局及重新繪製的RenderObject物件進行標記。然後等待下一次的Vsync訊號時來重新佈局及繪製UI。

2.2、標記RenderObject

對於RenderObject物件,可以通過markNeedsLayoutmarkNeedsPaint來標記是否需要重新佈局及重新繪製。但在當前階段只會呼叫markNeedsLayout來標記需要重新佈局的RenderObject物件,在下一階段才會標記需要重新繪製的RenderObject,所以先來看markNeedsLayout函式。

  void markNeedsLayout() {
    ...
    //判斷佈局邊界是否是是當前RenderObject物件
    if (_relayoutBoundary != this) {
      markParentNeedsLayout();
    } else {
      _needsLayout = true;
      if (owner != null) {
        //標記當前RenderObject及其子RenderObject物件需要重新佈局
        //將當前`RenderObject`新增到集合中。
        owner._nodesNeedingLayout.add(this);
        owner.requestVisualUpdate();
      }
    }
  }
  @protected
  void markParentNeedsLayout() {
    _needsLayout = true;
    final RenderObject parent = this.parent;
    if (!_doingThisLayoutWithCallback) {
      //呼叫父類的markNeedsLayout函式
      parent.markNeedsLayout();
    } else {
      assert(parent._debugDoingThisLayout);
    }
    assert(parent == this.parent);
  }
複製程式碼

markNeedsLayout函式的程式碼實現很簡單,就是不斷遍歷父RenderObject物件,從而找到佈局邊界的RenderObject物件,並將該RenderObject物件新增到集合_nodesNeedingLayout中,然後在下一階段就從該物件開始佈局。

在這裡有個“佈局邊界”的概念,在Flutter中,可以給任意節點設定佈局邊界,即當邊界內的任何物件發生重新佈局時,不會影響邊界外的物件,反之亦然。

在重新構建build函式及標記RenderObject完成後,就進入下一階段,開始佈局。

3、layout階段

在該階段,會確定每個元件的大小及位置,相當於Android中的onMeasure+onLayout函式所實現的功能。如果是第一次呼叫該函式,該階段就會遍歷所有的元件,來確定其大小及位置;否則該階段就會遍歷佈局邊界內的所有元件,來確定其大小及位置。

當上一階段中的buildOwner.buildScope(renderViewElement)函式執行完畢後,就會呼叫RendererBindingdrawFrame函式,該函式實現非常簡潔。

  //繪製幀
  void drawFrame() {
    //對指定元件及其子元件進行大小測量及位置確定
    pipelineOwner.flushLayout();
    pipelineOwner.flushCompositingBits();
    pipelineOwner.flushPaint();
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
  }
複製程式碼

其中flushLayout就是進行元件的大小及位置確定,在該函式中會遍歷集合_nodesNeedingLayout並呼叫集合中每個物件的_layoutWithoutResize函式。

  void flushLayout() {
    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)) {
          //呼叫RenderObject物件的_layoutWithoutResize函式
          if (node._needsLayout && node.owner == this)
            node._layoutWithoutResize();
        }
      }
    } finally {...}
  }
複製程式碼

_layoutWithoutResize函式是私有的,所以不存在重寫的問題。那麼就直接來看該函式。

  void _layoutWithoutResize() {
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {...}
    _needsLayout = false;
    markNeedsPaint();
  }
複製程式碼

_layoutWithoutResize函式很簡單,就直接呼叫了performLayout函式。

由於performLayout是一個抽象函式,需要在子類重寫,但都會在該函式中呼叫layout函式,然後又在layout函式中呼叫performLayout函式。以此類推,從而確定更新UI部分的元件大小及位置,總體流程如下。

Flutter的繪製流程簡述
當然,RenderObject物件的size也不是隨便確定的,因為在呼叫RenderObjectlayout函式時,會傳遞一個繼承自Constraints的物件。該物件是一個佈局約束,由父傳給子,子會根據該物件來決定自己的大小。

3.1、標記RenderObject

當大小及位置確定後,就又會對RenderObject進行一次標記,這次跟上一階段的標記大同小異,但這次是標記可繪製的RenderObject物件,然後在後面對這些物件進行重新繪製。標記可繪製的RenderObject物件是通過markNeedsPaint函式來實現的,程式碼如下。

  void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      //標記需要重新繪製的RenderObject物件
      //需要繪製當前圖層
      if (owner != null) {
        owner._nodesNeedingPaint.add(this);
        owner.requestVisualUpdate();
      }
    } else if (parent is RenderObject) {
      //沒有自己的圖層,與父類共用同一圖層
      final RenderObject parent = this.parent;
      //遍歷其父RenderObject物件
      parent.markNeedsPaint();
    } else {
      //當是RenderView時,需要自己建立新的圖層
      if (owner != null)
        owner.requestVisualUpdate();
    }
  }
複製程式碼

markNeedsPaint函式中涉及到了一個“重繪邊界”的概念。在進入和走出重繪邊界時,Flutter會強制切換新的圖層,這樣就可以避免邊界內外的互相影響。當然重繪邊界也可以在任何節點手動設定,但是一般不需要我們來實現,Flutter提供的控制元件預設會在需要設定的地方自動設定。

4、compositingBits階段

在元件的大小及位置確定後,就會進入當前階段。該階段主要是做一件事,就是將RenderObject樹上新增及刪除的RenderObject物件標記為“髒”,方便在下一階段對這些RenderObject物件進行重繪。具體程式碼實現是在flushCompositingBits函式中,該函式在Layout階段後立即呼叫。

  void flushCompositingBits() {
    ...
    //將RenderObject物件按照深度進行排序
    _nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
    for (RenderObject node in _nodesNeedingCompositingBitsUpdate) {
      if (node._needsCompositingBitsUpdate && node.owner == this)
        //將RenderObject物件及其子物件標記為“髒”
        node._updateCompositingBits();
    }
    _nodesNeedingCompositingBitsUpdate.clear();
    ...
  }
複製程式碼

_nodesNeedingCompositingBitsUpdate是一個集合,只有RenderObject物件的_needsCompositing為true時,才會新增到該集合中。在RenderObject物件建立時,_needsCompositing的值會根據isRepaintBoundaryalwaysNeedsCompositing來共同判斷。

  RenderObject() {
    //isRepaintBoundary決定當前RenderObject是否與父RenderObject分開繪製,預設為false,其值在當前物件的生命週期內無法修改。也就是判斷當前物件是否是繪製邊界
    //alwaysNeedsCompositing為true表示當前RenderObject會一直重繪,如視訊播放,預設為false
    _needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
  }
複製程式碼

然後在向樹中新增或者刪除RenderObject物件時會呼叫adoptChilddropChild函式,而這兩個函式都會呼叫markNeedsCompositingBitsUpdate函式,也就在markNeedsCompositingBitsUpdate函式內完成了將當前物件新增到集合中的操作。

  //向樹中新增當前節點
  @override
  void adoptChild(RenderObject child) {
    setupParentData(child);
    markNeedsLayout();
    //將當前物件的_needsCompositingBitsUpdate值標為true
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
    super.adoptChild(child);
  }
  //從樹中移除當前節點
  @override
  void dropChild(RenderObject child) {
    child._cleanRelayoutBoundary();
    child.parentData.detach();
    child.parentData = null;
    super.dropChild(child);
    markNeedsLayout();
    //將當前物件的_needsCompositingBitsUpdate值標為true
    markNeedsCompositingBitsUpdate();
    markNeedsSemanticsUpdate();
  }
  //
  void markNeedsCompositingBitsUpdate() {
    if (_needsCompositingBitsUpdate)
      return;
    _needsCompositingBitsUpdate = true;
    if (parent is RenderObject) {
      final RenderObject parent = this.parent;
      if (parent._needsCompositingBitsUpdate)
        return;
      if (!isRepaintBoundary && !parent.isRepaintBoundary) {
        parent.markNeedsCompositingBitsUpdate();
        return;
      }
    }
    //將當前物件或者其父物件新增到_nodesNeedingCompositingBitsUpdate集合中
    if (owner != null)
      owner._nodesNeedingCompositingBitsUpdate.add(this);
  }
複製程式碼

這樣就會在呼叫flushCompositingBits函式時,就會呼叫_updateCompositingBits函式來判斷是否將這些物件及子物件標記為“髒”,然後在下一階段進行繪製。

  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;
  }
複製程式碼

5、繪製階段

經過前面的佈局及“髒”RenderObject物件的標記,現在就可以在圖層上進行UI的繪製。通過呼叫flushPaint函式就可以重繪已經標記的“髒”RenderObject物件及其子物件。

  void flushPaint() {
    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) {
          //當前物件是否與layer進行關聯
          if (node._layer.attached) {
            //在圖層上繪製UI
            PaintingContext.repaintCompositedChild(node);
          } else {
            //跳過UI繪製,但當前節點為“髒”的狀態不會改變
            node._skippedPaintingOnLayer();
          }
        }
      }
    } finally {}
  }
複製程式碼

flushPaint函式中,每次遍歷“髒”RenderObject物件時,都會進行一次排序,避免重複繪製。然後在判斷當前物件是否與Layer進行關聯,如果沒有關聯,則無法進行繪製,但不會清除“髒”標記。下面來看repaintCompositedChild函式的實現。

  static void repaintCompositedChild(RenderObject child, { bool
    _repaintCompositedChild(
      child,
      debugAlsoPaintedParent: debugAlsoPaintedParent,
    );
  }

  static void _repaintCompositedChild(
    RenderObject child, {
    bool debugAlsoPaintedParent = false,
    PaintingContext childContext,
  }) {
    //拿到Layer物件
    OffsetLayer childLayer = child._layer;
    if (childLayer == null) {
      //建立新的Layer物件
      child._layer = childLayer = OffsetLayer();
    } else {
      //移除Layer物件的後繼節點
      childLayer.removeAllChildren();
    }
    //建立context物件
    childContext ??= PaintingContext(child._layer, child.paintBounds);
    //呼叫paint函式開始繪製
    child._paintWithContext(childContext, Offset.zero);
    childContext.stopRecordingIfNeeded();
  }
複製程式碼

在該函式中主要是對Layer物件的處理,然後呼叫_paintWithContext函式,在_paintWithContext函式中就會呼叫paint這個函式,從而實現UI的繪製。至此,就完成了UI的繪製,下面再來看一個被忽略的物件——Layer

5.1、Layer

Layer是“圖層”意思,在Flutter中是最容易被忽略但又無比重要的一個類。它非常貼近底層,可以很容易的看到呼叫Native方法。

Layer跟其他三棵樹一樣,也是一棵樹,有“髒”狀態的標記、更新等操作。不同的是,Layer是一個雙連結串列結構,在每個Layer物件中都會指向其前置節點與後置節點(葉子Layer的後置節點為null)。

abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
  //返回父節點
  @override
  ContainerLayer get parent => super.parent;

  //當前節點狀態,為true表示當前節點是“髒”資料。需要重繪
  bool _needsAddToScene = true;

  //將當前節點標記為“髒”
  @protected
  @visibleForTesting
  void markNeedsAddToScene() {
    // Already marked. Short-circuit.
    if (_needsAddToScene) {
      return;
    }
    _needsAddToScene = true;
  }
  
  @protected
  bool get alwaysNeedsAddToScene => false;

  //這個是一個非常重要的東西,主要用於節點資料的快取。儲存當前節點的渲染資料,如果當前節點不需要更新,就直接拿儲存的資料使用。
  @protected
  ui.EngineLayer get engineLayer => _engineLayer;
  
  //更改當前節點的資料
  @protected
  set engineLayer(ui.EngineLayer value) {
    _engineLayer = value;
      if (parent != null && !parent.alwaysNeedsAddToScene) {
        //將父節點標記需要更新的狀態
        parent.markNeedsAddToScene();
      }
    }
  }
  ui.EngineLayer _engineLayer;

  //更新當前節點狀態,如果_needsAddToScene為true,則將當前節點標記為“髒”
  @protected
  @visibleForTesting
  void updateSubtreeNeedsAddToScene() {
    _needsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
  }

  //指向後置節點
  Layer get nextSibling => _nextSibling;
  Layer _nextSibling;

  //指向前置節點
  Layer get previousSibling => _previousSibling;
  Layer _previousSibling;
  
  //將子節點從Layer樹中移除
  @override
  void dropChild(AbstractNode child) {
    if (!alwaysNeedsAddToScene) {
      markNeedsAddToScene();
    }
    super.dropChild(child);
  }
  //將當前節點新增到Layer樹中
  @override
  void adoptChild(AbstractNode child) {
    if (!alwaysNeedsAddToScene) {
      markNeedsAddToScene();
    }
    super.adoptChild(child);
  }

  //將當前節點從Layer樹中移除
  @mustCallSuper
  void remove() {
    parent?._removeChild(this);
  }

  
  void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
  
  void _addToSceneWithRetainedRendering(ui.SceneBuilder builder) {
    //使用當前節點的快取的資料
    if (!_needsAddToScene && _engineLayer != null) {
      builder.addRetained(_engineLayer);
      return;
    }
    addToScene(builder);
    //將當前節點標記為“乾淨”的
    _needsAddToScene = false;
  }
}
複製程式碼

5.2、Layer節點的新增

previousSiblingnextSibling分別是Layer的前置節點與後置節點,當向Layer樹中新增Layer節點時,也會將當前Layer設定為父節點的後置節點,父節點設定為當前節點的前置節點。這樣,就形成了一顆樹。

class ContainerLayer extends Layer {
  ...
  //將當前節點及其連結串列上的所有子節點都加入到Layer樹中
  @override
  void attach(Object owner) {
    super.attach(owner);
    Layer child = firstChild;
    while (child != null) {
      child.attach(owner);
      child = child.nextSibling;
    }
  }
  
  //將當前節點及其連結串列上的所有子節點都從Layer樹中移除
  @override
  void detach() {
    super.detach();
    Layer child = firstChild;
    while (child != null) {
      child.detach();
      child = child.nextSibling;
    }
  }
  //將child新增到連結串列中
  void append(Layer child) {
    adoptChild(child);
    child._previousSibling = lastChild;
    if (lastChild != null)
      lastChild._nextSibling = child;
    _lastChild = child;
    _firstChild ??= child;
  }
  ...
}
複製程式碼

在上述的append函式中就將子節點新增到Layer樹並加入到雙連結串列中。在adoptChild函式中最終會呼叫attach函式,從而完成Layer樹的新增。

5.3、Layer的狀態更新

_needsAddToScene是對Layer狀態的標記,如果為true,則表示當前Layer需要重寫進行繪製,否則表示當前Layer是“乾淨”的,不需要重新繪製,只需要拿Layer上次的資料與其他Layer資料一起交給GPU處理即可。從而達到節省資源的目的。

class ContainerLayer extends Layer {
  ...
  //更新Layer節點的狀態。
  @override
  void updateSubtreeNeedsAddToScene() {
    super.updateSubtreeNeedsAddToScene();
    Layer child = firstChild;
    while (child != null) {
      child.updateSubtreeNeedsAddToScene();
      _needsAddToScene = _needsAddToScene || child._needsAddToScene;
      child = child.nextSibling;
    }
  }
  ...
}
複製程式碼

updateSubtreeNeedsAddToScene函式就是更新Layer的狀態,可以發現,在更新當前Layer的狀態時,也會更新其所有子Layer的狀態。

關於Layer的更多內容可以去閱讀Flutter Framework 原始碼解析( 2 )—— 圖層詳解這篇文章。

6、其他階段

6.1、compositing階段

該階段主要是將更新後的資料傳遞給GPU。這時候呼叫的是compositeFrame函式,該函式很簡單,就是呼叫了一個Native函式。

  void compositeFrame() {
    Timeline.startSync('Compositing', arguments: timelineWhitelistArguments);
    try {
      final ui.SceneBuilder builder = ui.SceneBuilder();
      final ui.Scene scene = layer.buildScene(builder);
      if (automaticSystemUiAdjustment)
        _updateSystemChrome();
      //更新後資料交給GPU處理
      _window.render(scene);
      scene.dispose();
    } finally {
      Timeline.finishSync();
    }
  }
複製程式碼

6.2、semantics階段

在向GPU傳送資料後,Flutter還會呼叫flushSemantics函式。該函式與系統的輔助功能相關,一般情況下是不做任何處理。

6.3、finalization階段

在該階段,主要是將Element物件從樹中移除及處理新增在_postFrameCallbacks集合中的回撥函式。由於該回撥函式是在繪製結束時呼叫,所以在該回撥函式中,context已經建立成功。

7、總結

可以發現,Flutter的UI繪製還是蠻複雜的,涉及到的東西也比較多,如動畫的處理、輔助功能、非同步任務等。但整體上還是通過WidgetElementRenderObject這三棵樹來操作layer樹實現的UI的繪製。 熟悉了這四棵樹,也就會對Flutter的UI繪製有一個清晰的認識。

相關文章