本文是『 深入淺出 Flutter Framework 』系列文章的第三篇,主要圍繞 Element 相關內容進行分析介紹,包括 Element 分類、Element 與其他幾個核心元素的關係、Element 生命週期以及核心方法解讀等。
本文同時發表於我的個人部落格
本系列文章將深入 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 』
Overview
通過『 深入淺出 Flutter Framework 之 Widget 』的介紹,我們知道 Widget 本質上是 UI 的配置資料 (靜態、不可變),Element 則是通過 Widget 生成的『例項』,兩者間的關係就像是 json 與 object。
同一份配置 (Widget) 可以生成多個例項 (Element),這些例項可能會被安插在樹上不同的位置。
UI 的層級結構在 Element 間形成一棵真實存在的樹「Element Tree」,Element 有 2 個主要職責:
- 根據 UI (「Widget Tree」) 的變化來維護「Element Tree」,包括:節點的插入、更新、刪除、移動等;
- Widget 與 RenderObject 間的協調者。
分類
如圖所示,Element 根據特點可以分為 2 類:
- 「Component Element」 —— 組合型 Element,「Component Widget」、「Proxy Widget」對應的 Element 都屬於這一型別,其特點是子節點對應的 Widget 需要通過
build
方法去建立。同時,該型別 Element 都只有一個子節點 (single child); - 「Renderer Element」 —— 渲染型 Element,對應「Renderer Widget」,其不同的子型別包含的子節點個數也不一樣,如:LeafRenderObjectElement 沒有子節點,RootRenderObjectElement、SingleChildRenderObjectElement 有一個子節點,MultiChildRenderObjectElement 有多個子節點。
原生型 Element,只有 MultiChildRenderObjectElement 是多子節點的,其他都是單子節點。
同時,可以看到,Element
實現了BuildContext
介面 —— 我們在 Widget 中遇到的context
,其實就是該 Widget 對應的 Element。
關係
在繼續之前有必要先了解一下 Element 與其他幾個核心元素間的關係,以便在全域性上有個認識。
如圖:- Element 通過 parent、child 指標形成「Element Tree」;
- Element 持有 Widget、「Render Object」;
- State 是繫結在 Element 上的,而不是綁在「Stateful Widget」上(這點很重要)。
上述這些關係並不是所有型別的 Element 都有,如:「Render Object」只有「RenderObject Element」才有,State 只有「Stateful Element」才有。
生命週期
Element 作為『例項』,隨著 UI 的變化,有較複雜的生命週期:
-
parent 通過
Element.inflateWidget
->Widget.createElement
建立 child element,觸發場景有:UI 的初次建立、UI 重新整理時新老 Widget 不匹配(old element 被移除,new element 被插入); -
parent 通過
Element.mount
將新建立的 child 插入「Element Tree」中指定的插槽處 (slot);
dynamic Element.slot
——其含意對子節點透明,父節點用於確定其下子節點的排列順序 (兄弟節點間的排序)。因此,對於單子節點的節點 (single child),child.slot 通常為 null。 另外,slot 的型別是動態的,不同型別的 Element 可能會使用不同型別的 slot,如:Sliver 系列使用的是 int 型的 index,MultiChildRenderObjectElement 用兄弟節點作為後一個節點的 slot。 對於「component element」,mount
方法還要負責所有子節點的 build (這是一個遞迴的過程),對於「render element」,mount
方法需要負責將「render object」新增到「render tree」上。其過程在介紹到相應型別的 Element 時會詳情分析。
-
此時,(child) element 處於 active 狀態,其內容隨時可能顯示在螢幕上;
-
此後,由於狀態更新、UI 結構變化等,element 所在位置對應的 Widget 可能發生了變化,此時 parent 會呼叫
Element.update
去更新子節點,update 操作會在以當前節點為根節點的子樹上遞迴進行,直到葉子節點;(執行該步驟的前提是新老 Widget.[key && runtimeType] 相等,否則建立新 element,而不是更新現有 element); -
狀態更新時,element 也可能會被移除 (如:新老 Widget.[key || runtimeType] 不相等),此時,parent 將呼叫
deactivateChild
方法,該方法主要做了 3 件事:- 從「Element Tree」中移除該 element (將 parent 置為 null);
- 將相應的「render object」從「render tree」上移除;
- 將 element 新增到
owner._inactiveElements
中,在新增過程中會對『以該 element 為根節點的子樹上所有節點』呼叫deactivate
方法 (移除的是整棵子樹)。
void deactivateChild(Element child) { child._parent = null; child.detachRenderObject(); owner._inactiveElements.add(child); // this eventually calls child.deactivate() } 複製程式碼
-
此時,element 處於 "inactive" 狀態,並從螢幕上消失,該狀態一直持續到當前幀動畫結束;
-
從 element 進入 "inactive" 狀態到當前幀動畫結束期間,其還有被『搶救』的機會,前提是『帶有「global key」&& 被重新插入樹中』,此時:
- 該 element 將會從
owner._inactiveElements
中移除; - 對該 element subtree 上所有節點呼叫
activate
方法 (它們又復活了!); - 將相應的「render object」重新插入「render tree」中;
- 該 element subtree 又進入 "active" 狀態,並將再次出現在螢幕上。
上述過程經歷這幾個方法:
Parent Element.inflateWidget
-->Parent Element._retakeInactiveElement
-->BuildOwner._inactiveElements.remove
-->Child Element._activateWithParent
... - 該 element 將會從
-
對於所有在當前幀動畫結束時未能成功『搶救』回來的「Inactive Elements」都將被 unmount;
-
至此,element 生命週期圓滿結束。
核心方法
下面對 Element 中的幾個核心方法進行簡單介紹:
updateChild
updateChild
是 flutter framework 中的核心方法之一:
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
if (child != null) {
if (child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
return child;
}
deactivateChild(child);
assert(child._parent == null);
}
return inflateWidget(newWidget, newSlot);
}
複製程式碼
在「Element Tree」上,父節點通過該方法來修改子節點對應的 Widget。
根據傳入引數的不同,有以下幾種不同的行為:newWidget
==null
—— 說明子節點對應的 Widget 已被移除,直接 remove child element (如有);child
==null
—— 說明 newWidget 是新插入的,建立子節點 (inflateWidget);child
!=null
—— 此時,分為 3 種情況:- 若 child.widget == newWidget,說明 child.widget 前後沒有變化,若 child.slot != newSlot 表明子節點在兄弟結點間移動了位置,通過
updateSlotForChild
修改 child.slot 即可; - 通過
Widget.canUpdate
判斷是否可以用 newWidget 修改 child element,若可以,則呼叫update
方法; - 否則先將 child element 移除,並通 newWidget 建立新的 element 子節點。
- 若 child.widget == newWidget,說明 child.widget 前後沒有變化,若 child.slot != newSlot 表明子節點在兄弟結點間移動了位置,通過
子類一般不需要重寫該方法,該方法有點類似設計模式中的『模板方法』。
update
在更新流程中,若新老 Widget.[runtimeType && key] 相等,則會走到該方法。 子類需要重寫該方法以處理具體的更新邏輯:
Element 基類
@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}
複製程式碼
基類中的update
很簡單,只是對_widget
賦值。
子類重寫該方法時必須呼叫 super.
StatelessElement
父類
ComponentElement
沒有重寫該方法
void update(StatelessWidget newWidget) {
super.update(newWidget);
_dirty = true;
rebuild();
}
複製程式碼
通過rebuild
方法觸發重建 child widget (第 4 行),並以此來 update child element,期間會呼叫到StatelessWidget.build
方法 (也就是我們寫的 Flutter 程式碼)。
組合型 Element 都會在
update
方法中觸發rebuild
操作,以便重新 build child widget。
StatefulElement
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = _state._widget;
_dirty = true;
_state._widget = widget;
try {
_state.didUpdateWidget(oldWidget) as dynamic;
}
finally {
}
rebuild();
}
複製程式碼
相比StatelessElement
,StatefulElement.update
稍微複雜一些,需要處理State
,如:
- 修改 State 的
_widget
屬性; - 呼叫
State.didUpdateWidget
(熟悉麼)。
最後,同樣會觸發rebuild
操作,期間會呼叫到State.build
方法。
ProxyElement
void update(ProxyWidget newWidget) {
final ProxyWidget oldWidget = widget;
super.update(newWidget);
updated(oldWidget);
_dirty = true;
rebuild();
}
void updated(covariant ProxyWidget oldWidget) {
notifyClients(oldWidget);
}
Widget build() => widget.child;
複製程式碼
ProxyElement.update
方法需要關注的是對updated
的呼叫,其主要用於通知關聯物件 Widget 有更新。
具體通知邏輯在子類中處理,如:InheritedElement
會觸發所有依賴者 rebuild (對於 StatefulElement 型別的依賴者,會呼叫State.didChangeDependencies
)。
ProxyElement 的build
操作很簡單:直接返回widget.child
。
RenderObjectElement
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
複製程式碼
RenderObjectElement.update
方法呼叫了widget.updateRenderObject
來更新「Render Object」(熟悉麼)。
SingleChildRenderObjectElement
SingleChildRenderObjectElement
、MultiChildRenderObjectElement
是RenderObjectElement
的子類。
void update(SingleChildRenderObjectWidget newWidget) {
super.update(newWidget);
_child = updateChild(_child, widget.child, null);
}
複製程式碼
第 3 行,通過newWidget.child
呼叫updateChild
方法遞迴修改子節點。
MultiChildRenderObjectElement
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
_children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
}
複製程式碼
上述實現看似簡單,實則非常複雜,在updateChildren
方法中處理了子節點的插入、移動、更新、刪除等所有情況。
inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
複製程式碼
inflateWidget
屬於模板方法,故一般情況下子類不用重寫。
該方法的主要職責:通過 Widget 建立對應的 Element,並將其掛載 (mount) 到「Element Tree」上。
如果 Widget 帶有 GlobalKey,首先在 Inactive Elements 列表中查詢是否有處於 inactive 狀態的節點 (即剛從樹上移除),如找到就直接復活該節點。
主要呼叫路徑來自上面介紹的updateChild
方法。
mount
當 Element 第一次被插入「Element Tree」上時,呼叫該方法。由於此時 parent 已確定,故在該方法中可以做依賴 parent 的初始化操作。經過該方法後,element 的狀態從 "initial" 轉到了 "active"。
Element
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
if (widget.key is GlobalKey) {
final GlobalKey key = widget.key;
key._register(this);
}
_updateInheritance();
}
複製程式碼
還記得BuildOwner
嗎,正是在該方法中父節點的 owner 傳給了子節點。
如果,對應的 Widget 帶有 GlobalKey,進行相關的註冊。
最後,繼承來自父節點的「Inherited Widgets」。
子類重寫該方法時,必須呼叫 super。 關於「Inherited Widgets」,後文會詳細分析
ComponentElement
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
void _firstBuild() {
rebuild();
}
複製程式碼
組合型 Element 在掛載時會執行_firstBuild->rebuild
操作。
RenderObjectElement
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
複製程式碼
在RenderObjectElement.mount
中做的最重要的事就是通過 Widget 建立了「Render Object」(第 3 行),並將其插入到「RenderObject Tree」上 (第 4 行)。
SingleChildRenderObjectElement
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
複製程式碼
SingleChildRenderObjectElement
在 super (RenderObjectElement
) 的基礎上,呼叫updateChild
方法處理子節點,其實此時_child
為nil
,前面介紹過當 child 為nil
時,updateChild
會呼叫inflateWidget
方法建立 Element 例項。
MultiChildRenderObjectElement
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], previousChild);
_children[i] = newChild;
previousChild = newChild;
}
}
複製程式碼
MultiChildRenderObjectElement
在 super (RenderObjectElement
) 的基礎上,對每個子節點直接呼叫inflateWidget
方法。
markNeedsBuild
void markNeedsBuild() {
if (!_active)
return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
複製程式碼
markNeedsBuild
方法其實在介紹BuildOwer時已經分析過,其作用就是將當前 Element 加入_dirtyElements
中,以便在下一幀可以rebuild。
那麼,哪些場景會呼叫markNeedsBuild
呢?
State.setState
—— 這個在介紹 Widget 時已分析過了;Element.reassemble
—— debug hot reload;Element.didChangeDependencies
—— 前面介紹過當依賴的「Inherited Widget」有變化時會導致依賴者 rebuild,就是從這裡觸發的;StatefulElement.activate
—— 還記得activate
嗎?前文介紹過當 Element 從 "inactive" 到 "active" 時,會呼叫該方法。為什麼StatefulElement
要重寫activate
?因為StatefulElement
有附帶的 State,需要給它一個activate
的機會。
子類一般不必重寫該方法。
rebuild
void rebuild() {
if (!_active || !_dirty)
return;
performRebuild();
}
複製程式碼
該方法邏輯非常簡單,對於活躍的、髒節點呼叫performRebuild
,在 3 種場景下被呼叫:
- 對於 dirty element,在新一幀繪製過程中由
BuildOwner.buildScope
; - 在 element 掛載時,由
Element.mount
呼叫; - 在
update
方法內被呼叫。
上述第 2、3 點僅「Component Element」需要
performRebuild
Element 基類中該方法是no-op
。
ComponentElement
void performRebuild() {
Widget built;
built = build();
_child = updateChild(_child, built, slot);
}
複製程式碼
對於組合型 Element,rebuild 過程其實就是呼叫build
方法生成「child widget」,再由其更新「child element」。
StatelessElement.build:
Widget build() => widget.build(this);
StatefulElement.build:Widget build() => state.build(this);
ProxyElement.build:Widget build() => widget.child;
RenderObjectElement
void performRebuild() {
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
複製程式碼
在渲染型 Element 基類中只是用 Widget 更新了對應的「Render Object」。 在相關子類中可以執行更具體的邏輯。
生命週期視角
至此,Element 的核心方法基本已介紹完,是不是有點暈乎乎的感覺?inflateWidget
、updateChild
、update
、mount
、rebuild
以及performRebuild
等你中有我、我中有你,再加上不同型別的子類對這些方法的重寫。
下面,我們以 Element 生命週期為切入點將這些方法串起來。 對於一個 Element 節點來說在其生命週期內可能會歷經幾次『重大事件』:
- 被建立 —— 起源於父節點呼叫
inflateWidget
,隨之被掛載到「Element Tree」上, 此後遞迴建立子節點; - 被更新 —— 由「Element Tree」上祖先節點遞迴傳遞下來的更新操作,
parent.updateChild
->child.update
; - 被重建 —— 被呼叫
rebuild
方法(呼叫場景上面已分析); - 被銷燬 —— element 節點所在的子樹隨著 UI 的變化被移除。
依賴 (Dependencies)
在 Element 基類中有這樣兩個成員:
Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies;
複製程式碼
它們是幹嘛用的呢?
_inheritedWidgets
—— 用於收集從「Element Tree」根節點到當前節點路徑上所有的「Inherited Elements」; 前文提到過在mount
方法結束處會呼叫_updateInheritance
: 以下是 Element 基類的實現,可以看到子節點直接獲得父節點的_inheritedWidgets
:
void _updateInheritance() {
_inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼
以下是InheritedElement
類的實現,其在父節點的基礎上將自己加入到_inheritedWidgets
中,以便其子孫節點的_inheritedWidgets
包含它 (第 8 行):
void _updateInheritance() {
final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
if (incomingWidgets != null)
_inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
else
_inheritedWidgets = HashMap<Type, InheritedElement>();
_inheritedWidgets[widget.runtimeType] = this;
}
複製程式碼
_dependencies
—— 用於記錄當前節點依賴了哪些「Inherited Elements」,通常我們呼叫context.dependOnInheritedWidgetOfExactType<T>
時就會在當前節點與目標 Inherited 節點間形成依賴關係。
在 Element 上提供的便利方法
of
,一般殾會呼叫dependOnInheritedWidgetOfExactType
。
同時,在InheritedElement
中還有用於記錄所有依賴於它的節點:final Map<Element, Object> _dependents
。
最終,在「Inherited Element」發生變化,需要通知依賴者時,會利用依賴者的_dependencies
資訊做一下 (debug) check (第 4 行):
void notifyClients(InheritedWidget oldWidget) {
for (Element dependent in _dependents.keys) {
// check that it really depends on us
assert(dependent._dependencies.contains(this));
notifyDependent(oldWidget, dependent);
}
}
複製程式碼
小結
至此,Element 相關的內容基本已介紹完。總結提煉一下:
- Element 與 Widget 一一對應,它們間的關係就像 object 與 json;
- 只有「Render Element」才有對應的「Render Object」;
- Element 作為 Widget 與 RenderObejct 間協調者,會根據 UI(「Widget Tree」) 的變化對「Element Tree」作出相應的調整,同時對「RenderObject Tree」進行必要的修改;
- Widget 是不可變的、無狀態的,而 Element 是有狀態的。
最後,強烈推薦Keys! What are they good for?這篇文章,對於理解本文相關的內容有很大的幫助。