Flutter中的佈局繪製流程簡析(一)

開始

Flutter對比前端流行的框架,除了構建控件樹和控件狀態管理等,還多了佈局和繪製的流程,佈局和繪製以往都是前端開發可望而不可及的都被封鎖在瀏覽器渲染引擎的實現裏面,而咱們只能經過文檔或者作一些demo去深刻,就像盲人摸象,不少時候都是隻知其一不知其二。相對而言,Flutter把這個黑盒打開了,意味着咱們能夠作更加深刻的優化,開發效率也能成倍提升。
接下來就去深刻去了解,儘量把這個過程完整展示給你們。前端

入口

界面的佈局和繪製在每一幀都在發生着,甚至界面沒有變化,它也會存在;能夠想象每一幀裏面,引擎都像流水線的同樣重複着幾個過程:build(構建控件樹),layout(佈局), paint(繪製)和 composite(合成),周而復始。那麼驅動整個流水線的入口在哪裏呢?
直接來到WidgetBinding.drawFrame方法:node

void drawFrame() {
    ...
    try {
      if (renderViewElement != null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();
      buildOwner.finalizeTree();
    } finally {
     ...
    }
    ...
  }

這裏renderViewElement就是Root了,在第一幀的時候,控件樹尚未構建,固然也不存在renderViewElement了;而接下來buildOwner這個對象是幹嗎的呢?算法

BuilderOwner

先看一下從哪裏開始會用到builderOwner的方法:
clipboard.png瀏覽器

能夠看到咱們常用setState方法就與BuilderOwner緊密關聯了,接着再看BuilderOwner.scheduleBuildFor方法:框架

void scheduleBuildFor(Element element) {
    ...
    if (element._inDirtyList) {
      ...
      _dirtyElementsNeedsResorting = true;
      return;
    }
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled();
    }
    _dirtyElements.add(element);
    element._inDirtyList = true;
   ...
  }

這裏的處理過程:若是_scheduledFlushDirtyElements不爲true,就調起onBuildScheduled方法,並把Elment都加入到_dirtyElements中,那麼onBuildScheduled又會幹些啥尼?
回到WidgetBinding.initInstances方法:dom

void initInstances() {
    super.initInstances();
    ...
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    ...
  }

看到真實調用的是WidgetBinding._handleBuildScheduled方法,咱們繼續完善剛纔的調用過程:
clipboard.png
因此這裏就能夠看到咱們調用setState方法最終會觸發界面新的一幀繪製。ide

當觸發新的一幀時,咱們又回到最初的WidgetBinding.drawFrame方法中,那麼builderOwner.buildScope方法究竟會幹些工做:佈局

void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    ..l
    Timeline.startSync('Build', arguments: timelineWhitelistArguments);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {
         ...
        }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        ...
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          ...
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            index -= 1;
          }
        }
      }
        ...
        return true;
      }());
    } finally {
      for (Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
    }
  }

首先把_scheduledFlushDirtyElements標記設爲true,表示正在重新構建新的控件樹,而後_dirtyElements會作一輪排序,看一下Element._sort的方法如何實現的:性能

static int _sort(Element a, Element b) {
    if (a.depth < b.depth)
      return -1;
    if (b.depth < a.depth)
      return 1;
    if (b.dirty && !a.dirty)
      return -1;
    if (a.dirty && !b.dirty)
      return 1;
    return 0;
  }

