Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

Lision發表於2020-04-18

前言

Emmmmm...正式開始文章前先交代個事兒,2019 開年計劃那篇文章被我刪了。原因沒啥特別的,就真的只是臉疼...

嘛~ 好久沒更 Blog 了,最近筆者正在學習 Flutter 相關的知識(貌似現在學也不算特別晚),所以後續可能會有一波連續的 Flutter 相關的更新(關鍵字已加粗 ?)。

因為之前我的文章大多是原始碼剖析相關的,所以這次決定換種方式先從 Flutter 開發的狀態管理聊起。由於個人能力不足、水平有限,文章中難免有紕漏和謬誤,加上工作確實比較忙可能沒有時間校驗,希望發現的朋友能夠在文章下方評論交流討論(主要是避免誤人子弟,不過評論應該需要科學上網)。

本文會簡單介紹 Flutter 以及宣告式程式設計思想和程式碼畫風,對比 StatelessWidget & StatefulWidget 這兩個重要的 Widget,再聊聊 setState 背後的那些事兒。

索引

  • Flutter 簡介
  • 宣告式 UI & 響應式程式設計
  • Flutter 如何渲染 Widget
  • StatelessWidget & StatefulWidget
  • State.setState 背後的那些事兒

Flutter 簡介

Flutter 是 Google 開源的 UI 工具包,幫助開發者通過一套程式碼庫高效構建多平臺精美應用,支援移動、Web、桌面和嵌入式平臺。

Write once, run anywhere.」始終是大前端開發乃至整個程式界的一個永恆的話題。誠然 Flutter 的目標也是如此,不過它與之前業界已經存在的 React Native、Xamarin、Hybrid 等框架有何不同?

{% asset_img flutter_system_overview.png %}

從上圖可以看到 Flutter 的設計整體上有三層抽象:

  • 最上層 Framework 封裝好了 Material 和 Cupertino 這兩種分別對應 Android 和 iOS 官方設計風格的 UI 庫,除此之外還包含了渲染、動畫、繪製、手勢等等,這一層主要是提供給開發者便捷構建 App 使用的,其在開發時為 JIT、釋出時為 AOT 的特性兼顧了開發除錯的便捷與線上執行的高效能;
  • Engine 作為中間層,使用 C/C++ 實現了一套高效能、可移植的 Runtime,遮蔽了平臺間差異的同時支撐了上層的 Flutter 應用。
  • Embedder 由平臺指定語言實現,提供了 Flutter 所需的事件迴圈、執行緒、渲染等基礎 API。

Flutter 通過這套機制接管了最底層的系統介面,提供了一整套從底層渲染邏輯到上層開發語言的完整解決方案,使得它有著超越 React Native 的高保真、多端一致的體驗,和超越 Web 容器的高效能渲染能力。

宣告式 UI & 響應式程式設計

宣告式 UI

Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

這張圖很好的描述了宣告式 UI 的核心思想,簡單來說就是通過 state 作為入參根據已經寫好的構建 func 就能得到我們想要的 UI 效果。舉個 ? 更直觀:

預設的 Flutter Demo 介面:

{% asset_img flutter_default_demo.png %}

對應的程式碼:

return Scaffold(
  appBar: AppBar(
    title: Text(widget.title),
  ),
  body: Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.display1,
        ),
      ],
    ),
  ),
  floatingActionButton: FloatingActionButton(
    // _incrementCounter 內部邏輯可忽略
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
  ),
);
複製程式碼

In flutter, everything is a widget.

Emmmmm...可以注意到上面的程式碼中 children: <Widget>[...] 一行,Flutter 開發中就是通過這種方式巢狀組合 Widget 來描述使用者介面的。從這一角度講,Widget 是對一部分使用者介面的不可變描述。

預設的 Flutter Demo 跑起來很簡單,推薦大家親身體驗。簡單說一下我的感受,宣告式 UI 的優勢很大,具體表現為只需要在寫 UI 時將不同 state 對應的 UI 展示考慮並描述清楚就可以省去後續 state 變更時命令式 UI 需要手動管理檢視層級(新增或刪除檢視元素)和更新屬性(顏色、字號等)等麻煩。

