Flutter - setState更新機制

基于Flutter 1.22.3的源碼刨析,分析Flutter的StatefulWidget的UI更新機制,相關源碼:

  widgets/framework.dart
  widgets/binding.dart
  scheduler/binding.dart
  lib/ui/window.dart
  flutter/runtime/runtime_controller.cc

一、概述

對于Flutter來說,萬物皆為Widget,常見的Widget子類為StatelessWidget(無狀態)和StatefulWidget(有狀態);

  • StatelessWidget:內部沒有保存狀態,界面創建后不會發生改變;
  • StatefulWidget:內部有保存狀態,當狀態發生改變,調用setState()方法會觸發StatefulWidget的UI發生更新,對于自定義繼承自StatefulWidget的子類,必須要重寫createState()方法。

接下來看看setState()究竟干了哪些操作。

二、Widget更新流程

2.1 setState

[-> framework.dart::state]

abstract class State<T extends StatefulWidget> with Diagnosticable {
StatefulElement _element;
  /// It is an error to call this method after the framework calls [dispose].
  /// You can determine whether it is legal to call this method by checking
  /// whether the [mounted] property is true.
  @protected
  void setState(VoidCallback fn) {
   ...
    _element.markNeedsBuild();
  }
}

這里需要注意setState()方法要在dispose()方法調用前調用,可以通過mounted屬性值來判斷父Widget是否還包含該Widget.

2.2 markNeedsBuild

[->framework.dart::Element]

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// Marks the element as dirty and adds it to the global list of widgets to
  /// rebuild in the next frame.
  ///標記元素為臟元素,然后添加到list集合里 等下一幀刷新
    void markNeedsBuild() {
      if (!_active)
        return;
      if (dirty)
        return;
      _dirty = true;
      owner.scheduleBuildFor(this);
    }
}

設置 Element的 _dirty 為 true

2.3 scheduleBuildFor

[->framework.dart::BuildOwner]

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// Adds an element to the dirty elements list so that it will be rebuilt
  /// when [WidgetsBinding.drawFrame] calls [buildScope].
 void scheduleBuildFor(Element element) {  
  ...
   if (element._inDirtyList) { //是否在集合里面
        _dirtyElementsNeedsResorting = true;
        return;
      }
      if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
        _scheduledFlushDirtyElements = true;
        onBuildScheduled();
      }
      _dirtyElements.add(element);//記錄所有臟元素
      element._inDirtyList = true;
  }
}

將element添加到臟element集合,之后會被重建

2.4 _handleBuildScheduled

[-> binding.dart:: WidgetsBinding]


/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    // Initialization of [_buildOwner] has to be done after
    // [super.initInstances] is called, as it requires [ServicesBinding] to
    // properly setup the [defaultBinaryMessenger] instance.
    _buildOwner = BuildOwner();
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    window.onLocaleChanged = handleLocaleChanged;
    window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
    SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
    FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
  }

  void _handleBuildScheduled() {
    ensureVisualUpdate();
 }

在Flutter應用啟動過程初始化WidgetsBinding時,賦值onBuildScheduled等于_handleBuildScheduled()。

2.5 ensureVisualUpdate

[ -> binding.dart::SchedulerBinding]

mixin SchedulerBinding on BindingBase {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
  }

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

schedulerPhase的初始值為SchedulerPhase.idle。SchedulerPhase是一個enum枚舉類型,有以下5個可取值:

狀態 含義
idle 沒有正在處理的幀,可能正在執行的是WidgetsBinding.scheduleTask,scheduleMicrotask,Timer,事件handlers,或者其他回調等
transientCallbacks SchedulerBinding.handleBeginFrame過程, 處理動畫狀態更新
midFrameMicrotasks 處理transientCallbacks階段觸發的微任務(Microtasks)
persistentCallbacks WidgetsBinding.drawFrame和SchedulerBinding.handleDrawFrame過程,build/layout/paint流水線工作
postFrameCallbacks 主要是清理和計劃執行下一幀的工作

2.6 scheduleFrame

   void scheduleFrame() {
    //只有當APP處于用戶可見狀態才會準備調度下一幀方法
    if (_hasScheduledFrame || !framesEnabled)
      return;
    ensureFrameCallbacksRegistered();
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }

2.7 scheduleFrame

[ -> lib/ui/window.dart::Window]

  void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';

window是Flutter引擎中跟圖形相關接口打交道的核心類,這里是一個native方法

