深刻淺出 Flutter Framework 之 Widget

本文是『 深刻淺出 Flutter Framework 』系列文章的第一篇,主要以不一樣類型 Widget 的核心方法爲切入點,對其展開詳細分析。html

本文同時發表於個人我的博客git

Overview


Flutter 做爲一種新興跨平臺解決方案,自 2017 年 Google 在 I/O 大會上推出後,尤爲是在 2018 年 I/O 大會上發佈第一個預覽版後,迅速引發移動開發者的普遍關注,併成爲時下最熱門的跨平臺解決方案 ( 沒有之一 ) !github

本系列文章將深刻 Flutter Framework 內部逐步去分析其核心概念和流程,主要包括:設計模式

  • 『 深刻淺出 Flutter Framework 之 Widget 』
  • 『 深刻淺出 Flutter Framework 之 BuildOwner 』
  • 『 深刻淺出 Flutter Framework 之 Element 』
  • 『 深刻淺出 Flutter Framework 之 PipelineOwner 』
  • 『 深刻淺出 Flutter Framework 之 RenderObejct 』
  • 『 深刻淺出 Flutter Framework 之 Layer 』
  • 『 深刻淺出 Flutter Framework 之 Binding 』
  • 『 深刻淺出 Flutter Framework 之 Rendering Pipeline 』
  • 『 深刻淺出 Flutter Framework 之 自定義 Widget 』

其中,前 7 篇屬於基礎篇,分別介紹 Flutter 中幾個最核心的概念。Rendering Pipeline 篇則是在此基礎上從 Build、Layout 到 Paint 的流程將它們串起來,分析 Flutter UI 是如何建立的、如何更新的。最後,自定義 Widget 屬於回顧、實踐篇,分析自定義一個 Render Widget 至少須要哪些步驟。緩存

下圖所示,Flutter 總體分爲三層:Framework (dart)、Engine (C/C++)、Embedder (Platform),上述文章主要集中在 Framework 這一層。 app

Widget


Everything’s a widget.less

在開發 Flutter 應用過程當中,接觸最多的無疑就是Widget,是『描述』 Flutter UI 的基本單元,經過Widget能夠作到:異步

  • 描述 UI 的層級結構 (經過Widget嵌套);
  • 定製 UI 的具體樣式 (如:fontcolor等);
  • 指導 UI 的佈局過程 (如:paddingcenter等);
  • ...

Google 在設計Widget時,還賦予它一些鮮明的特色:ide

  • 聲明式 UI —— 相對於傳統 Native 開發中的命令式 UI,聲明式 UI 有很多優點,如:開發效率顯著提高、UI 可維護性明顯增強等;函數

  • 不可變性 —— Flutter 中全部Widget都是不可變的(immutable),即其內部成員都是不可變的(final),對於變化的部分須要經過「Stateful Widget-State」的方式實現;

  • 組合大於繼承 —— Widget設計遵循組合大於繼承這一優秀的設計理念,經過將多個功能相對單一的Widget組合起來即可獲得功能相對複雜的Widget

Widget類定義處有這樣一段註釋:

這段註釋闡明瞭 Widget的本質: 用於配置Element的,Widget本質上是 UI 的配置信息 (附帶部分業務邏輯)。

咱們一般會將經過Widget描述的 UI 層級結構稱之爲「Widget Tree」,但與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,實質上並不存在「Widget Tree」。爲了描述方便,將 Widget 組合描述的 UI 層級結構稱之爲「Widget Tree」,也何嘗不可。

分類

如上圖所示,按照功能劃分 Widget大體能夠分爲 3 類:

  • 「Component Widget」 —— 組合類 Widget,這類 Widget 都直接或間接繼承於StatelessWidgetStatefulWidget,上一小節提到過在 Widget 設計上遵循組合大於繼承的原則,經過組合功能相對單一的 Widget 能夠獲得功能更爲複雜的 Widget。日常的業務開發主要是在開發這一類型的 Widget;

  • 「Proxy Widget」 —— 代理類 Widget,正如其名,「Proxy Widget」自己並不涉及 Widget 內部邏輯,只是爲「Child Widget」提供一些附加的中間功能。典型的如:InheritedWidget用於在「Descendant Widgets」間傳遞共享信息、ParentDataWidget用於配置「Descendant Renderer Widget」的佈局信息;

  • 「Renderer Widget」 —— 渲染類 Widget,是最核心的Widget類型,會直接參與後面的「Layout」、「Paint」流程,不管是「Component Widget」仍是「Proxy Widget」最終都會映射到「Renderer Widget」上,不然將沒法被繪製到屏幕上。這 3 類 Widget 中,只有「Renderer Widget」有與之一一對應的「Render Object」

