您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Flutter三棵树系列之BuildOwner
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Flutter三棵树系列之BuildOwner
自猿其说Tech
2022-04-27
IP归属:未知
621浏览
Flutter
前端
**关键词:**flutter三棵树 BuildOwner flutter渲染原理 ### 1 引言 先简单介绍下Flutter的三棵树,所谓的三棵树指的是Widget树、Element树以及RenderObject树。Widget树可以理解成为配置信息,RenderObject树负责渲染,而Eelement则是链接Widget树和RenderObject树的纽带,三棵树的相互配合,共同完成了Flutter的渲染逻辑。Flutter中一共有两个比较重要的xxxOwner类,一个是PipelineOwner,负责管理需要重新绘制的RenderObject,本文主要介绍另外一个Owner类--BuildOwner,从源码角度对其进行解析,希望能加深对三棵树渲染机制的理解。 ### 2 BuildOwner是什么? BuildOwner是element的管理类,主要负责dirtyElement、inactiveElement、globalkey关联的element的管理。 ```java final _InactiveElements _inactiveElements = _InactiveElements();//存储inactiveElement。 final List<Element> _dirtyElements = <Element>[];//存储dirtyElement,就是那些需要重建的element。 final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};//存储所有有globalKey的element。 ``` ### 3 BuildOwner在哪创建? BuildOwner是全局唯一的,当然也可以创建一个buildOwner用来管理离屏的widget。其在widgetsBinding的init方法中创建,并在runApp中的attachRootWidget方法中赋值给root element,子element在其mount方法中可以获取到parent的BuildOwner,达到全局使用唯一BuildOwner的效果。 ```java //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 } //... } ``` ### 4 dirtyElements的管理 dirty element,顾名思义,指的是那些被标记了dirty的element,需要重新build的element。当一个element被标记了dirty后,flutter并没有马上对其执行rebuild操作,而是将所有的dirty element放在一块,这样做可以大大提高整体的构建效率。 #### 4.1 添加 添加操作主要用的是BuildOwner的scheduleBuildFor方法,当你使用State类时,一个完整的链条如下: ![](//img1.jcloudcs.com/developer.jdcloud.com/b744b736-f60f-4170-a71b-565444ea3b9220220427142920.png) ```java //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 } ``` #### 4.2 处理 ![](//img1.jcloudcs.com/developer.jdcloud.com/cb94aa6e-c024-419f-b9ad-07b00885479b20220427142948.png) 真正处理的地方是在BuildOwner的buildScope方法里。framework在每次调用drawFrame时都会调用此方法重新构建dirtyElement,可以参考下WidgetsBinding的drawFrame方法,在runApp一开始启动时,也会调用此方法完成element tree的mount操作,具体可以参考RenderObjectToWidgetAdapter的attachToRenderTree方法。处理整个dirtyElements的流程可以参考上图。 ```java 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的源码。 ```java void rebuild() { if (_lifecycleState != _ElementLifecycle.active || !_dirty)//如果dirty为false,直接返回,不再执行build操作。 return; performRebuild(); } ``` 当列表中的所有element都执行完rebuild方法后,就会将其清空,并将dirtyElement的inDirtyList置为false,对应于源码的finally中的代码。 #### 4.3 问题 看4.2中所示源码标记2的地方,dirtyCount不应该等于dirtyElements.length吗?为什么会小于呢?下面详细解释下: 执行element.rebuild方法时,内部还会调用updateChild方法用来更新child,在一些场景下updateChild方法会调用inflateWidget来创建新的element(会在element里详细介绍),如果newWidget的key为GlobalKey,这个GlobalKey也有对应的element,并且Widgets.canUpdate()返回true,那么就调用其_activateWithParent方法。 ```java //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方法: ```java void activate() { //... if (_dirty) owner!.scheduleBuildFor(this); //... } ``` 看到没,如果重新捞起来的element是dirty的,那么会再次调用scheduleBuildFor方法,将此element加入到dirtyElement列表里面。这也就是为什么标记2处dirtyCount会小于dirtyElements.length的原因。此时,因为有新element加入到dirtyElement列表里,所以要重新sort。 上述代码调用流程如下图: ![](//img1.jcloudcs.com/developer.jdcloud.com/a4eda811-bc06-4b65-be33-b184ead9f6cc20220427143210.png) 总结下,buildScope方法主要是对dirtyElements列表中的每一个element执行了rebuild操作,rebuild会调用updateChild方法,当需要重新调用inflateWidget方法创建新element时,如果child使用了GlobalKey并且GlobalKey对应的element是dirty状态的,那么就会将其加入到dirtyElements列表中,从而导致dirtyElements数量的变化。 ### 5 inactiveElements的管理 inactiveElements主要用来管理非活跃状态的element,特别是可以用来处理key为GlobalKey的element的move操作。其实inactiveElements是一个对象,内部维护了一个Set以及用于debug模式下asset判断的locked属性,当然还有其他方法,类定义如下: ```java class _InactiveElements { bool _locked = false; final Set<Element> _elements = HashSet<Element>(); ..... } ``` #### 5.1 添加 在element的deactivateChild方法里完成了inactiveElement的元素添加操作。 ```java //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调用的两个重要时机: 1)updateChild方法里,介绍element时会详细介绍。 ```java Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) { if (newWidget == null) { if (child != null) deactivateChild(child); return null; } .... } ``` 2)_retakeInactiveElement方法里(inflateWidget方法里调用的),上面介绍过,主要是用于拥有GlobaleKey的element在tree上的移动操作。 #### 5.2 清空 其清空操作是在BuildOwner里的finalizeTree方法里面,此方法里会调用element的unmount方法,源码如下。 ```java //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(); } ``` 需要注意的是: 1. unmount时会将列表按着深度优先排序,也就说先unmount depth大的,再unmount depth小的。 2. 真正执行unmount操作时,也是先unmount chidlren 然后unmount自己。 3. 每次渲染完一帧后,都会调用finalizeTree方法,具体的方法是WidgetsBinding的drawFrame方法中。 ### 6 key为GloablKey的Element的管理 主要有两个方法,一个方法用于注册,一个方法用于解注册,在element的mount方法里,判断是否用的GlobalKey,如果是的话调用注册方法,在element的unmount方法里调用解注册方法。 ```java void _registerGlobalKey(GlobalKey key, Element element) { _globalKeyRegistry[key] = element; } void _unregisterGlobalKey(GlobalKey key, Element element) { if (_globalKeyRegistry[key] == element) _globalKeyRegistry.remove(key); } ``` ### 7 总结 正如引言所说,BuildOwner的设计目的就是用来管理Element,像上面介绍的dirty element、inactive element以及那些key为GlobalKey的element。从源码来看,这个类并没有多么复杂的逻辑,但是在flutter三棵树中还是扮演者挺重要的作用。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:沈明亮(京东快递App团队)
原创文章,需联系作者,授权转载
上一篇:时序时空数据库调研
下一篇:联合索引下Order By和范围匹配SQL优化介绍
相关文章
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
Flutter异步编程中Completer的使用
聊一聊多线程不得不知的Future(一)
自猿其说Tech
文章数
426
阅读量
2150414
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2150414
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号