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