核心方法源碼分析

下面,咱們重點介紹各種型 Widget 的核心方法,以便更好地理解 Widget 是如何參與整個 UI 的構建過程。

Widget

Widget,全部 Widget 的基類。

如上圖所示,在 Widget基類中有 3 個重要的方法 (屬性):

  • Key key —— 在同一父節點下,用做兄弟節點間的惟一標識,主要用於控制當 Widget 更新時,對應的 Element 如何處理 (是更新仍是新建)。若某 Widget 是其「Parent Widget」惟一的子節點時,通常不用設置 key;

GlobalKey 是一類較特殊的 key,在介紹 Element 時會附帶介紹。

  • Element createElement() —— 每一個Widget都有一個與之對應的Element,由該方法負責建立,createElement能夠理解爲設計模式中的工廠方法,具體的Element類型由對應的Widget子類負責建立;

  • static bool canUpdate(Widget oldWidget, Widget newWidget) —— 是否能夠用 new widget 修改前一幀用 old widget 生成的 Element,而不是建立新的 Element,Widget類的默認實現爲:2個WidgetruntimeTypekey都相等時,返回true,便可以直接更新 (key 爲 null 時,認爲相等)。

上述更新流程,一樣在介紹 Element 時會重點分析。

StatelessWidget

無狀態-組合型 Widget,由其build方法描述組合 UI 的層級結構。在其生命週期內狀態不可變。

ps: 對於有父子關係的類,在子類中只會介紹新增或有變化的方法

  • StatelessElement createElement() ——「Stateless Widget」對應的 Element 爲StatelessElement,通常狀況下StatelessWidget子類沒必要重寫該方法,即子類對應的 Element 也是StatelessElement

  • Widget build(BuildContext context) —— 算是 Flutter 體系中的核心方法之一,以『聲明式 UI』的形式描述了該組合式 Widget 的 UI 層級結構及樣式信息,也是開發 Flutter 應用的主要工做『場所』。該方法在 3 種狀況下被調用:

    • Widget 第一次被加入到 Widget Tree 中 (更準確地說是其對應的 Element 被加入到 Element Tree 時,即 Element 被掛載『mount』時);
    • 「Parent Widget」修改了其配置信息;
    • 該 Widget 依賴的「Inherited Widget」發生變化時。

當「Parent Widget」或 依賴的「Inherited Widget」頻繁變化時,build方法也會頻繁被調用。所以,提高build方法的性能就顯得十分重要,Flutter 官方給出了幾點建議:

  • 減小沒必要要的中間節點,即減小 UI 的層級,如:對於「Single Child Widget」,不必經過組合「Row」、「Column」、「Padding」、「SizedBox」等複雜的 Widget 達到某種佈局的目標,或許經過簡單的「Align」、「CustomSingleChildLayout」便可實現。又或者,爲了實現某種複雜精細的 UI 效果,不必定要經過組合多個「Container」,再附加「Decoration」來實現,經過 「CustomPaint」自定義或許是更好的選擇;

  • 儘量使用const Widget,爲 Widget 提供const構造方法;

    關於 const constructor 推薦 Dart Constant Constructors 看看這篇文章的評論。

  • 必要時,能夠將「Stateless Widget」重構成「Stateful Widget」,以即可以使用「Stateful Widget」中一些特定的優化手法,如:緩存「sub trees」的公共部分,並在改變樹結構時使用GlobalKey

  • 儘可能減少 rebuilt 範圍,如:某個 Widget 因使用了「Inherited Widget」,致使頻繁 rebuilt,能夠將真正依賴「Inherited Widget」的部分提取出來,封裝成更小的獨立 Widget,並儘可能將該獨立 Widget 推向樹的葉子節點,以便減少 rebuilt 時受影響的範圍。

StatefulWidget

有狀態-組合型 Widget,但要注意的是StatefulWidget自己仍是不可變的,其可變狀態存在於State中。

  • StatefulElement createElement() ——「Stateful Widget」對應的 Element 爲StatefulElement,通常狀況下StatefulWidget子類不用重寫該方法,即子類對應的Element 也是StatefulElement

  • State createState() —— 建立對應的 State,該方法在StatefulElement的構造方法中被調用。能夠簡單地理解爲當「Stateful Widget」被添加到 Widget Tree 時會調用該方法。