值得一提的是 SwiftUI 也使用了宣告式 UI 的思想(宣告式 UI 才是未來)。

響應式程式設計

響應式程式設計 ,在計算機領域,響應式程式設計是一個專注於資料流和變化傳遞的非同步程式設計正規化。 這意味著可以使用程式語言很容易地表示靜態(例如陣列)或動態(例如事件發射器)資料流, 並且在關聯的執行模型中,存在著可推斷的依賴關係,這個關係的存在有利於自動傳播與資料流有關的更改。

響應式程式設計對於大家來說應該早已不是什麼新鮮事物了,單在移動客戶端領域就有諸如 ReactiveCocoaRxSwiftRxJava、RxXxx...簡單來說這種程式設計思想或者說正規化下開發者只需關注可能存在的資料狀態以及與之對應的邏輯從而大大減輕了維護這些對應關係與狀態細節的工作負擔。因為用過的人都說好,所以目前漸漸被推為移動客戶端乃至整個大前端的主流程式設計思想。

不難看出其實宣告式 UI 和響應式程式設計從思想上是完全契合的,我們只需要將資料流對應到 UI=f(state) 公式中的 state 就可以了。

Flutter 如何渲染 Widget

Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

在介紹 StatelessWidgetStatefulWidget 之前先要了解一下 Flutter 的渲染模型,簡單來講 Flutter 在渲染 Widget 時用到三棵樹:

  • Widget,負責描述 Element 的配置。
  • Element,負責管理 Widget 和 RenderObject 的生命週期。
  • RenderObject,負責控制尺寸,佈局和繪製工作。

一言以蔽之,Element 會根據我們書寫的 Widget 對其的配置描述來生成並管理對應的 Element 與 RenderObject,由 RenderObject 負責最終的繪製工作。

NOTE: Element 會根據 Widget 的描述最大程度複用現有 Element 與 RenderObject 以提升效能。

StatelessWidget & StatefulWidget

StatelessWidget

StatelessWidget, A widget that does not require mutable state.

StatelessWidget 如其名 Stateless,它不需要追蹤 State 並根據 State 更新 UI,所以一般 StatelessWidget 內部的屬性用 final 修飾宣告且構造方法一般以 const 修飾。

NOTE: const 修飾的 Widget 構造方法使用時可以提效。

StatefulWidget

StatefulWidget, A widget that has mutable state.

StatefulWidgetStatelessWidget 一樣,也可以通過一系列(組合巢狀)其他更具體描述 UI 的 Widget(比如 Text)來構建部分使用者介面。區別在於 Stateful,即它需要追蹤 State 並根據 State 更新 UI。

區別

StatelessWidgetStatefulWidget 大概是我們用 Flutter 技術棧開發 App 時最常打交道的兩個 Widget 了。

共同點:

  • 這兩個 Widget 都可以用來通過對其他一系列 Widget 構建完成一部分使用者介面的封裝。

不同點:

  • StatelessWidget 更適用於一些不需通過使用者互動或其他原因通過 State 控制更新的 UI 封裝。
  • StatefulWidget 更適用於追蹤某個會根據使用者互動或其他因素影響的 State,並根據最新的 State 實時更新的 UI 封裝。

此外,Flutter 對於 StateLessWidgetStatefulWidget 的繪製也有差異:

  • StatelessWidget 通過 StatelessElement.build 觸發 build
  • StatefulWidget 通過 StatefulElement.build 觸發 State.build

State.setState 背後的那些事兒

NOTE: 下面分析的 State.setState 原始碼版本為 Flutter SDK v1.9.1+hotfix.6。

我們還以 Flutter 預設 Demo 來分析,在 Demo 中我們點選介面右下角的 FloatingActionButton (就是藍色帶有 + 號的圓形按鈕)之後會重新整理介面,螢幕中間的 Text Widget 會顯示按鈕被按下的次數。

Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

FloatingActionButton 點選之後呼叫了 _incrementCounter 方法:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  ...
}
複製程式碼

