Flutter 必知必會系列 —— 從 SchedulerBinding 中看 Flutter 幀調度
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第2天,點擊查看活動詳情
前面我們介紹了 GestureBinding
,知道了 Flutter
的手勢處理流程,這一篇我們就從 SchedulerBinding
的代碼中看幀調度過程。
往期精彩:
Flutter 必知必會系列 —— mixin 和 BindingBase 的巧妙配合
Flutter 必知必會系列 —— 從 GestureBinding 中看 Flutter 手勢處理過程
Flutter 幀的階段
Flutter 把一幀分為了 6 個階段,每個階段做不同的事情,並且把幀階段包裝在枚舉類 SchedulerPhase
中,作為前期準備,我們認識一下這個類和這幾個階段是啥。
idle —— 空閒
這個階段沒有幀任務執行,在這個階段執行的代碼是:SchedulerBinding.scheduleTask
發佈的 TaskCallback
、scheduleMicrotask
註冊的微任務、Timer
聲明的任務、用户的手勢處理器、Future
和 Stream
的任務。
這裏大家需要先看一下 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 中,幀和隊列的執行和關聯如下:
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; }
``
就幹了兩件事:第一:確保
window的
onBeginFrame和
onDrawFrame回調已經設置了。
第二:調用
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'; ```
那麼 window
的 onBeginFrame
和 onDrawFrame
便是幀調度執行的內容了。
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
是一個 Map
,key
是幀的 id
,value
是這一幀在 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:請調度我吧,我的回調已經準備好了!分別是 onBeginFrame
和 onDrawFrame
。
總結
幀調度就説完啦,和我們平時寫的代碼一樣,把一個大任務分成幾個階段,每個階段對應一個回調數組,從開始到結束依次是:動畫、佈局、合成、繪製、收尾。