// 代碼已精簡處理(本文中其餘代碼會作一樣的簡化處理)
StatefulElement(StatefulWidget widget)
    : _state = widget.createState(), super(widget) {
    _state._element = this;
    _state._widget = widget;
}
複製代碼

其實是「Stateful Widget」對應的「Stateful Element」被添加到 Element Tree 時,伴隨「Stateful Element」的初始化,createState方法被調用。從後文可知一個 Widget 實例能夠對應多個 Element 實例 (也就是同一份配置信息 (Widget) 能夠在 Element Tree 上不一樣位置配置多個 Element 節點),所以,createState方法在「Stateful Widget」生命週期內可能會被調用屢次。

另外,須要注意的是配有GlobalKey的 Widget 對應的 Element 在整個 Element Tree 中只有一個實例。

State

The logic and internal state for a 「Stateful Widget」.

State 用於處理「Stateful Widget」的業務邏輯以及可變狀態。 因爲其內部狀態是可變的,故 State 有較複雜的生命週期:

如上圖,State 的生命週期大體能夠分爲 8 個階段:

  • 在對應的「Stateful Element」被掛載 (mount) 到樹上時,經過StatefulElement.constructor --> StatefulWidget.createState建立 State 實例;

StatefulElement.constructor中的_state._element = this;可知,State._emelent指向了對應的 Element 實例,而咱們熟知的State.context引用的就是這個_elementBuildContext get context => _element;State實例與Element實例間的綁定關係一經肯定,在整個生命週期內不會再變了 (Element 對應的 Widget 可能會變,但對應的 State 永遠不會變),期間,Element能夠在樹上移動,但上述關係不會變 (即「Stateful Element」是帶着狀態移動的)。

  • StatefulElement 在掛載過程當中接着會調用State.initState,子類能夠重寫該方法執行相關的初始化操做 (此時能夠引用contextwidget屬性);

  • 一樣在掛載過程當中會調用State.didChangeDependencies,該方法在 State 依賴的對象 (如:「Inherited Widget」) 狀態發生變化時也會被調用,*子類不多須要重寫該方法,*除非有很是耗時不宜在build中進行的操做,由於在依賴有變化時build方法也會被調用;

  • 此時,State 初始化已完成,其build方法此後可能會被屢次調用,在狀態變化時 State 可經過setState方法來觸發其子樹的重建;

  • 此時,「element tree」、「renderobject tree」、「layer tree」已構建完成,完整的 UI 應該已呈現出來。此後由於變化,「element tree」中「parent element」可能會對樹上該位置的節點用新配置 (Widget) 進行重建,當新老配置 (oldWidget、newWidget)具備相同的「runtimeType」&&「key」時,framework 會用 newWidget 替換 oldWidget,並觸發一系列的更新操做 (在子樹上遞歸進行)。同時,State.didUpdateWidget方法被調用,子類重寫該方法去響應 Widget 的變化;

上述 3 棵樹以及更新流程在後續文章中會有詳細介紹

  • 在 UI 更新過程當中,任何節點都有被移除的可能,State 也會隨之移除,(如上一步中「runtimeType」||「key」不相等時)。此時會調用State.deactivate方法,因爲被移除的節點可能會被從新插入樹中某個新的位置上,故子類重寫該方法以清理與節點位置相關的信息 (如:該 State 對其餘 element 的引用)、同時,不該在該方法中作資源清理;

從新插入操做必須在當前幀動畫結束以前

  • 當節點被從新插入樹中時,State.build方法被再次調用;

  • 對於在當前幀動畫結束時還沒有被從新插入的節點,State.dispose方法被執行,State 生命週期隨之結束,此後再調用State.setState方法將報錯。子類重寫該方法以釋聽任何佔用的資源。

至此,State 中的核心方法基本都已在上述過程當中介紹了,下面重點看一下 setState方法:

void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    return true;
  }());
  
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    return true;
  }());
  
  _element.markNeedsBuild();
}
複製代碼

從上述源碼能夠看到,關於setState方法有幾點值得關注:

  • State.dispose後不能調用setState

  • 在 State 的構造方法中不能調用setState

  • setState方法的回調函數 (fn) 不能是異步的 (返回值爲Future),緣由很簡單,由於從流程設計上 framework 須要根據回調函數產生的新狀態去刷新 UI;

  • 經過setState方法之因此能更新 UI,是在其內部調用_element.markNeedsBuild()實現的 (具體過程在介紹 Element 時再詳細分析)。

