内容简介:为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。废话不多说,听故事!《帝国的纷争》十载干戈,移动端格局渐定,壁垒分明。
为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。
废话不多说,听故事!《帝国的纷争》
故事
十载干戈,移动端格局渐定,壁垒分明。
北方草原金帐王朝Javascript虽内部纷争不断,但却一直窥视中原大陆,数年来袭扰不断,如今已夺得小片领土(ReactNative)。民间盛传:大前端融合之势已现!
2018年冬,Android边境小城Flutter突然宣布立国!并对两个移动端帝国正式宣战!!短短几日,已攻下数城。
而今天我们要讲的故事,就发生在战火最严重的Android边陲重镇:View城。
某日,Android View 城军事会议:
镇边大将军对手下谋士道:“Flutter 最近对我们发起了数次进攻,已下数城,知己不知彼乃军家大忌!谁能给我说说这个Flutter和我们现在的View到底有什么区别?”
下方谋士面面相窥,不得已终于一个谋士站了出来:“我愿意替将军前去打探一番!”
数日后,谋士:“臣卧底归来,探明Flutter与我们View城的主要区别在于编程范式和视图逻辑单元不同”
将军:“先讲编程范式如何不同?”
Android/Flutter 编程范式
将军,我们Android现在视图开发是命令式的,我们的每一个View都直接听从将军(Developer)的指挥,例如:想要变更界面某个文案,便要指明具体TextView调用他的setText方法命令文字发生变更;
而Flutter的视图开发是声明式的,对方的将军要做的是维护一套数据集,以及设定好一套布军计划(WidgetTree),并且为Widget“绑定”数据集中的某个数据,根据这个数据来渲染。
例如当需要变更文案时,便改变数据集中的数据,然后直接触发WidgetTree的重新渲染。这样Flutter的将军不再需要关注每一个士兵,大部分的精力都用来维护核心数据即可。
如果每一次操作都消耗一点将军的精力值,又刚好有同一个数据“绑定”到了多个View或Widget上。命令式的编程需要做的事情是 命令N个View发生变更,消耗N点精力值;
声明式编程需要做的事情是 变更数据+触发WidgetTree重绘,消耗2点精力值;对精力的解放,也是Flutter可以快速招揽到那么多将军的原因之一。
将军:”但每次数据变更,都会触发WidgetTree的重绘,消耗的资源未免也太大了吧,我现在虽然多消耗些精力,但不会存在大量对象创建的情况“。
Widget、Element、RenderObject概念
谋士:这也是马上要讲的第二点不同。因为WidgetTree会大量的重绘,所以Widget必然是廉价的。
Flutter UI有三大元素: Widget、Element、RenderObject 。对应这三者也有三个owner负责管理他们,分别是 WidgetOwner(将军&Developer)、BuildOwner、PipelineOwner 。
-
Widget,Widget 并不是真正的士兵,它只是将军手中的棋子,是一些廉价的纯对象,持有一些渲染需要的配置信息,棋子在不断被替换着。
-
RenderObject,RenderObject 是真正和我们作战的士兵,在概念上和我们Android的View一样,渲染引擎会根据RenderObject来进行真正的绘制,它是相对稳定且昂贵的。
-
Element,使得不断变化Widget转变为相对稳定的RenderObject的功臣是Element。
WidgetOwner(Developer)在不断改变着布军计划,然后向 BuildOwner 发送着一张又一张 计划表(WidgetTree) ,首次的计划表(WidgetTree)会生成一个与之对应的ElementTree,并生成对应的RenderObjectTree。
后续BuildOwner每次收到新的计划表就与上一次的进行对比,在ElementTree上只更新变化的部分,Element变化之后,与之对应的RenderObject也就更新了。
可以看到WidgetTree全部被替换了,但ElementTree和RenderObjectTree只替换了变化的部分。
差点忘了讲 PipelineOwner , PipelineOwner 类似于Android中的ViewRootImpl,管理着真正需要绘制的View,
最后PipelineOwner会对RenderObjectTree中发生变化节点的进行layout、paint、合成等等操作,最后交给底层引擎渲染。
将军:“我大概明白了,看来保证声明式编程性能稳定的核心在于这个Element和BuildOwner。但我看这里还有两个问题,RenderObject好像少了一个节点?你画图画错了吗?还有能给我讲下他是怎么把Widget和RenderObject链接起来,以及发生变化时,BuildOwner是如何做到元素Diff的吗?”
Widget、Element、RenderObject之间的关系
首先,每一个Widget家族的老长辈Widget赋予了所有的Widget子类三个关键的能力:保证自身唯一以及定位的Key, 创建Element的 createElement, 和 canUpdate。 canUpdate 的作用后面讲。
Widget子类里还有一批特别优秀强壮的,是在纸面上代表着有渲染能力的RenderObjectWidget,它还有一个创建 RenderObject的 createRenderObject 方法。
从这里你也看出来了,Widget、Element、RenderObject的创建关系并不是线性传递的, Element和RenderObject都是Widget创建出来的** ,也并不是每一个Widget都有与之对应的RenderObjectWidget**。这也解释上面图中RenderObjectTree看起来和前面的WidgetTree缺少了一些节点。
Widget、Element、RenderObject 的第一次创建与关联
讲第一次创建,一定要从第一个被创建出来的士兵说起。我们都知道Android的ViewTree:
-PhoneWindow - DecorView - TitleView - ContentView
已经预先有这么多View了,相比Android的ViewTree,Flutter的WidgetTree则要简单的多,只有最底层的root widget。
- RenderObjectToWidgetAdapter<RenderBox> - MyApp (自定义) - MyMaterialApp (自定义)
简单介绍一下RenderObjectToWidgetAdapter,不要被他的adapter名字迷惑了,RenderObjectToWidgetAdapter其实是一个RenderObjectWidget,他就是第一个优秀且强壮的Widget。
这个时候就不得不搬出代码来看了,runApp源码:
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() ..attachRootWidget(app) ..scheduleWarmUpFrame(); }
WidgetsFlutterBinding ”迷信“了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,此时Flutter 的国家引擎开始转动了!
void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget ).attachToRenderTree(buildOwner, renderViewElement); }
我们最需要关注的是 attachRootWidget(app)
这个方法,这个方法很神圣 ,很多的第一次就在这个方法里实现了!!(将军:“很神圣?你是不叛变了?”),app 是我们传入的自定义Widget,内部会创建RenderObjectToWidgetAdapter,并将app做为它的child的。
Root的创建比较简单:
attachRootWidget(app) attachToRenderTree mount createRenderObject
它的child,也就是我们传入的app是怎么挂载父控件上的呢?
createElement mount widget.createRenderObject attachRenderObject
就这样,WidgetTree、ElementTree、RenderObject创建完成,并有各自的链接关系。
通过上图我们可以看到,虽然createRenderObject方法的实现是在Widget当中,但持有RenderObject引用的却是Element。
这可也可以理解的嘛~ Widget在不停的变换,Element相对稳定并持有RenderObject,当Element都被换掉的时候,RenderObject也就该被下掉了。
另外在 attachToRenderTree
方法里我们也可以这段代码: element._newWidget = this;
。这里的this是Widget自身。element也持有了widget的引用。 element就是Widget 和RenderObject的中间商,它也确实在赚差价……
这个时候Root Widget,Root Element,Root RenderObject都已经创建完成并且三者链接成功。将军您看还有什么问题吗?
将军:“Flutter内部还有中间商赚差价呢?真腐败!你刚才说第6不已经执行了一次刷新流程是什么意思?”
子Widget的首次初始化和后续发生变化时Element tree的变化流程是一样的,我们马上就讲!
Flutter的刷新流程:Element的复用
attachRootWidget -> attachToRenderTree RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { if (element == null) { ... } else { element._newWidget = this; element.markNeedsBuild(); } return element; }
我们会看到 attachToRenderTree
方法最后还调用了 element.markNeedsBuild();
敲黑板!记住这句话,很重要!(将军:“你还给我敲黑板呢?”)
Flutter如果想要刷新界面,需要在StatefulWidget里调用 setState()
方法, setState()
干啥了呢?
@protected
void setState(VoidCallback fn) {
…
_element.markNeedsBuild();
}
哈哈哈~是不是和 attachToRenderTree
中最后调用的方法一样!
继续这样追着代码看,我怕将军您睡着了,所以我们后面我竟可能用少的源码去进行讲解!
将军我们实际演练一下,假设Flutter派出了这么一个WidgetTree:
刷新第1步:Element标记自身为dirty,并通知buildOwner处理
当对方想改变下方Text Widget的文案时,会在MyStatefulWidget内部调用 setState((){_title="ttt"})
(点击事件没写,暂时先靠想象),之后该widget对应的element将自身标记为 dirty
状态,并调用 owner.scheduleBuildFor(this);
通知buildOwner进行处理。
后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了(将军:“好好的一个对象就这么被浪费了,哎……现在的年轻人~”)。
原来的子Widget肯定是没救了,但他们的Element大概率还是有救的。
刷新第2步:buildOwner将element添加到集合_dirtyElements中,并通知ui.window安排新的一帧
buildOwner会将所有dirty的Element添加到_dirtyElements当中,等待下一帧绘制时集中处理。
还会调用 ui.window.scheduleFrame();
通知底层渲染引擎安排新的一帧处理。
刷新第3步:底层引擎最终回到Dart层,并执行buildOwner的buildScope方法
这里很重要,所以用代码讲更清晰!
void buildScope(Element context, [VoidCallback callback]){...}
buildScope需要传入一个Element的参数,通过字面意思我们应该能理解,大概就是对这个Element以下(包含)的范围rebuild。
void buildScope(Element context, [VoidCallback callback]) { ... try { ... //1.排序 _dirtyElements.sort(Element._sort); ... int dirtyCount = _dirtyElements.length; int index = 0; while (index < dirtyCount) { try { //2.遍历rebuild _dirtyElements[index].rebuild(); } catch (e, stack) { } index += 1; } } finally { for (Element element in _dirtyElements) { element._inDirtyList = false; } //3.清空 _dirtyElements.clear(); ... } }
3.1步:按照Element的深度从小到大,对_dirtyElements进行排序
为啥要 排序 呢?因为父Widget的build方法必然会触发子Widget的build,如果先build了子Widget,后面再build父Widget时,子Widget又要被build一次。所以这样排序之后,可以避免子Widget的重复build。
3.2步:遍历执行_dirtyElements当中element的rebuild方法
值得一提的是,遍历执行的过程中,也有可能会有新的element被加入到_dirtyElements集合中,此时会根据dirtyElements集合的长度判断是否有新的元素进来了,如果有,就重新排序。
element的rebuild方法最终会调用 performRebuild()
,而 performRebuild()
不同的Element有不同的实现
3.3步:遍历结束之后,清空dirtyElements集合
刷新第4步:执行performRebuild()
performRebuild()不同的Element有不同的实现,我们暂时只看最常用的两个Element:
- ComponentElement,是StatefulWidget和StatelessElement的父类
- RenderObjectElement, 是有渲染功能的Element的父类
ComponentElement的performRebuild()
void performRebuild() { Widget built; try { built = build(); } ... try { _child = updateChild(_child, built, slot); } ... }
执行element的 build();
,以StatefulElement的build方法为例: Widget build() => state.build(this);
。 就是执行了我们复写的StatefulWidget的state的build方法啦~
执行build方法build出来的是啥呢? 当然就是这个StatefulWidget的子Widget了。重点来了!敲黑板!!(将军:“又给我敲黑板??”)Element就是在这个地方赚差价的!
_child = updateChild(_child, built, slot);
Element updateChild(Element child, Widget newWidget, dynamic newSlot) { ... //1 if (newWidget == null) { if (child != null) deactivateChild(child); return null; } if (child != null) { //2 if (child.widget == newWidget) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); return child; } //3 if (Widget.canUpdate(child.widget, newWidget)) { if (child.slot != newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); return child; } deactivateChild(child); } //4 return inflateWidget(newWidget, newSlot); }
参数child 是上一次Element挂载的child Element, newWidget 是刚刚build出来的。updateChild有四种可能的情况:
- 1.如果刚build出来的widget等于null,说明这个控件被删除了,child Element可以被删除了。
- 2.如果child的widget和新build出来的一样(Widget复用了),就看下位置一样不,不一样就更新下,一样就直接return了。Element还是旧的Element
- 3.看下Widget是否可以update,
Widget.canUpdate
的逻辑是 判断key值和运行时类型 是否相等。如果满足条件的话,就更新,并返回。
中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!
- 4.如果上述三个条件都没有满足的话,就调用
inflateWidget()
创建新的Element
这里再看下 inflateWidget()
方法:
Element inflateWidget(Widget newWidget, dynamic newSlot) { final Key key = newWidget.key; if (key is GlobalKey) { final Element newChild = _retakeInactiveElement(key, newWidget); if (newChild != null) { newChild._activateWithParent(this, newSlot); final Element updatedChild = updateChild(newChild, newWidget, newSlot); return updatedChild; } } final Element newChild = newWidget.createElement(); newChild.mount(this, newSlot); return newChild; }
首先会尝试通过GlobalKey去查找可复用的Element,复用失败就调用Widget的方法创建新的Element,然后调用mount方法,将自己挂载到父Element上去,mount之前我们也讲过,会在这个方法里创建新的RenderObject。
RenderObjectElement的performRebuild()
@override void performRebuild() { widget.updateRenderObject(this, renderObject); _dirty = false; }
与ComponentElement的不同之处在于,没有去build,而是调用了 updateRenderObject
方法更新RenderObject。
不同Widget也有不同的updateRenderObject实现,我们看一下最常用的RichText,也就是Text。
void updateRenderObject(BuildContext context, RenderParagraph renderObject) { assert(textDirection != null || debugCheckHasDirectionality(context)); renderObject ..text = text ..textAlign = textAlign ..textDirection = textDirection ?? Directionality.of(context) ..softWrap = softWrap ..overflow = overflow ..textScaleFactor = textScaleFactor ..maxLines = maxLines ..locale = locale ?? Localizations.localeOf(context, nullOk: true); }
一些看起来比较熟悉的赋值操作,像不像Android的view呀? 要不怎么说RenderObject实际相当于Android里的View呢。
到这里你基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了吧~
Flutter的刷新流程:PipelineOwner对RenderObject的管理
在底层引擎最终回到Dart层,最终会执行 WidgetsBinding 的drawFrame ()
WidgetsBinding
void drawFrame() { try { if (renderViewElement != null) buildOwner.buildScope(renderViewElement); super.drawFrame(); buildOwner.finalizeTree(); } finally { } ... }
buildOwner.buildScope(renderViewElement);
就是我们上面讲过的。
下面看一下 super.drawFrame();
主要是PipelineOwner对RenderObject的管理我们暂时放在下期介绍。
Flutter的刷新流程:清理
drawFrame方法在最后执行了 buildOwner.finalizeTree();
void finalizeTree() { Timeline.startSync('Finalize tree', arguments: timelineWhitelistArguments); try { lockState(() { _inactiveElements._unmountAll(); // this unregisters the GlobalKeys }); ... } catch (e, stack) { _debugReportException('while finalizing the widget tree', e, stack); } finally { Timeline.finishSync(); } }
在做最后的清理工作。
将军:“_inactiveElements”又是个啥?之前咋没见过?
还记的前面讲Element赚差价的updateChild方法吗?所有没用的element都调用了 deactivateChild
方法进行回收:
void deactivateChild(Element child) { child._parent = null; child.detachRenderObject(); owner._inactiveElements.add(child); // this eventually calls child.deactivate() }
也就在这里将被废弃的element添加到了_inactiveElements当中。
另外在废弃element之后,调用 inflateWidget
创建新的element时,还调用了 _retakeInactiveElement
尝试通过GlobalKey复用element,此时的复用池也是在_inactiveElements当中。
从这里也能了解到,如果你没有在一帧里通过GlobeKey完成Element的复用,_inactiveElements在最后将被清空,就没办法在复用了。
结尾
将军,现在您对Flutter的绘制流程有了初步的了解了吗?
将军:“有些了解了,但你讲了这么多,对比起来我们Android,听起来Flutter这一套绘制流程没啥缺点? ”
当然有了,我们现在也只了解了Flutter的冰山一角,很多东西还没有发现。
但就只说动态向ViewTree中插入组件这一条,Flutter就没有我们灵活。比如Toast,现在的Toast组件都只能依赖methodChannel去调用我们原生的Toast。而Flutter提供的Scaffold组件,也是通过预先埋好的组件坑位,才实现了Material风格的Toast。
因为Flutter是声明式的,想要在运行中随时向WidgetTree插入一个Widget,目前还没有成熟接口。
但相信随着Flutter开发者对Flutter内部原理越来越熟悉,这种问题很快就会被解决的。
以上所述就是小编给大家介绍的《帝国的纷争-Flutter-UI绘制解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ViewGroup 默认顺序绘制子 View,如何修改?什么场景需要修改绘制顺序?
- Shader 绘制基础图形
- css绘制特殊图形
- View 绘制流程分析
- CSS图形绘制总结
- Flutter绘制弯曲虚线
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。