Flutter框架分析(四)-- Flutter框架的執行

ad6623發表於2019-03-15

Flutter框架分析分析系列文章:

《Flutter框架分析(一)-- 總覽和Window》

《Flutter框架分析(二)-- 初始化》

《Flutter框架分析(三)-- Widget,Element和RenderObject》

《Flutter框架分析(四)-- Flutter框架的執行》

《Flutter框架分析(五)-- 動畫》

《Flutter框架分析(六)-- 佈局》

前言

前面幾篇文章介紹了Flutter框架的渲染流水線,window,初始化以及WidgetElementRenderObject體系。其中對WidgetElementRenderObject的介紹主要是一些靜態的說明,瞭解了以上這些技術點之後,在這篇文章裡我們會通過動態執行的方式來介紹一下Flutter框架是如何執行的。 從之前介紹的渲染流水線可以知道,這個過程大致可以分為兩段操作。第一段是從State.setState()到去engine那裡請求一幀,第二段就是Vsync訊號到來以後渲染流水線開始重建新的一幀最後送入engine去顯示。我們先來看第一段Flutter框架都做了什麼。

排程之前

先看一下State.setState()

void setState(VoidCallback fn) {
    
    final dynamic result = fn() as dynamic;
  
    _element.markNeedsBuild();
  }
複製程式碼

這裡會呼叫到ElementmarkNeedsBuild()函式。

void markNeedsBuild() {
    if (!_active)
      return;
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }
複製程式碼

Element首先看自己是不是active的狀態,不是的話就直接返回了,如果是“髒”(dirty)的狀態也直接返回,不是的話會置上這個狀態然後呼叫BuildOwnerscheduleBuildFor()函式,這個BuildOwner我們之前介紹過,它的例項是在WidgetsBinding初始化的時候構建的。每個Element的都會持有BuildOwner的引用。由其父Elementmount的時候設定。

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

BuildOwner會維護一個_dirtyElements列表,所有被標記為“髒”(dirty)的element都會被新增進去。在此之前會呼叫onBuildScheduled()。這個函式是WidgetsBinding初始化的時候設定給BuildOwner的,對應的是WidgetsBinding._handleBuildScheduled()

void _handleBuildScheduled() {
    ensureVisualUpdate();
  }
複製程式碼

這裡會呼叫到ensureVisualUpdate()。這個函式定義在SchedulerBinding裡的

void ensureVisualUpdate() {
    switch (schedulerPhase) {
      case SchedulerPhase.idle:
      case SchedulerPhase.postFrameCallbacks:
        scheduleFrame();
        return;
      case SchedulerPhase.transientCallbacks:
      case SchedulerPhase.midFrameMicrotasks:
      case SchedulerPhase.persistentCallbacks:
        return;
    }
  }
複製程式碼

函式ensureVisualUpdate()會判斷當前排程所處的狀態,如果是在idle(空閒)或者postFrameCallbacks執行狀態則呼叫scheduleFrame()。其他狀態則直接返回。下面這三個狀態正是渲染流水線執行的時候。

void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }
複製程式碼

在函式scheduleFrame()裡我們看到了熟悉的window。這裡就是通知engine去排程一幀的地方了。排程之後會置上_hasScheduledFrame標誌位,避免重複請求。另外一個標誌位_framesEnabled是代表當前app的狀態,或者說其所處的生命週期是否允許重新整理介面。這個狀態有四種:resumedinactivepausedsuspending

  • resumed:app可見且可以響應使用者輸入。
  • inactive:app不能響應使用者輸入,例如在Android上彈出系統對話方塊。
  • paused:app對使用者不可見。
  • suspending:app掛起??這個狀態貌似Android和iOS都沒有上報。

_framesEnabled只有在resumedinactive狀態下才為true。也就是說,只有在這兩個狀態下Flutter框架才會重新整理頁面。

至此第一階段,也就是排程之前的工作做完了。看起來比較簡單,主要就是把需要重建的Element放入_dirtyElements列表。接下來Flutter框架會等待Vsync訊號到來以後engine回撥框架,這就是第二段要做的事情了。

Vsync到來之後

我們之前說過Vsync訊號到來之後,engin會按順序回撥window的兩個回撥函式:onBeginFrame()onDrawFrame()。這兩個回撥是SchedulerBinding初始化的時候設定給window的。對應的是SchedulerBinding.handleBeginFrame()SchedulerBinding.handleDrawFrame()

onBeginFrame