關於 State 最後再強調 2 點:

  • State.build方法依賴了自身狀態會變化的對象,如:ChangeNotifierStream或其餘能夠被訂閱的對象,須要確保在initStatedidUpdateWidgetdispose等 3 方法間有正確的訂閱 (subscribe) 與取消訂閱 (unsubscribe) 的操做:

    • initState中執行 subscribe;
    • 若是關聯的「Stateful Widget」與訂閱有關,在didUpdateWidget中先取消舊的訂閱,再執行新的訂閱;
    • dispose中執行 unsubscribe。
  • State.initState方法中不能調用BuildContext.dependOnInheritedWidgetOfExactType,但State.didChangeDependencies會隨之執行,在該方法中能夠調用。

ParentDataWidget

ParentDataWidget以及下面要介紹的InheritedElement都繼承自ProxyWidget,因爲ProxyWidget做爲抽象基類自己沒有任何功能,故下面直接介紹ParentDataWidgetInheritedElement

ParentDataWidget做爲 Proxy 型 Widget,其功能主要是爲其餘 Widget 提供 ParentData信息。雖然其 child widget 不必定是 RenderObejctWidget 類型,但其提供的 ParentData信息最終都會落地到 RenderObejctWidget 類型子孫 Widget 上。

ParentData 是『parent renderobject』在 layout『child renderobject』時使用的輔助定位信息,詳細信息會在介紹 RenderObject 時介紹。

void attachRenderObject(dynamic newSlot) {
  assert(_ancestorRenderObjectElement == null);
  _slot = newSlot;
  _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
  _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
  final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
  if (parentDataElement != null)
    _updateParentData(parentDataElement.widget);
}

ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
  Element ancestor = _parent;
  while (ancestor != null && ancestor is! RenderObjectElement) {
    if (ancestor is ParentDataElement<RenderObjectWidget>)
      return ancestor;
    ancestor = ancestor._parent;
  }
  return null;
}

void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) {
  parentData.applyParentData(renderObject);
}
複製代碼

上面這段代碼來自RenderObjectElement,能夠看到在其attachRenderObject方法第 6 行從祖先節點找ParentDataElement,若是找到就用其 Widget(ParentDataWidget) 中的 parentData 信息去設置 Render Obejct。在查找過程當中如查到RenderObjectElement (第 13 行),說明當前 RenderObject 沒有 Parent Data 信息。 最終會調用到ParentDataWidget.applyParentData(RenderObject renderObject),子類須要重寫該方法,以便設置對應RenderObject.parentData

來看個例子,一般配合Stack使用的Positioned(繼承自ParentDataWidget):

void applyParentData(RenderObject renderObject) {
  assert(renderObject.parentData is StackParentData);
  final StackParentData parentData = renderObject.parentData;
  bool needsLayout = false;

  if (parentData.left != left) {
    parentData.left = left;
    needsLayout = true;
  }
  ...
  if (parentData.width != width) {
    parentData.width = width;
    needsLayout = true;
  }
  ...
  if (needsLayout) {
    final AbstractNode targetParent = renderObject.parent;
    if (targetParent is RenderObject)
      targetParent.markNeedsLayout();
  }
}
複製代碼

能夠看到,Positioned在必要時將本身的屬性賦值給了對應的RenderObject.parentData (此處是StackParentData),並對「parent render object」調用markNeedsLayout(第 19 行),以便從新 layout,畢竟修改了佈局相關的信息。

abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget 複製代碼

如上所示,ParentDataWidget在定義上使用了泛型<T extends RenderObjectWidget>,其背後的含義是: 從當前ParentDataWidget節點向上追溯造成的祖先節點鏈(『parent widget chain』)上,在 2 個ParentDataWidget類型的節點造成的鏈上至少要有一個『RenderObject Widget』類型的節點。由於一個『RenderObject Widget』不能接受來自 2 個及以上『ParentData Widget』的信息。

InheritedWidget

InheritedWidget 用於在樹上向下傳遞數據。 經過 BuildContext.dependOnInheritedWidgetOfExactType能夠獲取最近的「Inherited Widget」,須要注意的是經過這種方式獲取「Inherited Widget」時,當「Inherited Widget」狀態有變化時,會致使該引用方 rebuild。

具體原理在介紹 Element 時會詳細分析。

一般,爲了使用方便會「Inherited Widget」會提供靜態方法of,在該方法中調用BuildContext.dependOnInheritedWidgetOfExactTypeof方法能夠直接返回「Inherited Widget」,也能夠是具體的數據。

