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

這是我參與8月更文挑戰的第7天,活動詳情查看:8月更文挑戰微信

前言

上一篇咱們對比了 setStateModelBinding這兩種狀態管理的區別,從結果來看,setState 的方式的性能明顯低於 ModelBinding 這種使用 InheritedWidget 的方式。這是由於 setState的時候,無論子組件有沒有依賴狀態數據,都會蔣所有子組件移除後重建。那麼 setState 這個過程作了什麼事情,會致使這樣的結果呢?本篇咱們經過 Flutter 的源碼來分析一下 setState 的過程。markdown

setState 的定義

咱們先來看 setState 的定義,setState 定義在State<T extends StatefulWidget> with Diagnosticable這個類中,也就是 StatefulWidget或其子類的狀態類。方法體代碼很少,在執行業務代碼作了一些異常處理,具體的代碼咱們不貼了,主要是作了以下處理:app

  • 傳給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 的時候註釋是這麼說的:ide

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 實例化對象。post

能夠理解爲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 入門與實戰的專欄文章。

👍🏻:以爲有收穫請點個贊鼓勵一下!

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

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

相關文章
相關標籤/搜索