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方法里完成解注册方法。
作者:京东物流 沈明亮
来源:京东云开发者社区
Flutter三棵树系列之BuildOwner的更多相关文章
- 《Flutter 动画系列一》25种动画组件超全总结
动画运行的原理 任何程序的动画原理都是一样的,即:视觉暂留,视觉暂留又叫视觉暂停,人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称&q ...
- 《Flutter 动画系列》组合动画
老孟导读:在前面的文章中介绍了 <Flutter 动画系列>25种动画组件超全总结 http://laomengit.com/flutter/module/animated_1/ < ...
- Flutter基础系列之入门(一)
1.Flutter是什么? 官方介绍:Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面. Flutter可以与现有的代码一起工作.在全世界,Flutter ...
- Flutter基础系列之混合开发(二)
1.混合开发的场景 1.1作为独立页面加入 这是以页面级作为独立的模块加入,而不是页面的某个元素. 原生页面可以打开Flutter页面 Flutter页面可以打开原生页面 1.2作为页面的一部分嵌入 ...
- Flutter 即学即用系列博客总结篇
前言 迟到的总结篇,其实大家看我之前发的系列博客最后一篇,发文时间是 3 月 29 日.距离现在快两个月了. 主要是因为有很多事情在忙,所以这篇就耽搁了. 今天终于可以跟大家会面了. 系列博客背景 F ...
- 【Flutter 实战】17篇动画系列文章带你走进自定义动画
老孟导读:Flutter 动画系列文章分为三部分:基础原理和核心概念.系统动画组件.8篇自定义动画案例,共17篇. 动画核心概念 在开发App的过程中,自定义动画必不可少,Flutter 中想要自定义 ...
- Flutter 异常处理之图片篇
背景 说到异常处理,你可能直接会认为不就是 try-catch 的事情,至于写一篇文章单独来说明吗? 如果你是这么想的,那么本篇说不定会给你惊喜哦~ 而且本篇聚焦在图片的异常处理. 场景 学以致用,有 ...
- Flutter 快捷开发 Mac Android Studio 篇
老孟导读:此快捷方式适用于Mac下的 Android Studio .Windows 下的快捷方式请参考这篇文章:https://juejin.im/post/5efe71365188252e7d7f ...
- 【Flutter 实战】一文学会20多个动画组件
老孟导读:此篇文章是 Flutter 动画系列文章第三篇,后续还有动画序列.过度动画.转场动画.自定义动画等. Flutter 系统提供了20多个动画组件,只要你把前面[动画核心](文末有链接)的文章 ...
- 【Flutter 实战】动画序列、共享动画、路由动画
老孟导读:此篇文章是 Flutter 动画系列文章第四篇,本文介绍动画序列.共享动画.路由动画. 动画序列 Flutter中组合动画使用Interval,Interval继承自Curve,用法如下: ...
随机推荐
- 4.0 SDK Workshop 纪实:一起体验多人、多屏幕共享新功能
在本月初,声网发布了 RTC Native SDK 4.0 版本.该版本提供了更高的开发灵活度,可明显提升实时场景开发效率,并让第三方插件开发更容易.上周六(8月20日),我们组织了一场小型的线下 W ...
- Rancher 系列文章-K3s Traefik MiddleWare 报错-Failed to create middleware keys
概述 书接上回:<Rancher 系列文章-K3S 集群升级>, 我们提到:通过一键脚本升级 K3S 集群有报错. 接下来开始进行 Traefik 报错的分析和修复, 问题是: 所有 Tr ...
- StringBuilder 导致堆内存溢出
StringBuilder 导致堆内存溢出 原始问题描述: Exception in thread "main" java.lang.OutOfMemoryError: Java ...
- 基于Label studio实现UIE信息抽取智能标注方案,提升标注效率!
基于Label studio实现UIE信息抽取智能标注方案,提升标注效率! 项目链接见文末 人工标注的缺点主要有以下几点: 产能低:人工标注需要大量的人力物力投入,且标注速度慢,产能低,无法满足大规模 ...
- HTTP协议初见
HTTP协议 四大特性 基于请求端响应 客户端发送请求,服务端才响应,服务端不会主动给客户端发送响应. 基于TCP/IP作用于应用层之上的协议 此协议属于应用层 无状态 服务端不会保存客户 ...
- [网络/Linux]处理安全报告/安全漏洞的一般流程与思路
对近期工作中所经历的4次处理第三方网络安全公司的安全报告及其安全漏洞的经验做一点小结. 1 流程 Stage1 阅读/整理/分类:安全漏洞报告的安全漏洞 (目的:快速了解漏洞规模和分布) Stage2 ...
- 3.载荷和结果实体类以及Jwt
1.昨天为了将两个项目推送到远程仓库,了解了一下分支,将一个小工程作为一个分支,这是发生在git add .,git commit -m "描述"以及git reomte add ...
- DG修复:清理归档配置归档清理脚本
问题描述:DG同步断了十天,发现FRA归档盘符满了.需要清理下,重新增量恢复DG Error 12528 received logging on to the standby FAL[client, ...
- 随手记:linux校准时间
记录一下校准时间操作的执行步骤: 首先使用 date 查看当前时间是否准确 校准时间命令 ntpdate cn.pool.ntp.org 如果没有权限: sudo -i 会出现输入密码,直接输入密码即 ...
- JUC(七)分支合并框架
JUC分支合并框架 简介 Fork/Join可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务的结果合并称为最终的计算结果. Fork:负责将任务拆分 Join:合并拆分任务 ForkJoi ...