写点什么

Flutter 三棵树系列之 BuildOwner | 京东云技术团队

  • 2023-05-30
    北京
  • 本文字数:6085 字

    阅读完需:约 20 分钟

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 类时,一个完整的链条如下:



//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 方法里完成解注册方法。


作者:京东物流 沈明亮

来源:京东云开发者社区

发布于: 刚刚阅读数: 2
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Flutter三棵树系列之BuildOwner | 京东云技术团队_flutter_京东科技开发者_InfoQ写作社区