Flutter三棵樹系列之BuildOwner

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/05/30/17442473.html
-Advertisement-
Play Games

Flutter開發中三棵樹的重要性不言而喻,瞭解其原理有助於我們開發出性能更優的App,此文主要從源碼角度介紹Element樹的管理類BuildOwner。 ...


引言

Flutter開發中三棵樹的重要性不言而喻,瞭解其原理有助於我們開發出性能更優的App,此文主要從源碼角度介紹Element樹的管理類BuildOwner。

是什麼?

BuildOwner是element的管理類,主要負責dirtyElement、inactiveElement、globalkey關聯的element的管理。

final _InactiveElements _inactiveElements = _InactiveElements();//存儲inactiveElement。
final List<Element> _dirtyElements = <Element>[];//存儲dirtyElement,就是那些需要重建的element。
final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};//存儲所有有globalKey的element。

在哪創建的?

BuildOwner是全局唯一的,當然也可以創建一個buildOwner用來管理離屏的widget。其在widgetsBinding的init方法中創建,併在runApp中的attachRootWidget方法中賦值給root element,子element在其mount方法中可以獲取到parent的BuildOwner,達到全局使用唯一BuildOwner的效果。

//WidgetsBinding類
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    
    _buildOwner = BuildOwner();//創建buildOwner
    buildOwner!.onBuildScheduled = _handleBuildScheduled;//賦值buildScheduled方法
    // ...
  }
}

//Element類的mount方法
void mount(Element? parent, Object? newSlot) {
    //...
    _parent = parent;
    _depth = _parent != null ? _parent!.depth + 1 : 1;
    if (parent != null) {
      //當parent為null時,這個element肯定是root element,
      //root element的buildOwner是在runApp中調用assignOwner方法賦值的。
      _owner = parent.owner;//與parent公用一個buildOwner
    }
    //...
  }

dirtyElements的管理

添加

添加操作主要用的是BuildOwner的scheduleBuildFor方法,當你使用State類時,一個完整的鏈條如下:

//StatfuleWidget的State類中調用setState方法
void setState(VoidCallback fn) {
  final Object? result = fn() as dynamic;
  _element!.markNeedsBuild();
}
​
//Element里的markNeedsBuild方法
void markNeedsBuild() {
  //如果不是活躍狀態,直接返回。
    if (_lifecycleState != _ElementLifecycle.active)
      return;
    if (dirty)
      return;
    _dirty = true;
    owner!.scheduleBuildFor(this);
  }
​
//BuildOwner里的scheduleBuildFor方法
  void scheduleBuildFor(Element element) {
    if (element._inDirtyList) {
      _dirtyElementsNeedsResorting = true;
      return;
    }
    ...
    _dirtyElements.add(element);//加入到dirtyElement列表裡
    element._inDirtyList = true;//將element的inDirtyList置為true
  }

處理

真正處理的地方是在BuilOwner的buildScope方法里。framework在每次調用drawFrame時都會調用此方法重新構建dirtyElement,可以參考下WidgetsBinding的drawFrame方法,在runApp一開始啟動時,也會調用此方法完成element tree的mount操作,具體可以參考
RenderObjectToWidgetAdapter的attachToRenderTree方法。

void buildScope(Element context, [ VoidCallback? callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  try {
    //先執行回調方法
    if (callback != null) {
      try {
        callback();
      } finally {
      }
    }
    //採用深度排序,排序的結果是parent在child的前面
    _dirtyElements.sort(Element._sort);
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      final Element element = _dirtyElements[index];
      try {
        // 依次調用element的rebuild方法,調用完rebuild方法後,
        // element的dirty屬性會被置為false
        element.rebuild();
      } catch (e, stack) {
      }
      index += 1;
      // 標記 2
      if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
        _dirtyElements.sort(Element._sort);
        dirtyCount = _dirtyElements.length;
        while (index > 0 && _dirtyElements[index - 1].dirty) {
          index -= 1;
        }
      }
    }
  } finally {
    //最後將dirtyElements清空,並將element的inDirtyList屬性置為false
    for (final Element element in _dirtyElements) {
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
  }
}