嗯,由於在這裏最初排序都是標記爲dirty的Element,因此最後的結果是,depth小的Element會排最前,depth大的排最後;也就是說父Element會比子Element更早被rebuild,這樣能夠防止子Element會重複rebuild。
當在rebuild過程當中有可能會加入新的Dirty Element,因此每次rebuild的時候都會從新檢查_dirtyElements是否有增長或者檢查_dirtyElementsNeedsResorting標記位,接着重新排序一遍,這個時候咱們的_dirtyElements列表中就有可能存在以前已經rebuild完,dirty爲false的Element了,從新排序後,depth小的和dirty不爲true的會排最前,從新把index定位到第一個Dirty Element繼續rebuild。
若是在這個過程咱們想把已經rebuild過一次的Element想重複加入到_dirtyElements中,造成死循環,會怎樣的尼,這個時候Element._inDirtyList仍是爲true,代表Element已經在_dirtyElements列表中,在開發模式下引擎會報錯,給出相應提示;通常狀況下是不該該出現的,萬一出現就須要思考一下代碼是否合理了。優化

接着先跳過super.drawFrame方法,來到builderOwner.finalizeTree方法:

void finalizeTree() {
    Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments);
    try {
      lockState(() {
        _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
      });
     ...
    } catch (e, stack) {
      _debugReportException('while finalizing the widget tree', e, stack);
    } finally {
      Timeline.finishSync();
    }
  }

主要把_inactiveElements都進行一次清理,因此使用GlobalKey的控件,若是想起到重用控件的效果,必須在同一幀裏面完成「借用」,不然就會被清理了。

簡單總結一下BuilderOwner的功能就是:管理控件rebuild過程,讓控件有序的進行rebuild。

PipelineOwner

終於來到super.drawFrame方法,這個方法實際上調起的是RenderBinding.drawFrame方法:

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.
  }

咱們又見到一個跟BuilderOwner名稱很類似的PipelineOwner,那PipelineOwner又起到什麼樣的功能尼?直接深刻
pipelineOwner.flushLayout方法:

void flushLayout() {
    Timeline.startSync('Layout', arguments: timelineWhitelistArguments);
    _debugDoingLayout = true;
    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 {
      _debugDoingLayout = false;
      Timeline.finishSync();
    }
  }

跟builderOwner處理類似,先進行一次排序,depth小的排最前優先處理,而後調起RenderObject._layoutWithoutResize方法。

暫時先整理一下,這個時候咱們出現三個名詞:Widget,Element,RenderObject;它們的關係到底是咋樣的尼,假設你熟悉前端的Vue或者React框架,它們的關係等同於下面這張圖:

clipboard.png

也就是說RenderObject負責着界面的佈局繪製和事件處理等;而Element則是進行virtual dom diff,而且負責建立RenderObject;Widget則是咱們控件業務邏輯組織的地方, 負責建立Element。

大概能夠想到PipelineOwner的主要功能:負責管理那些dirty render object,讓它們進行佈局和繪製。

接着RenderObject._layoutWithoutResize方法:

void _layoutWithoutResize() {
    ...
    try {
      performLayout();
      markNeedsSemanticsUpdate();
    } catch (e, stack) {
      _debugReportException('performLayout', e, stack);
    }
    ...
    _needsLayout = false;
    markNeedsPaint();
  }

能夠看到其實直接調用了RenderObject.performLayout方法,而這個方法則是應由開發者本身實現的佈局邏輯,接着會調起RenderObject.markNeedsPaint方法,也就是說每次從新layout都會觸發一次paint。

void markNeedsPaint() {
    if (_needsPaint)
      return;
    _needsPaint = true;
    if (isRepaintBoundary) {
      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();
    }
  }

這裏的邏輯,主要判斷當前的RenderObject.isRepaintBoundary是否爲true,若是是則把當前RenderObject加入到PipelineOwner對應的列表中等待接下來的flushPaint處理,並觸發下一幀的繪製;當isRepaintBoundary不爲true的時候,則會一直往上查找直到找到isRepaintBoundary爲true的RenderObject,也就是有可能會找到根節點RenderView,而後加入到_nodesNeedingPaint列表中:

class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox> {
    ...
    bool get isRepaintBoundary => true;
    ...
}

這樣的話咱們就得注意了,若是常常須要重繪區域,最好把isRepaintBoundary標記true,這樣就儘可能避免觸發全局重繪,提升性能,對應的flutter就已經提供了一個RepaintBoundary控件,自動把isRepaintBoundary標記爲true,很是方便咱們去作優化。

