Flutter 必知必會系列 —— 從 SchedulerBinding 中看 Flutter 幀調度

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第2天,點擊查看活動詳情

前面我們介紹了 GestureBinding,知道了 Flutter 的手勢處理流程,這一篇我們就從 SchedulerBinding 的代碼中看幀調度過程。

往期精彩:

Flutter 必知必會系列 —— runApp 做了啥

Flutter 必知必會系列 —— mixin 和 BindingBase 的巧妙配合

Flutter 必知必會系列 —— 從 GestureBinding 中看 Flutter 手勢處理過程

Flutter 幀的階段

Flutter 把一幀分為了 6 個階段,每個階段做不同的事情,並且把幀階段包裝在枚舉類 SchedulerPhase 中,作為前期準備,我們認識一下這個類和這幾個階段是啥。

idle —— 空閒

這個階段沒有幀任務執行,在這個階段執行的代碼是:SchedulerBinding.scheduleTask 發佈的 TaskCallbackscheduleMicrotask 註冊的微任務、Timer 聲明的任務、用户的手勢處理器、FutureStream 的任務。

這裏大家需要先看一下 Dart 的任務執行順序,這個鏈接需要翻牆哦~,瞭解一下 Dart 是如何處理事件隊列、微任務隊列的,以便我們寫出更好的異步代碼。

transientCallbacks —— 瞬時任務階段

這個階段執行 SchedulerBinding.scheduleFrameCallback 添加的回調,一般情況下,這些回調執行動畫有關的計算,我們在前面的動畫介紹中,講過這個地方,👉 Flutter 動畫是這麼動起來的

midFrameMicrotasks —— 幀中微任務處理階段

transientCallbacks 也會產生一些微任務,這些微任務會在這個階段執行。

persistentCallbacks —— 持續任務處理階段

這個階段主要處理 SchedulerBinding.addPersistentFrameCallback 添加的回調,與 transientCallbacks 階段相對應,這個階段處理 build/layout/paint

postFrameCallbacks —— 幀結束階段

這個階段執行 SchedulerBinding.addPostFrameCallback 添加的回調,一般情況下會做一些幀清理工作和發起下一幀。

比如我們舉個例子:

void update(TextEditingValue newValue) { if (_value == newValue) return; _value = newValue; if (SchedulerBinding.instance!.schedulerPhase == SchedulerPhase.persistentCallbacks) { //第一處 SchedulerBinding.instance!.addPostFrameCallback(_markNeedsBuild); } else { _markNeedsBuild(); } } 這一段代碼是 EditText 的代碼,在更新顯示字段的時候,看第一處的顯示邏輯。如果當前是繪製階段,那就在本幀的結尾增加一個發起重新 build 的任務,否則就直接發起重新 build 任務。

小結

幀的調度分為六個階段,代碼體現在 SchedulerPhase 中,幀和隊列的執行和關聯如下:

image.png

Flutter 發起幀調度

幀調度的方式

Flutter 中發起幀調度的方法就下面幾個:

| 方法名 | 作用 | | --- | --- | | scheduleWarmUpFrame | 調度一幀,並且該幀儘可能快的執行,不需要等待引擎的 "Vsync" 信號 | | scheduleFrame | 調度一幀 | | scheduleForcedFrame | 強行調度一幀,即使是在熄屏的情況下也會執行| | scheduleFrameCallback | 調度一幀,並且給這一幀設置一個執行回調,回調會在 transient 階段執行

這幾個方法大差不差,我們以核心的 scheduleFrame 的為例,看看幹了啥事。

```dart void scheduleFrame() { if (_hasScheduledFrame || !framesEnabled) return; ensureFrameCallbacksRegistered();//第一處 window.scheduleFrame(); _hasScheduledFrame = true; }

void ensureFrameCallbacksRegistered() { window.onBeginFrame ??= _handleBeginFrame; window.onDrawFrame ??= _handleDrawFrame; }

`` 就幹了兩件事:第一:確保windowonBeginFrameonDrawFrame回調已經設置了。 第二:調用window` 的發起幀流程。

之前我們提到過,Flutter 和 Native 的交互都是通過回調的方式,window 的發起流程最終會調用到 void scheduleFrame() native 'PlatformConfiguration_scheduleFrame'; 這是一個 native 方法,相當於 Flutter 吿訴原生,原生請開始繪製吧,我已經準備好了。然後與原生在收到 "Vsync" 信號之後,會調用到第一處註冊的兩個回調 onBeginFrame 和 onDrawFrame。

我們看:

```dart @pragma('vm:entry-point') void _beginFrame(int microseconds, int frameNumber) { PlatformDispatcher.instance._beginFrame(microseconds); PlatformDispatcher.instance._updateFrameData(frameNumber); }

@pragma('vm:entry-point') void _drawFrame() { PlatformDispatcher.instance._drawFrame(); }


void scheduleFrame() native 'PlatformConfiguration_scheduleFrame'; ```

那麼 windowonBeginFrameonDrawFrame 便是幀調度執行的內容了。

onBeginFrame 開始響應

我們來看 Flutter 是怎麼響應的。

dart /// ...代碼省略 void handleBeginFrame(Duration? rawTimeStamp) { _hasScheduledFrame = false; try { _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; // 第三處 } } 第一處和第三處是修改調度的階段,先設置為 transientCallbacks 階段,並且在這個階段,執行了 _transientCallbacks 中的回調。

_transientCallbacks 中的回調是啥呢? 就是 scheduleFrameCallback 方法參數中添加的。

dart int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) { scheduleFrame(); _nextFrameCallbackId += 1; _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling); return _nextFrameCallbackId; } \_transientCallbacks 是一個 Mapkey 是幀的 idvalue 是這一幀在 transient 階段應該執行的回調數組。 在使用 scheduleFrameCallback 方法發起幀任務的時候,需要傳遞一個 callback 回調,這個回調就是發起幀的下一幀的 transient 階段執行。

