這是我參與8月更文挑戰的第7天,活動詳情檢視:8月更文挑戰
前言
上一篇我們對比了 setState
和 ModelBinding
這兩種狀態管理的區別,從結果來看,setState
的方式的效能明顯低於 ModelBinding
這種使用 InheritedWidget
的方式。這是因為 setState
的時候,不管子元件有沒有依賴狀態資料,都會蔣全部子元件移除後重建。那麼 setState
這個過程做了什麼事情,會導致這樣的結果呢?本篇我們通過 Flutter 的原始碼來分析一下 setState
的過程。
setState 的定義
我們先來看 setState 的定義,setState 定義在State<T extends StatefulWidget> with Diagnosticable
這個類中,也就是 StatefulWidget
或其子類的狀態類。方法體程式碼不多,在執行業務程式碼做了一些異常處理,具體的程式碼我們不貼了,主要是做了如下處理:
- 傳給
setState
的回撥方法不能為空。 - 生命週期校驗:元件已經從元件樹移除的時候會被
dispose
掉,因此不能在dispose
後呼叫setState
。通常這會發生在定時器、動畫或非同步回撥的過程中。這樣的呼叫可能會導致記憶體洩露。 - 在
created
階段和沒有裝載階段(mounted
)不可以呼叫setState
,也就是不能在建構函式裡呼叫setState
。通常應該在initState
之後呼叫setState
。 - setState 的回撥方法不能返回 Future 物件,也就是不能在
setState
中執行非同步操作,只能是同步操作。如果要執行非同步操作應該咋setState
之外進行呼叫。
@protected
void setState(VoidCallback fn) {
// 省略異常處理程式碼
_element!.markNeedsBuild();
}
複製程式碼
最為關鍵的就一行程式碼:_element!.markNeedsBuild()
,從函式名稱來看就是標記元素需要構建。那麼這個_element
又是從哪來的?繼續挖!
Element 是什麼?
我們來看_element
的定義,_element
是一個 StatefulElement
物件,實際上,我們還發現,在獲取BuildContext
的時候,返回的也是_element
。在獲取 BuildContext 的時候註釋是這麼說的:
The location in the tree where this widget builds ——widget構建的渲染樹的具體位置。
BuildContext
是一個抽象類,因此可以推斷出 StatefulElement
實際上是其介面實現類或子類。往上溯源,發現整個的類層級是下面這樣的,其中 Element
、ComponentElement
都是抽象類,而 markNeedsBuild
方法是在 Element
抽象類定義的。而對於 Element,官方的定義為:
An instantiation of a Widget at a particular location in the tree. —— 在渲染樹中的 Widget 例項化物件。
可以理解為Element
是將 Widget
配置和渲染樹做橋接的物件,也就是實際的渲染過程更多的是由 Element
來控制的。
classDiagram
BuildContext <|.. Element
DiagnosticableTree <|-- Element
Element <|-- ComponentElement
ComponentElement <|-- StatefulElement
class Element {
Element(Widget widget)
+_sort(Element a, Element b)
-reassemble()
-markNeedsBuild()
-get renderObject
-updateChild(Element? child, Widget? newWidget, dynamic newSlot)
-mount(Element? parent, dynamic newSlot)
-unmount()
-update(covariant Widget newWidget)
-detachRenderObject()
-attachRenderObject(dynamic newSlot)
-deactivateChild(Element child)
-activate()
-didChangeDependencies()
-markNeedsBuild()
-rebuild()
-performRebuild()
-Element? _parent
-int _depth
-Widget _widget
-BuildOwner? _owner
_ElementLifecycle _lifecycleState
}
上面的圖我們Element的關鍵屬性和方法列出來的。
-
_depth
屬性:元素在元件樹中的層級,根節點的該值必須大於0。 -
_sort
方法:比較兩個Element
元素a和 b的層級,層級值(_depth
)越大,層級越深,顯示的層也就越靠前。 -
_parent
:父節點元素,可能為空。 -
_widget
:配置元素的元件配置(其實是Widget
物件,Widget
本身是渲染元素的配置引數,並不是真正渲染的元素)。 -
_owner
:管理元素宣告週期的物件。 -
_lifecycleState
:生命週期狀態屬性,預設是initial
狀態。 -
獲取
renderObject
的get
方法:會遞迴呼叫返回元素及其子元素中需要渲染的物件(子元素是RenderObjectElement
物件)。 -
reassemble
方法:重新裝配方法,只在debug
階段會用到,例如熱過載的時候就會呼叫該方法。該方法處理將元素自身標記為需要build
外(呼叫markNeedsBuild
方法),還會遞迴遍歷全部子節點,呼叫子節點的reassemble
方法。 -
updateChild
:這是渲染過程的核心方法,通過新的元件配置來更新指定的子元素。這裡存在四種組合:- 如果child
為空的話而newWidget
不為空,那麼就會建立一個新的元素來渲染:- 如果
child
不為空,但是newWidget
為空,那就表明元件配置中已經沒有child
這個元素了,因此需要移除它。 - 如果二者都不為空,則需要根據
child
的當前是否可以更新(Widget.canUpdate
)來處理,如果可以更新,那麼使用新的元件配置更新元素;否則我們需要移除舊的元素,並使用新的元件配置建立一個新的元素。 - 如果二者都為空,那麼什麼都不做。
- 如果
返回的結果也分三種情況:
1. 如果建立了一個新的元素,則返回新構建的子元素。
2. 如果舊的元素被更新,返回更新後的子元素。
3. 如果子元素被移除,而沒有新的替換的話,返回null。
複製程式碼
mount
方法:在新元素首次被建立的時候呼叫該方法,按照給定的插入位置(slot)將元素插入給定的父節點。呼叫該方法後,元素的狀態會從initial
改為active
。這裡還會將子元素的層級(_depth)設定為父元素的層級+1。update
方法:當父節點使用新的配置元件(newWidget
)更改元素時,會呼叫該方法。要求新的配置型別和舊的保持一致。detachRenderObject
和attachRenderObject
:分別對應從元件樹移除renderObject 和新增 RenderObject。deactivateChild
方法:將子元素加入到不活躍的元素列表,之後再從渲染樹中移除。activate
方法:狀態從inactive 切換到 active 時會呼叫,屬於生命週期函式。注意元件第一次掛載的時候不會呼叫這個方法,而是 mount 方法。deactivate
方法:狀態從 active 切換到 inactive 時會被呼叫,也就是元素被移入到不活躍列表的時候會被呼叫。。unmount
方法:狀態從 inactive 切換到defunct(不再存在)狀態時呼叫,此時元素將脫離渲染樹,並且再也不會在渲染樹存在。didChangeDependencies
:當元素的依賴發生改變的時候呼叫,該方法也會呼叫markNeedBuild
方法。markNeedsBuild
方法:將元素標記為dirty
狀態,以便在渲染下一幀時重建元素。這個方法的核心是做了下面的事情:
_dirty = true;
owner!.scheduleBuildFor(this)
複製程式碼
rebuild
方法:當元素的BuildOwner
物件呼叫scheduleBuildFor
方法的時候,會呼叫rebuild
方法來重建元素。首次裝載的時候是在mount
方法中觸發,配置元件更改時會在build
方法觸發。這個方法呼叫了performRebuild
方法來重建元素。performRebuild
是一個有 Element 的字類實現的方法,也就是每個元素具體怎麼重建由子類來決定。
內容看著很多,我們來理一下渲染的狀態流轉,這是一個元素的生命週期的狀態圖。元件會被移除出現在 deactivate
方法中,而觸發 deactivate
方法的是一個元素被移入到不活躍元素列表中。將元素移入到不活躍列表的方法是deactivateChild
,也就是父節點上的操作——當一個子元素不再屬於父元素構建的渲染樹時,就會加入到不活躍的元素列表中。
graph LR
createElement --> 初始化((initial))
初始化((initial)) --mount--> 已裝載((mounted))
已裝載((mounted)) --activate--> 活躍((active))
活躍((active)) --deactivate--> 不活躍((inactive))
不活躍((inactive))--unmount--> 不再存在((defunct))
不再存在((defunct))--> dispose
performRebuild
方法
現在我們知道在 setState 的時候,實際會呼叫 performRebuild
方法來重新構建元件樹,那麼 performRebuild
方法做了什麼事情?在 Element 中,performRebuild 方法是個空方法,需要子類去實現。因此我們去 StatefulElement 找找看,程式碼如下:
@override
void performRebuild() {
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}
複製程式碼
還得往上找,那就是 ComponentElement
了,終於找著了!
@override
void performRebuild() {
// 省略除錯的程式碼
Widget? built;
try {
// ...
built = build();
// ...
} catch (e, stack) {
// ...
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
_dirty = false;
// ...
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
// 省略異常處理
}
// 省略除錯程式碼
}
複製程式碼
這裡的關鍵在於呼叫了 build
方法和updateChild
方法。其中 通過 built = build()
獲取了最新的Widget
,由於 build 方法重新構建了元件配置,因此會呼叫對應的 Widget 的建構函式和 build 方法。然後再呼叫 updateChild
方法更新子元素。如前所述,updateChild
更新子元件有三種組合。而我們這裡_child
和 built
肯定不為空,那麼關鍵就在於 built
(Widget
物件)的 canUpdate
是否為 true
。這個方法在 Widget 類定義:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
複製程式碼
註釋說明是如果 Widget
的 key
沒有設定(一般不推薦給元件設定 key),那麼兩個元件的 runtimeType
一致就可以更新。因此,實際上大部分情況下返回的都是 true
。我們除錯更新程式碼結果也是一樣,最終走到的是Element
的 updateChild
的這個分支:
// ...
else if (hasSameSuperclass &&
Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot) updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
}
複製程式碼
由此我們可以推斷,setState
方法呼叫後確實會重新構建整個 Widget
,但是並不一定會將 Widget
配置的 Element
元素樹的每一個元素都移除,然後用新的元素替換來重新渲染一遍。實際上我們除錯的時候開啟 Flutter
的除錯工具也可以看到,實際上的Widget
對應的 Element
在點選按鈕後並沒有發生改變。
總結
雖然setState
的呼叫並沒有像 Widget
層那樣,在渲染控制層的 Element
那一層重新構建全部element
。但是,這並不代表 setState
的使用沒問題,首先,像之前篇章說的那樣,它會重新構建整個 Widget
樹,這會帶來效能損耗;其次,由於整個 Widget
樹改變了,意味著整棵樹對應的渲染層Element
物件都會執行 update
方法,雖然不一定會重新渲染,但是這整棵樹的遍歷的效能開銷也很高。因此,從效能上考慮,還是儘量不要使用 setState
——除非,這個元件真的很簡單,而且下級元件沒有或者很少。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!