有時,「Inherited Widget」是做爲另外一個類的實現細節而存在的,其自己是私有的(外部不可見),此時of方法就會放到對外公開的類上。最典型的例子就是Theme,其自己是StatelessWidget類型,但其內部建立了一個「Inherited Widget」:_InheritedThemeof方法就定義在上Theme上:

static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
  final _InheritedTheme inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();

  return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}
複製代碼

of方法返回的是ThemeData類型的具體數據,並在其內部首先調用了BuildContext.dependOnInheritedWidgetOfExactType

咱們常用的「Inherited Widget」莫過於MediaQuery,一樣提供了of方法:

static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
  final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
  if (query != null)
    return query.data;
  if (nullOk)
    return null;
}
複製代碼

  • InheritedElement createElement() ——「Inherited Widget」對應的 Element 爲InheritedElement,通常狀況下InheritedElement子類不用重寫該方法;

  • bool updateShouldNotify(covariant InheritedWidget oldWidget) —— 在「Inherited Widget」rebuilt 時判斷是否須要 rebuilt 那些依賴它的 Widget;

以下是MediaQuery.updateShouldNotify的實現,在新老Widget.data 不相等時才 rebuilt 那依賴的 Widget。

bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
複製代碼

RenderObjectWidget

真正與渲染相關的 Widget,屬於最核心的類型,一切其餘類型的 Widget 要渲染到屏幕上,最終都要回歸到該類型的 Widget 上。

  • RenderObjectElement createElement() ——「RenderObject Widget」對應的 Element 爲RenderObjectElement,因爲RenderObjectElement也是抽象類,故子類須要重寫該方法;

  • RenderObject createRenderObject(BuildContext context) —— 核心方法,建立 Render Widget 對應的 Render Object,一樣子類須要重寫該方法。該方法在對應的 Element 被掛載到樹上時調用(Element.mount),即在 Element 掛載過程當中同步構建了「Render Tree」(詳細過程後續文章會詳細分析);

@override
RenderFlex createRenderObject(BuildContext context) {
  return RenderFlex(
    direction: direction,
    mainAxisAlignment: mainAxisAlignment,
    mainAxisSize: mainAxisSize,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: getEffectiveTextDirection(context),
    verticalDirection: verticalDirection,
    textBaseline: textBaseline,
  );
}
複製代碼

上面是Flex.createRenderObject的源碼,真實感覺一下 (仍是代碼更有感受)。能夠看到,用Flex的信息(配置)初始化了RenderFlex

FlexRowColumn的基類,RenderFlex繼承自RenderBox,後者繼續自RenderObject

  • void updateRenderObject(BuildContext context, covariant RenderObject renderObject) —— 核心方法,在 Widget 更新後,修改對應的 Render Object。該方法在首次 build 以及須要更新 Widget 時都會調用;
@override
void updateRenderObject(BuildContext context, covariant RenderFlex renderObject) {
  renderObject
    ..direction = direction
    ..mainAxisAlignment = mainAxisAlignment
    ..mainAxisSize = mainAxisSize
    ..crossAxisAlignment = crossAxisAlignment
    ..textDirection = getEffectiveTextDirection(context)
    ..verticalDirection = verticalDirection
    ..textBaseline = textBaseline;
}
複製代碼

Flex.updateRenderObject的源碼也很簡單,與Flex.createRenderObject幾乎一一對應,用當前Flex的信息修改renderObject

  • void didUnmountRenderObject(covariant RenderObject renderObject) —— 對應的「Render Object」從「Render Tree」上移除時調用該方法。

RenderObjectWidget的幾個子類:LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget只是重寫了createElement方法以便返回各自對應的具體的 Element 類實例。

小結


至此,重要的基礎型 Widget 基本介紹完了,總結一下:

  • Widget 本質上是 UI 的配置信息 (附加部分業務邏輯),並不存在一顆真實的「Widget Tree」(與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比);

  • Widget 從功能上能夠分爲 3 類:「Component Widget」、「Proxy Widget」以及「Renderer Widget」;

  • Widget 與 Element 一一對應,Widget 提供建立 Element 的方法 (createElement,本質上是一個工廠方法);

  • 只有「Renderer Widget」纔會參與最終的 UI 生成過程(Layout、Paint),只有該類型的 Widget 纔有與之對應的「Render Object」,一樣由其提供建立方法(createRenderObject)。

下篇再見!

相關文章
相關標籤/搜索