_MyHomePageState._incrementCounter 內部邏輯也僅僅是呼叫了 State.setState 而已。正如 Flutter 所宣傳的公式 UI = f(state) 一樣,開發者只需要呼叫 State.setState 傳入 VoidCallback 內部寫好相關資料更新邏輯即可更新 UI,那麼 Flutter 是如何做到這一點的呢?

State.setState 背後邏輯

Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

State.setState 背後呼叫巢狀較多,實際所做的事情理解起來卻很簡單。如不喜在文內閱讀原始碼可直接跳轉至「總結 State.setState 背後邏輯」小節看相關邏輯總結。

State.setState

@optionalTypeArgs
abstract class State<T extends StatefulWidget> extends Diagnosticable {
  ...
  @protected
  void setState(VoidCallback fn) {
    // assert ...
    final dynamic result = fn() as dynamic;
    // assert ...
    _element.markNeedsBuild();
  }
  ...
}
複製程式碼

可見 State.setState 內除去斷言的邏輯只有兩行程式碼:

  • 執行入參 VoidCallback 邏輯,Demo 中對應 _counter++;
  • 執行 _element.markNeedsBuild();

Element.markNeedsBuild

abstract class Element extends DiagnosticableTree implements BuildContext {
  ...
  void markNeedsBuild() {
    // assert ...
    if (!_active)
      return;
    // assert ...
    if (dirty)
      return;
    _dirty = true;
    owner.scheduleBuildFor(this);
  }
  ...
}
複製程式碼

Element.markNeedsBuild 內部的邏輯也很簡單明瞭:

  • 如此 Element 不再活躍則直接 return 無需做任何事
  • 如此 Element 已經被標記為 dirty 也無需重複標記直接 return
  • 以上條件不滿足時說明此 Element 是活躍且需要重新構建的,所以標記為 dirty 後呼叫 owner.scheduleBuildFor(this);

BuildOwner.scheduleBuildFor

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

BuildOwner.scheduleBuildFor(this); 內部邏輯也不復雜:

  • !_scheduledFlushDirtyElements && onBuildScheduled != null 可以理解為如果還沒有安排過重新整理此 BuildOwner 下被標記為 Dirty 的 Elements 且安排構建的邏輯不為空,滿足以上條件則將此 BuildOwner 安排重新整理 Dirty Elements 的標記置為 true
  • 將傳入的 Element 加入此 BuildOwner 管理的 _dirtyElements 並將此 Element _inDirtyList 標記為 true

BuildOwner.onBuildScheduled

其實 onBuildScheduled(); 跟下去是 BuildOwner 內部屬性 onBuildScheduled,型別為 VoidCallback,在 WidgetsBinding.initInstances 時被賦值為 WidgetsBinding._handleBuildScheduled:

mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  ...
  @override
  void initInstances() {
    ...
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    ...
  }
  ...
}
複製程式碼

WidgetsBinding 可以理解為 Widget 層與 Flutter engine 之間的膠水層,篇幅原因不展開講,我們直接看 WidgetsBinding._handleBuildScheduled

mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  ...
  void _handleBuildScheduled() {
    // assert ...
    ensureVisualUpdate();
  }
  ...
}
複製程式碼

SchedulerBinding.ensureVisualUpdate