這個方法會先執行方法入參的回調,回調執行完畢後對dirty element列表根據element的depth屬性進行排序,depth越低越靠前,也就說parent肯定在child前面,然後按照這個順序依次調用element的rebuild方法。為什麼要這麼排序呢?如果是先執行child的rebuild方法,當執行其parent的rebuild方法時,內部會直接調用updateChild方法導致child重新build,並不會判斷child是否是dirty。而當parent執行完rebuild方法後,其child的dirty會被置為false,再次調用child的rebuild方法時,發現child的dirty為false,那麼就直接返回。所以這麼排序的目的是防止child多次執行build操作。下麵是rebuild的源碼。

void rebuild() {
  if (_lifecycleState != _ElementLifecycle.active || !_dirty)//如果dirty為false,直接返回,不再執行build操作。
    return;
  performRebuild();
}

當列表中的所有element都執行完rebuild方法後,就會將其清空,並將dirtyElement的inDirtyList置為false,對應於源碼的finally中的代碼。

看源碼中標記2的地方,dirtyCount不應該等於dirtyElements.length嗎?為什麼會小於呢?下麵詳細解釋下:

執行element.rebuild方法時,內部還會調用updateChild方法用來更新child,在一些場景下updateChild方法會調用inflateWidget來創建新的element(會在element里詳細介紹),如果newWidget的key為GlobalKey,這個GlobalKey也有對應的element,並且Widgets.canUpdate()返回true,那麼就調用其_activateWithParent方法。

//Element的inflateWidget方法
Element inflateWidget(Widget newWidget, Object? newSlot) {
  final Key? key = newWidget.key;
  if (key is GlobalKey) {
    //重新設置此element的位置,配合下麵的代碼完成了key為GlobalKey的element在tree上的移動操作。
    final Element? newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      //調用element的activeWithParent方法
      newChild._activateWithParent(this, newSlot);
      final Element? updatedChild = updateChild(newChild, newWidget, newSlot);
      return updatedChild!;
    }
  }
  //...
}
​
//Element的retakeInactiveElement方法
Element? _retakeInactiveElement(GlobalKey key, Widget newWidget) {
    //有對應的element
    final Element? element = key._currentElement;
    if (element == null)
      return null;
    //如果Widget.canUpdate的結果是false就直接返回null。
    if (!Widget.canUpdate(element.widget, newWidget))
      return null;
    final Element? parent = element._parent;
    //脫離和原來parent的關係,將其加入到_inactiveElements列表裡
    if (parent != null) {
      parent.forgetChild(element);
      parent.deactivateChild(element);
    }
    //將上一步加入到inactiveElements列表裡的element再從中remove掉
    owner!._inactiveElements.remove(element);
    return element;
  }
​
//Element的activateWithParent方法
void _activateWithParent(Element parent, Object? newSlot) {
    _parent = parent;
    //更新depth,保證其depth一定比parent要深,最小為parent.depth+1
    _updateDepth(_parent!.depth);
    //調用element及其child的active方法
    _activateRecursively(this);
    attachRenderObject(newSlot);
  }
​
//Element的updateDepth方法
void _updateDepth(int parentDepth) {
    final int expectedDepth = parentDepth + 1;
    if (_depth < expectedDepth) {
      _depth = expectedDepth;
      visitChildren((Element child) {
        child._updateDepth(expectedDepth);
      });
    }
  }
​
//Element的activateRecursively方法
static void _activateRecursively(Element element) {
    //調用自己的activate方法
    element.activate();
    //調用cihldren的activate方法
    element.visitChildren(_activateRecursively);
  }

最終調用到了element的activate方法:

void activate() {
  //...
  if (_dirty)
    owner!.scheduleBuildFor(this);
  //...
}

看到沒,如果重新撈起來的element是dirty的,那麼會再次調用scheduleBuildFor方法,將此element加入到dirtyElement列表裡面。這也就是為什麼標記2處dirtyCount會小於dirtyElements.length的原因。此時,因為有新element加入到dirtyElement列表裡,所以要重新sort。