那麼誰調用了 scheduleFrameCallback 這個方法呢? 就是動畫!所以動畫的計算先與佈局繪製等。具體可以看這裏👉 Flutter 動畫是這麼動起來的

不管回調會不會異常,都會執行到第三處,第三處就是幀調度進入了 midFrameMicrotasks 階段。

上面就是 \_handleBeginFrame,會執行本幀的 \_transientCallbacks 回調,因為動畫發起的時候會設置這個回調,所以基本就是動畫的計算。

因為動畫會是 Future 等的計算,所以在 midFrameMicrotasks 階段,這些異步的計算依然會執行。 下面我們看 _handleDrawFrame 的執行。

onDrawFrame 繪製任務

```dart /// 代碼省略 void handleDrawFrame() { try { // PERSISTENT FRAME CALLBACKS _schedulerPhase = SchedulerPhase.persistentCallbacks; //第一處 for (final FrameCallback callback in _persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!);

// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks; //第二處
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
  _invokeFrameCallback(callback, _currentFrameTimeStamp!);

} finally { _schedulerPhase = SchedulerPhase.idle; _currentFrameTimeStamp = null; } } `` 上面的代碼還是比較清晰的,就是 **修改狀態、執行回調`**。我們仔細看。

首先看第一處的代碼,就是從 midFrameMicrotasks 階段進入到 persistentCallbacks。 然後執行 _persistentCallbacks 回調,_persistentCallbacks 回調是誰呢?

_persistentCallbacks 是通過 addPersistentFrameCallback 添加的。 void addPersistentFrameCallback(FrameCallback callback) { _persistentCallbacks.add(callback); } 那麼誰調用了這個方法呢?就是 RendererBinding 的初始化中。

```dart /// 代碼省略 mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable { @override void initInstances() { super.initInstances(); addPersistentFrameCallback(_handlePersistentFrameCallback);//第一處 }

void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); }

void drawFrame() { pipelineOwner.flushLayout(); pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); }

} ``` 就是第一處的代碼, RendererBinding 初始化時,添加了全局的幀 persistent 回調。 回調的任務就是佈局(Layout)、合成(CompositingBit)、繪製(Paint)

所以説,在 persistentCallbacks 階段,執行的任務就是佈局、合成、繪製。

我們繼續看,persistentCallbacks 的回調執行完了之後,就會到 postFrameCallbacks 階段,執行 _postFrameCallbacks 回調。postFrameCallbacks 階段是幀的末尾階段,大家可以使用這個方法來做一些收尾的工作。比如獲取尺寸等等。和 persistentCallbacks 不同,_postFrameCallbacks 是一次性的。

```dart void handleDrawFrame() {

try { // PERSISTENT FRAME CALLBACKS _schedulerPhase = SchedulerPhase.persistentCallbacks; for (final FrameCallback callback in _persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!); //第一處

_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
    List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear(); //第二處

for (final FrameCallback callback in localPostFrameCallbacks)
  _invokeFrameCallback(callback, _currentFrameTimeStamp!);

} finally { _schedulerPhase = SchedulerPhase.idle; _currentFrameTimeStamp = null; } } `` 第一處並沒清空_persistentCallbacks回調,第二處在執行完了之後,會清空_postFrameCallbacks` 回調。

所以大家通過 addPostFrameCallback 添加的回調只會執行一次。

小結

現在我們知道了發起幀調度就是通知 Native:請調度我吧,我的回調已經準備好了!分別是 onBeginFrameonDrawFrame

總結

幀調度就説完啦,和我們平時寫的代碼一樣,把一個大任務分成幾個階段,每個階段對應一個回調數組,從開始到結束依次是:動畫、佈局、合成、繪製、收尾

image.png