关于flutter:Flutter三棵树系列之BuildOwner-京东云技术团队

2次阅读

共计 9022 个字符,预计需要花费 23 分钟才能阅读完成。

引言

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 办法里实现解注册办法。

作者:京东物流 沈亮堂

起源:京东云开发者社区

正文完
 0