總結下,buildScope方法主要是對dirtyElements列表中的每一個element執行了rebuild操作,rebuild會調用updateChild方法,當需要重新調用inflateWidget創建新element時,如果child使用了GlobalKey並且GlobalKey對應的element是dirty狀態的,那麼就會將其加入到dirtyElements列表中,導致dirtyElements數量的變化。

inactiveElements的管理

inactiveElements主要用來管理非活躍狀態的element,特別是可以用來處理key為GlobalKey的element的move操作。其實inactiveElements是一個對象,內部維護了一個Set以及用於debug模式下asset判斷的locked屬性,當然還有其他方法,類定義如下:

class _InactiveElements {
  bool _locked = false;
  final Set<Element> _elements = HashSet<Element>();
  .....
}

添加

在element的deactivateChild方法里完成了inactiveElement的元素添加操作。

//Element類
void deactivateChild(Element child) {
  child._parent = null;
  child.detachRenderObject();
  owner!._inactiveElements.add(child); // add 操作
}
​
//InactiveElements類的add方法
void add(Element element) {
    assert(!_locked);
    if (element._lifecycleState == _ElementLifecycle.active)
      _deactivateRecursively(element);//遞歸調用element的child的deactivate方法
    _elements.add(element);
  }
​
//InactiveElements類的_deactivateRecursively方法,調用element的deactive方法
static void _deactivateRecursively(Element element) {
    element.deactivate();
    element.visitChildren(_deactivateRecursively);
  }

deactiveChild調用的兩個重要時機:

  • updateChild方法里,介紹element時會詳細介紹。
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  ....
}
  • _retakeInactiveElement方法里(inflateWidget方法里調用的),上面介紹過,主要是用於擁有GlobaleKey的element在tree上的移動操作。

清空

其清空操作是在BuildOwner里的finalizeTree方法裡面,此方法里會調用element的unmount方法,源碼如下。

//BuildOwner類
void finalizeTree() {
  lockState(_inactiveElements._unmountAll);
}
​
//InactiveElement類
void _unmountAll() {
    _locked = true;//debug模式下的判斷屬性
    final List<Element> elements = _elements.toList()..sort(Element._sort);
    _elements.clear();//源list清空
    try {
      //反轉後調用unmount方法,也就是說先調用的child的unmount方法,然後調用的parent的unmount方法。
      elements.reversed.forEach(_unmount);
    } finally {
      assert(_elements.isEmpty);
      _locked = false;
    }
  }
​
//InactiveElement類
void _unmount(Element element) {
    //先unmount children,再unmount自己
    element.visitChildren((Element child) {
      _unmount(child);
    });
    element.unmount();
  }

需要註意的是:

  • unmount時會將列表按著深度優先排序,也就說先unmount depth大的,再unmount depth小的。

  • 真正執行unmount操作時,也是先unmount chidlren 然後unmount自己。

  • 每次渲染完一幀後,都會調用finalizeTree方法,具體的方法是WidgetsBinding的drawFrame方法中。

key為GloablKey的Element的管理

主要有兩個方法,一個方法用於註冊,一個方法用於解註冊,在element的mount方法里,判斷是否用的GlobalKey,如果是的話調用註冊方法,在element的unmount方法里調用解註冊方法。

void _registerGlobalKey(GlobalKey key, Element element) {
  _globalKeyRegistry[key] = element;
}
​
void _unregisterGlobalKey(GlobalKey key, Element element) {
  if (_globalKeyRegistry[key] == element)
    _globalKeyRegistry.remove(key);
}

總結