既然有markNeedsPaint方法,固然也有markNeedsLayout方法:

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

處理邏輯基本上跟markNeedsPaint差很少,_relayoutBoundary也能夠減小全局從新佈局,能夠把佈局範圍縮小,提升性能,可是_relayoutBoundary的設置是有點不同的,等會再去討論。

簡單整理一下

當咱們用調起setState改變某些狀態,例如:控件的高度;先回到BuilderOwner.buildScope,繼續dirty element的rebuild方法:

void rebuild() {
    if (!_active || !_dirty)
      return;
    performRebuild();
  }

接着執行performRebuild方法:

void performRebuild() {
    Widget built;
    try {
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      _debugReportException('building $this', e, stack);
      built = new ErrorWidget(e);
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      _debugReportException('building $this', e, stack);
      built = new ErrorWidget(e);
      _child = updateChild(null, built, slot);
    }
  }

控件會從新build出子控件樹,而後調起updateChild方法:

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    if (child != null) {
      if (child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        return child;
      }
      if (Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        return child;
      }
      deactivateChild(child);
    }
    return inflateWidget(newWidget, newSlot);
  }
  1. 若是newWidget爲null可是child不爲null,也就是刪除原來的控件,就會調起deactivateChild方法,會把當前的Element加入到BuilderOwner._inactiveElements列表中(最後可能會被清除也可能會被重用)。

  2. 若是newWidget和child都不爲null,也就是更新原來的控件,先調起Widget.canUpdate方法判斷是否可以更新(通常都是根據Widget運行時類型是否相同來判斷),若是相同調起update方法,繼續更新的邏輯,若是不同,就要deactivate原來的控件,而且建立新的控件。

  3. 若是child爲null而Widegt不爲null,也就是要建立新的控件。

接下來會分別分析更新的邏輯和建立的邏輯:

  • 更新

直接來到StatefulElement.update方法:

void update(StatefulWidget newWidget) {
    super.update(newWidget);
    final StatefulWidget oldWidget = _state._widget;
    _dirty = true;
    _state._widget = widget;
    try {
      _state.didUpdateWidget(oldWidget);
    } finally {
    }
    rebuild();
  }

這裏首先會調起一個控件很重要的生命回調didUpdateWidget,綜合上述能夠知道,這裏是當新的子控件和舊的子控件類型一致時纔會調起;接着就是子控件的rebuild過程,而後不停重複下去。

  • 建立

直接來到Element.inflateWidget方法:

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot)
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  }

這裏判斷key是否爲GlobalKey,若是是會調起_retakeInactiveElement方法,目的是從Globalkey上重用控件,並把控件從BuilderOwner._inactiveElements列表上移除,防止它被unmount,接着就是重新跑一次updateChild流程;若是不是就在新的子控件上建立新的Element,而且mount上去。

可是若是多個child的時候是怎麼更新的尼?
來到MultiChildRenderObjectElement.update方法:

void update(MultiChildRenderObjectWidget newWidget) {
    super.update(newWidget);
    _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
    _forgottenChildren.clear();
  }

框架裏面好像只規定跟RenderObject相關的控件才能夠支持多個child,而updateChildren就是一個flutter版本的virtual dom diff算法的實現。

剛纔假設咱們須要修改控件的高度,既然跟顯示有關,必然跟RenderObejct相關,直接來到RenderObjectElement.update方法:

void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }

最後調起的是RenderObjectWidget.updateRenderObject方法,在這裏咱們能夠獲得新建立的RenderObject,咱們在這裏把新的RenderObject的屬性賦值給舊的RenderObject,而在RenderObject相關屬性的setter方法中會調起markNeedsLayout方法,這樣在下一幀佈局繪製的時候就會生效。

相關文章
相關標籤/搜索