這個回撥會直接走到SchedulerBinding.handleBeginFrame()

  void handleBeginFrame(Duration rawTimeStamp) {
   ...
    _hasScheduledFrame = false;
    try {
      // TRANSIENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.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 {
      _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
  }
複製程式碼

這個函式主要是在依次回撥“Transient”回撥函式,這些回撥函式是在排程之前設定在SchedulerBinding裡的,這裡的“Transient”意思是臨時的,或者說是一次性的。原因是這些回撥函式只會被呼叫一次。注意看程式碼裡_transientCallbacks被置為空Map了。如果想在下一幀再次呼叫的話需要提前重新設定回撥。這些回撥主要和動畫有關係。也就是渲染流水線裡的第一階段,動畫(Animate)階段。關於動畫後續我會再寫文章從框架角度分析一下動畫的機制。

在執行回撥之前_schedulerPhase的狀態被設定為SchedulerPhase.transientCallbacks。回撥處理完以後狀態更新至SchedulerPhase.midFrameMicrotasks意思是接下來會處理微任務佇列。處理完微任務以後,engine會接著回撥onDrawFrame()

onDrawFrame

這個回撥會直接走到SchedulerBinding.handleDrawFrame()

void handleDrawFrame() {
    try {
      // PERSISTENT FRAME CALLBACKS
      _schedulerPhase = SchedulerPhase.persistentCallbacks;
      for (FrameCallback callback in _persistentCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);

      // POST-FRAME CALLBACKS
      _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;
      _currentFrameTimeStamp = null;
    }
  }
複製程式碼

handleDrawFrame裡按順序處理了兩類回撥,一類叫“Persistent”回撥,另一類叫“Post-Frame”回撥。

“Persistent”字面意思是永久的。這類回撥一旦註冊以後是不能取消的。主要用來驅動渲染流水線。渲染流水線的構建(build),佈局(layout)和繪製(paint)階段都是在其中一個回撥裡的。

“Post-Frame”回撥主要是在新幀渲染完成以後的一類呼叫,此類回撥只會被呼叫一次。

在執行“Persistent”回撥之前_schedulerPhase狀態變為SchedulerPhase.persistentCallbacks。在執行“Post-Frame”回撥之前_schedulerPhase狀態變為SchedulerPhase.postFrameCallbacks。最終狀態變為SchedulerPhase.idle

這裡我們主要關注一個“Persistent”回撥:WidgetsBinding.drawFrame()。這個函式是在RendererBinding初始化的時候加入到“Persistent”回撥的。

void drawFrame() {
   try {
    if (renderViewElement != null)
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    buildOwner.finalizeTree();
  } finally {
     ...
  }
}
複製程式碼

這裡首先會呼叫buildOwner.buildScope(renderViewElement)。其入參renderViewElement是element tree的根節點。此時渲染流水線就進入了構建(build)階段。接下來呼叫了super.drawFrame()。這個函式定義在RendererBinding中。

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

可以看出渲染流水線的接力棒傳到了pipelineOwner的手裡,渲染流水線就進入了佈局(layout)階段和繪製(paint)階段。關於最後這兩個階段本篇不做詳細介紹。這裡大家只要知道繪製完成以後Flutter框架最終會呼叫window.render(scene)將新幀的資料送入engine顯示到螢幕。

最後呼叫buildOwner.finalizeTree();。這個函式的作用是清理不再需要的Element節點。在element tree更新以後可能有些節點就不再需要掛載在樹上了,在finalizeTree()的時候會將這些節點及其子節點unmount。

構建(build)階段

void buildScope(Element context, [VoidCallback callback]) {
    try {
      _scheduledFlushDirtyElements = true;
      _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;
      }
      
    } finally {
      for (Element element in _dirtyElements) {
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
     
    }
  }
複製程式碼

還記得在排程幀之前會把需要更新的Element標記為“髒”(dirty)並放入BuildOwner_dirtyElements列表。這裡Flutter會先按照深度給這個列表排個序。因為Element在重建的時候其子節點也都會重建,這樣如果父節點和子節點都為“髒”的話,先重建父節點就避免了子節點的重複重建。

排完序就是遍歷_dirtyElements列表。依次呼叫Element.rebuild()。這個函式又會呼叫到Element.performRebuild()。我們之前介紹Element的時候說過performRebuild()由其子類實現。

我們之前的出發點是State.setState()。那就先看看StatefulElement如何做的。它的performRebuild()是在其父類ComponentElement裡:

void performRebuild() {
    Widget built;
    built = build();
    try {
      _child = updateChild(_child, built, slot);
    } catch (e, stack) {
      ...
    }
  }
複製程式碼

回憶一下ComponentElement。這個build()函式最終會呼叫到State.build()了。返回的就是我們自己例項化的Widget。拿到這個新Widget就去呼叫updateChild()。之前在講Element的時候我們介紹過updateChild()這個函式。由增,刪,改這麼幾種情況,對於MyWidget,從State.setState()過來是屬於改的情況。此時會呼叫child.update(newWidget);。這個update()函式又是由各個Element子類實現的。這裡我們只列舉幾個比較典型的。

StatefulElementStatelessElementupdate()函式最終都會呼叫基類Elementrebuild()函式。好像在兜圈圈的感覺。。。

RenderObjectElementupdate()函式就比較簡單了

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

更新只是呼叫了一下RenderObjectWidget.updateRenderObject()。這個函式我們之前介紹過,只是把新的配置設定到現有的RenderObject上。

回到上面那個兜圈圈的問題。理清這裡的呼叫關係的關鍵就是要搞清楚是此時的Element是在對自己進行操作還是對孩子進行操作。假設我們有這樣的一個三層element tree進行更新重建。

  父(StatefulElement)

  子(StatefulElement)

  孫(LeafRenderObjectElement)
複製程式碼

那麼從父節點開始,呼叫順序如下:

父.rebuild()--->父.performRebuild()--->父.updateChild()--->子.update()--->子.rebuild()--->子.performRebuild()--->子.updateChild()--->孫.update()。

可見構建(build)過程是從需要重建的Element節點開始一層層向下逐個更新子節點。直到遇到葉子節點為止。

至此渲染流水線的構建(build)階段就跑完了。接下來就由pipelineOwner驅動開始佈局(layout)和繪製(paint)階段了。這兩個階段留待以後再給大家介紹一下。

總結

本篇文章從我們熟悉的State.setState()函式出發,大致介紹了Flutter框架是如何執行渲染流水線的。總體來說其執行時分為兩個階段,向engine排程幀之前和Vsync訊號到來engine回撥Flutter框架之後。剩餘篇幅則是以更新Element為例介紹了一下渲染流水線的構建(build)階段都做了一些什麼事情。限於篇幅,沒有更多涉及Element的新增和刪除步驟。大家感興趣的話可以直接看原始碼來了解相關資訊。

相關文章