Flutter三棵树系列之BuildOwner

这篇具有很好参考价值的文章主要介绍了Flutter三棵树系列之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类时,一个完整的链条如下:

Flutter三棵树系列之BuildOwner

//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.toymoban.com/news/detail-464254.html

到了这里,关于Flutter三棵树系列之BuildOwner的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Flutter系列文章-Flutter应用优化

    当涉及到优化 Flutter 应用时,考虑性能、UI 渲染和内存管理是至关重要的。在本篇文章中,我们将通过实例深入讨论这些主题,展示如何通过优化技巧改进你的 Flutter 应用。 1. 使用 const 构造函数 在构建小部件时,尽可能使用 const 构造函数来创建静态小部件。这将避免在每次

    2024年02月11日
    浏览(39)
  • Flutter系列文章-Flutter进阶2

    这一节我将再详细地为您介绍 Flutter 进阶主题,包括导航和路由、状态管理、异步处理、HTTP请求和Rest API,以及数据持久化。让我们逐个介绍这些主题。 在 Flutter 中,导航和路由是构建多页面应用的关键概念。导航是指从一个页面(或称为路由)切换到另一个页面的过程。每

    2024年02月15日
    浏览(42)
  • Flutter系列文章-Flutter UI进阶

    在本篇文章中,我们将深入学习 Flutter UI 的进阶技巧,涵盖了布局原理、动画实现、自定义绘图和效果、以及 Material 和 Cupertino 组件库的使用。通过实例演示,你将更加了解如何创建复杂、令人印象深刻的用户界面。 Row 和 Column 是常用的布局组件,但灵活地使用它们可以带来

    2024年02月13日
    浏览(35)
  • Flutter系列:Flutter常见问答(可用于面试)

    Flutter系列 Flutter常见问答 作者 : 李俊才 (jcLee95):https://blog.csdn.net/qq_28550263 邮箱 : 291148484@163.com 本文地址 :https://blog.csdn.net/qq_28550263/article/details/135604801 【简介】:本文总结了数十道 Flutter/Dart 中常见的问题,所有的问题提供了答案提示。 1. 什么是 Flutter? Flutter 是由

    2024年01月17日
    浏览(41)
  • 根据列表构造一棵树

    List allMenus = menuDao.findBy(“status”, Status.AVAILABLE.getKey(), “priority”, false); List rootMenu = new ArrayList (); if (CollectionUtils.isNotEmpty(allMenus)) { MapLong, Menu menuMap = new HashMapLong, Menu(); for (Menu menu : allMenus) { if (menu.getParentId() == null) { rootMenu.add(menu); } menuMap.put(menu.getId(), menu); } for (Menu menu :

    2024年02月08日
    浏览(30)
  • Flutter系列文章-Flutter环境搭建和Dart基础

    Flutter是Google推出的一个开源的、高性能的移动应用开发框架,可以用一套代码库开发Android和iOS应用。Dart则是Flutter所使用的编程语言。让我们来看看如何搭建Flutter开发环境,并了解Dart语言的基础知识。 1. 安装Flutter SDK 首先,访问Flutter官网下载Flutter SDK。选择适合你操作系统

    2024年02月15日
    浏览(47)
  • elementui-树形控件实现 两棵树的联动,两棵树有相同的id节点时节点的状态保持一致

    1、遇到一个情景:需要勾选用户的权限和回显用户的权限,权限分为两棵树形结构,这两颗树有一些节点的id是一样的,需要在第一个树勾选的时候,另外一棵树也勾选,不勾选的时候状态保持一致,回显也是一样的操作 2、代码 树形的代码: 树形节点点击的事件 data数据

    2024年02月02日
    浏览(39)
  • Flutter系列文章-Flutter在实际业务中的应用

    1. 跨平台开发: 在移动应用开发中,面对不同的平台(iOS和Android),我们通常需要编写两套不同的代码。而Flutter通过一套代码可以构建适用于多个平台的应用,大大提高了开发效率,降低了维护成本。 2. 混合开发: 在一些已有的原生应用中,引入Flutter可以用于开发某些特

    2024年02月11日
    浏览(43)
  • Java另一棵树的子树

    目录 1.题目描述 2.题解 思路分析 具体实现 完整代码 给你两棵二叉树  root   和  subRoot  。检验  root  中是否包含和  subRoot   具有相同结构和节点值的子树。如果存在,返回   true  ;否则,返回  false   。 二叉树   tree   的一棵子树包括  tree   的某个节点和这个节点的

    2024年02月08日
    浏览(33)
  • 力扣572:另一棵树的子树

    力扣572:另一棵树的子树 给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。 二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。 示例

    2024年02月05日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包