主頁 > 移動端開發 > Flutter三棵樹系列之BuildOwner

Flutter三棵樹系列之BuildOwner

2023-05-31 09:54:47 移動端開發

引言

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方法里完成解注冊方法,

作者:京東物流 沈明亮

來源:京東云開發者社區

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/553909.html

標籤:其他

上一篇:Health Kit檔案大變樣,一起嘗鮮!

下一篇:返回列表

標籤雲
其他(160058) Python(38189) JavaScript(25466) Java(18161) C(15234) 區塊鏈(8268) C#(7972) AI(7469) 爪哇(7425) MySQL(7219) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5344) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4580) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2434) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1977) 功能(1967) Web開發(1951) HtmlCss(1950) C++(1927) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1879) .NETCore(1862) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • Flutter三棵樹系列之BuildOwner

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

    uj5u.com 2023-05-31 09:54:47 more
  • Health Kit檔案大變樣,一起嘗鮮!

    Health Kit檔案全新升級,開發場景更清晰,聚焦你關心的問題,快來一起嘗鮮! 檔案入口請戳:[檔案入口~](https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/description-000000155 ......

    uj5u.com 2023-05-31 09:54:17 more
  • Flutter調優--深入探究MediaQuery引起界面Rebuild的原因及解決辦

    app界面逐漸復雜時,我們不得不考慮去優化界面性能。本文中介紹的例子在開發中是很常見的,如果不了解MediaQuery.of的機制,可能會引起大量使用此方法的界面發生重繪操作,造成頁面卡頓、幀率下降。我們詳細分析了背后的原始碼邏輯,介紹了解決辦法,希望能給大家的調優作業提供些許幫助。 ......

    uj5u.com 2023-05-30 08:19:24 more
  • 了不起的互聯網老男孩,在創業路上不掉隊

    “青春如同奔流的江河,一去不回來不及道別”,老男孩這首歌戳中了太多職場中年男人的心酸苦楚,面對經濟下行壓力、互聯網行業變革以及中年職場危機,互聯網人應該如何應對?如何建立和現實叫板的能力? 有2位在互聯網創業多年的開發者,經歷了從PC互聯網到移動互聯網的發展變遷,踩過不少坑,一路磕磕碰碰走到現在,放 ......

    uj5u.com 2023-05-30 08:19:00 more
  • 了不起的互聯網老男孩,在創業路上不掉隊

    “青春如同奔流的江河,一去不回來不及道別”,老男孩這首歌戳中了太多職場中年男人的心酸苦楚,面對經濟下行壓力、互聯網行業變革以及中年職場危機,互聯網人應該如何應對?如何建立和現實叫板的能力? 有2位在互聯網創業多年的開發者,經歷了從PC互聯網到移動互聯網的發展變遷,踩過不少坑,一路磕磕碰碰走到現在,放 ......

    uj5u.com 2023-05-30 08:18:34 more
  • hybrid探索與實作

    hybrid混合開發是一種離線移動應用開發方式,它結合了Web技術和原生技術,以網頁的形式嵌入到一個原生容器中。 ......

    uj5u.com 2023-05-29 10:20:26 more
  • hybrid探索與實作

    hybrid混合開發是一種離線移動應用開發方式,它結合了Web技術和原生技術,以網頁的形式嵌入到一個原生容器中。 ......

    uj5u.com 2023-05-29 10:06:11 more
  • From Java To Kotlin:空安全、擴展、函式、Lambda很詳細,這次終于

    Kotlin 是一種靜態型別的編程語言,由 JetBrains 開發。它可以編譯成 Java 位元組碼,也可以編譯成 JavaScript 代碼。Kotlin 具有現代化的語法和功能,可以與 Java 互操作,并且可以在 Android 開發中使用。
    Kotlin 的語法簡潔、易讀、易寫,具有許多現代... ......

    uj5u.com 2023-05-28 08:43:49 more
  • From Java To Kotlin:空安全、擴展、函式、Lambda很詳細,這次終于

    Kotlin 是一種靜態型別的編程語言,由 JetBrains 開發。它可以編譯成 Java 位元組碼,也可以編譯成 JavaScript 代碼。Kotlin 具有現代化的語法和功能,可以與 Java 互操作,并且可以在 Android 開發中使用。
    Kotlin 的語法簡潔、易讀、易寫,具有許多現代... ......

    uj5u.com 2023-05-28 08:42:44 more
  • Flutter熱更新技術探索

    APP發布到市場后,難免會遇到嚴重的BUG阻礙用戶使用,因此有在不發布新版本APP的情況下使用熱更新技術立即修復BUG需求。原生APP(例如:Android & IOS)的熱更新需求已經比較成熟,但Flutter技術堆疊目前還缺少類似的技術方案,因此Flutter研發團隊,也需要類似的熱更新技術。 ......

    uj5u.com 2023-05-26 15:05:50 more