BuildOwner是全局唯一的,在WidgetsBinding的init方法中創建,內部主要用來管理dirtyElements、inactiveElements以及key為GlobalKey的element。

  • 在BuildOwner的scheduleBuildFor方法里會向dirtyElements里添加dirty element,在buildScope方法里會調用每一個dirty element的rebuild方法,執行rebuild前會對dirty elements進行按深度排序,先執行parent後執行child,目的是為了避免child的build方法被重覆執行。在繪製每一幀時(WidgetsBinding的drawFrame方法),會調用buildScope方法。

  • inactiveElements並不是一個列表,而是一個類,裡面用set集合來保存inactive狀態的element,還實現了一些此集合的操作方法,比如add操作等等。

  • 當調用element的updateChild方法時,某些場景下會調用deactiveChild方法,會將element添加到inaciveElements裡面,並調用element的deactive方法,使其變為deactive狀態;調用updateChild方法時,在某些場景下會調用inflateWidget方法用來創建新element,如果此element的key是GlobalKey,並且此key有對應的element、widget.canUpdate返回true,那麼就會將此element與原parent脫離關係(調用的是parent的forgetChild方法),並且將其從inactiveElements中remove掉,完成了在tree上的move操作。

  • 當繪製完一幀時(WidgetsBinding的drawFrame方法),會調用BuildOwner的finalizeTree方法用來清空inactiveElements,並且調用每一個inactive element的unmount方法。

  • globalKey的管理比較簡單,用一個map來記錄globalKey和element的對應關係,在element的mount方法里完成註冊操作,unmount方法里完成解註冊方法。

作者:京東物流 沈明亮

來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ![file](https://img2023.cnblogs.com/other/2685289/202305/2685289-20230530115137903-2141604303.png) > 文章摘要:用一杯星巴克的錢,自己動手2小時的時間,就可以擁有自己訓練的開源大模型,並可以根據不同的 ...
  • 今天這個《1萬7千道法律職業考試題ACCESS資料庫》集收了海量的法考題庫試題,是從法律職業考試軟體取提出來的,讓你備考通關更加高效。今天這個《1萬7千道法律職業考試題ACCESS資料庫》集收了海量的法考題庫試題,是從法律職業考試軟體取提出來的,讓你備考通關更加高效。 包含分類:1.法理學(607) ...
  • 雖然之前弄到過《1萬多公務員考試基礎知識題庫ACCESS資料庫》,但完全沒有今天這份資料庫那麼美。今天這份數據是從一款考試學習類的軟體中破解提取出來的,據數非常不錯,不但有大小分類,而且題型包含:單項選擇題(25575條)、簡答題(942條)。 題庫中有些包含圖片問答,或者選項中有含圖片,如: 本數 ...
  • 摘要:究竟是不是cpu占比高的問題導致redis超時的呢? 本文分享自華為雲社區《我又和redis超時杠上了》,作者:藍胖子的編程夢 。 背景 經過上次redis超時排查,並聯繫雲服務商解決之後,redis超時的現象好了一陣子,但是最近又有超時現象報出,但與上次不同的是,這次超時的現象發生在業務高峰 ...
  • # Rollup ROLLUP 在多維分析中是“上捲”的意思,即將數據按某種指定的粒度進行進一步聚合。 通過建表語句創建出來的表稱為 Base 表(Base Table,基表) 在 Base 表之上,我們可以創建任意多個 ROLLUP 表。這些 ROLLUP 的數據是基於 Base 表產生的,並且在 ...
  • 推理題類的數據有一些,比如《1000道邏輯推理考題ACCESS資料庫》、《近5千偵探腦筋急轉彎選擇題ACCESS資料庫》等,但是今天遇到了一份有些圖片的推理題庫,感覺非常不錯,就是記錄數少了一些,請看以下截圖,截圖包含所有欄位,所有圖片放在一個文件夾中。 分類情況如下:邏輯推理(60)、腦筋急轉彎( ...
  • 截圖下方有顯示“共有記錄數”,截圖包含了表的所有欄位列。該數據提供ACCESS資料庫文件(擴展名是MDB)以及EXCEL文件(擴展名是XLS)。 共有23710條記錄,根據AUTHOR_ID關聯AUTHORS作者表中的ID欄位 包含6567個作者,根據ID關聯QUOTES表中的AUTHOR_ID欄位 ...
  • Health Kit文檔全新升級,開發場景更清晰,聚焦你關心的問題,快來一起嘗鮮! 文檔入口請戳:[文檔入口~](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/description-000000155 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...