Flutter入門與實戰(四十四):從原始碼分析setState 的時候到底發生了什麼?

島上碼農發表於2021-08-07

這是我參與8月更文挑戰的第7天,活動詳情檢視:8月更文挑戰

前言

上一篇我們對比了 setStateModelBinding這兩種狀態管理的區別,從結果來看,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 實際上是其介面實現類或子類。往上溯源,發現整個的類層級是下面這樣的,其中 ElementComponentElement 都是抽象類,而 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 狀態。

  • 獲取renderObjectget 方法:會遞迴呼叫返回元素及其子元素中需要渲染的物件(子元素是 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)更改元素時,會呼叫該方法。要求新的配置型別和舊的保持一致。
  • detachRenderObjectattachRenderObject:分別對應從元件樹移除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 更新子元件有三種組合。而我們這裡_childbuilt肯定不為空,那麼關鍵就在於 builtWidget 物件)的 canUpdate 是否為 true。這個方法在 Widget 類定義:

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType &&
      oldWidget.key == newWidget.key;
}
複製程式碼

註釋說明是如果 Widgetkey 沒有設定(一般不推薦給元件設定 key),那麼兩個元件的 runtimeType 一致就可以更新。因此,實際上大部分情況下返回的都是 true。我們除錯更新程式碼結果也是一樣,最終走到的是ElementupdateChild 的這個分支:

// ...
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 入門與實戰的專欄文章。

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章