mixin SchedulerBinding on BindingBase, ServicesBinding {
  ...
  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.postFrameCallbacks 時呼叫 scheduleFrame();

SchedulerPhase.scheduleFrame

mixin SchedulerBinding on BindingBase, ServicesBinding {
  ...
  void scheduleFrame() {
    if (_hasScheduledFrame || !_framesEnabled)
      return;
    // assert ...
    window.scheduleFrame();
    _hasScheduledFrame = true;
  }
  ...
}
複製程式碼

這裡的邏輯更簡單:

  • _hasScheduledFrame || !_framesEnabled 可理解為如果已經安排過 Frame 或當前 Frame 不可用,這種情況直接 return
  • 不滿足上述情況則呼叫 window.scheduleFrame(); 然後標記 _hasScheduledFrametrue

Window.scheduleFrame

void scheduleFrame() native 'Window_scheduleFrame';
複製程式碼

這裡的 Window.scheduleFrame 是 Native 方法,可以理解為通過 Window 與 Flutter engine 打交道,註冊了 VSync 回撥。

總結 State.setState 背後邏輯

Flutter 狀態管理 0x00 - 基礎知識及 State.setState 背後邏輯

簡單來說 State.setState 就幹了這麼幾件事:

  • 執行入參 VoidCallback 邏輯,即執行開發者寫好的資訊變更邏輯
  • 嘗試將當前 StatefulWidget 對應的 StatefulElement 標記為 dirty
  • 通過 BuildOwner.onBuildScheduledSchedulerPhase.scheduleFrame 再到 Window.scheduleFrame 一步步完成了 VSync 的回撥註冊

注意上面 State.setState 第二條主要邏輯就是把 State 關聯的 Element 標記為 dirty。在 VSync 回撥後會通過 Native 到 Flutter engine 呼叫 Flutter _drawFrame 方法,將之前標記為 dirty 的 Element 重新構建,最終會執行到開發者熟悉的 State.build 方法。

State.build 是如何被執行的

這裡因為篇幅原因簡單描述下後續邏輯,畢竟本文重點不在於 Flutter 如何渲染 Widget:

  • State 是開發者重寫 StatefulWidget.createState 返回的
  • State 對應的 Element 是 StatefulWidget.createElement 返回的,型別為 StatefulElement
  • StatefulElement 被標記為 dirty 後在 Flutter _drawFrame 時重新構建會呼叫 StatefulElement.build,其原始碼為 Widget build() => state.build(this);
  • state.build(this) 中的 state 就是我們的 State,開發者寫的 State.build 方法就這樣被執行了

總結

本文是 Flutter 狀態管理的開篇,為了照顧一些還沒來得及學習 Flutter 或者剛入門 Flutter 的初心者所以文章從前到後做了一些鋪墊介紹過渡。文章內容也比較淺顯易懂,基本上是圍繞 Flutter 建立 App 預設的 Demo 來展開講解一些基本的 Flutter 知識點:

  • Flutter 渲染 Widget 的三顆樹概念
  • StatelessWidget & StatefulWidget
  • State.setState 背後邏輯

由於狀態管理這個話題非常大且複雜,文章因為篇幅原因就到這裡,後續的文章(如果有的話)應該不會再花大篇幅做 Flutter 基礎知識的鋪墊和過渡了(但是該有的前置知識點肯定還會有)。時間緊張,文章難免出現謬誤,估計自己也沒有時間做校對了,有問題還望在評論區提醒。

擴充套件補充

寫到這裡我意識到 Flutter 狀態管理這個話題下可以引出很多值得挖掘的內容,而且筆者堅信宣告式 UI + 響應式程式設計會是未來移動客戶端乃至整個大前端的主流程式設計正規化。鑑於此,後續的文章計劃圍繞 Flutter 狀態管理逐步深挖,內容也將在 Flutter 技術棧的基礎上做適度橫向對比。

Emmmmm...後續的文章將會發布在我的《Flutter 狀態管理》小專欄。至於專欄定價嘛,目前隨便定了 ¥69(作為小透明的我瑟瑟發抖 ing~)。其實倒也不是很在意能賣出多少份,或者從中收到多少錢,畢竟自己寫文章的精力拿去隨便接個朋友的私活都應該比這次通過小專欄賺到的多。比起收入更多的是對自己的鞭策吧,畢竟是收費的東西,只要有一個人訂閱了這個專欄就意味著對我的肯定,我就有責任把這個方向挖掘的更深入,同時文章質量也應該比部落格內容更高。

其實早在去年就有幸被小專欄的開發者@安卓大王子邀請去小專欄寫文章,不過當時自己感覺沒什麼好的內容方向所以只是開通了專欄但沒有任何輸出。時至今日終於找到了一個值得深挖的方向,自然而然想到了這個平臺。個人感覺小專欄這個平臺沒有太重的商業化味道,這點很討喜,在微信上面的閱讀體驗也比自己發公眾號要好一些,更難能可貴的是省去了發公眾號的繁瑣操作流程。可能正是商業化氛圍弱的原因,上面的文章質量還都挺不錯的,推薦各位讀者朋友可以上去試讀或者寫作。

相關文章