2.7.1 ScheduleFrame(C++)

[->引擎庫 lib/ui/window/platform_configuration.cc]

  void ScheduleFrame(Dart_NativeArguments args) {
  UIDartState::ThrowIfUIOperationsProhibited();
  UIDartState::Current()->platform_configuration()->client()->ScheduleFrame();
}

通過RegisterNatives()完成native方法的注冊,“PlatformConfiguration_scheduleFrame”所對應的native方法如上所示。

2.7.2 RuntimeController::ScheduleFrame

[->runtime/runtime_controller.cc]

  // |PlatformConfigurationClient|
void RuntimeController::ScheduleFrame() {
  client_.ScheduleFrame();
}

2.7.3 Engine::ScheduleFrame

[->flutter/shell/common/engine.cc]

  void Engine::ScheduleFrame(bool regenerate_layer_tree) {
  animator_->RequestFrame(regenerate_layer_tree);
}

Engine::ScheduleFrame()經過層層調用,最終會注冊Vsync回調。 等待下一次vsync信號的到來,然后再經過層層調用最終會調用到Window::BeginFrame()。這里不展開解釋了。

2.8 Window::BeginFrame

[-> flutter/lib/ui/window/window.cc]

  void Window::BeginFrame(fml::TimePoint frameTime) {
  std::shared_ptr<tonic::DartState> dart_state = library_.dart_state().lock();
  if (!dart_state)
    return;
  tonic::DartState::Scope scope(dart_state);

  int64_t microseconds = (frameTime - fml::TimePoint()).ToMicroseconds();

  DartInvokeField(library_.value(), "_beginFrame",
                  {
                      Dart_NewInteger(microseconds),
                  });

  //執行MicroTask
  UIDartState::Current()->FlushMicrotasksNow();

  DartInvokeField(library_.value(), "_drawFrame", {});
}

Window::BeginFrame()過程主要工作:

  • 執行_beginFrame
  • 執行FlushMicrotasksNow
  • 執行_drawFrame

可見,Microtask位于beginFrame和drawFrame之間,那么Microtask的耗時會影響ui繪制過程。

2.9handleBeginFrame

[-> lib/src/scheduler/binding.dart:: SchedulerBinding]

  void handleBeginFrame(Duration rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineWhitelistArguments);
  _firstRawTimeStampInEpoch ??= rawTimeStamp;
  _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
  if (rawTimeStamp != null)
    _lastRawTimeStamp = rawTimeStamp;

  profile(() {
    _profileFrameNumber += 1;
    _profileFrameStopwatch.reset();
    _profileFrameStopwatch.start();
  });

  //此時階段等于SchedulerPhase.idle;
  _hasScheduledFrame = false;
  try {
    Timeline.startSync('Animate', arguments: timelineWhitelistArguments);
    _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,執行相應的Animate操作,可通過scheduleFrameCallback()/cancelFrameCallbackWithId()來完成添加和刪除成員,再來簡單看看這兩個方法。

2.10 handleDrawFrame

[-> lib/src/scheduler/binding.dart:: SchedulerBinding]

  void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // 標識結束"Animate"階段
  try {
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    //執行PERSISTENT FRAME回調
    for (FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);

    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    // 執行POST-FRAME回調
    final List<FrameCallback> localPostFrameCallbacks = List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (FrameCallback callback in localPostFrameCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);
  } finally {
    _schedulerPhase = SchedulerPhase.idle;
    Timeline.finishSync(); //標識結束”Frame“階段
    profile(() {
      _profileFrameStopwatch.stop();
      _profileFramePostEvent();
    });
    _currentFrameTimeStamp = null;
  }
}

該方法主要功能:

  • 遍歷_persistentCallbacks,執行相應的回調方法,可通過addPersistentFrameCallback()注冊,一旦注冊后不可移除,后續每一次frame回調都會執行;
  • 遍歷_postFrameCallbacks,執行相應的回調方法,可通過addPostFrameCallback()注冊,handleDrawFrame()執行完成后會清空_postFrameCallbacks內容。

三、小結

可見,setState()過程主要工作是記錄所有的臟元素,添加到BuildOwner對象的_dirtyElements成員變量,然后調用scheduleFrame來注冊Vsync回調。 當下一次vsync信號的到來時會執行handleBeginFrame()和handleDrawFrame()來更新UI。

本文參考了gityan大佬的博客,感謝大佬分享!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372