为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。前端
废话很少说,听故事!《纷争再起》编程
十载干戈,移动端格局渐定,壁垒分明。bash
北方草原金账王朝Javascript虽内部纷争不断,但却一直窥视中原大陆,数年来袭扰不断,现在已夺得小片领土(ReactNative)。民间盛传:大前端融合之势已现!markdown
2018年冬,Android边境小城Flutter忽然宣布立国!并对两个移动端帝国正式宣战!!短短几日,已攻下数城。app
而今天咱们要讲的故事,就发生在战火最严重的Android边陲重镇:View城。less
某日,Android View 城军事会议:ide
镇边大将军对手下谋士道:“Flutter 最近对咱们发起了数次进攻,已下数城,知己不知彼乃军家大忌!谁能给我说说这个Flutter和咱们如今的View到底有什么区别?”布局
下方谋士面面相窥,不得已终于一个谋士站了出来:“我愿意替将军前去打探一番!”性能
很多天后,谋士:“臣卧底归来,探明Flutter与咱们View城的主要区别在于编程范式和视图逻辑单元不一样”ui
将军:“先讲编程范式如何不一样?”
将军,咱们Android如今视图开发是命令式的,咱们的每个View都直接遵从将军(Developer)的指挥,例如:想要变动界面某个文案,便要指明具体TextView调用他的setText方法命令文字发生变动;
而Flutter的视图开发是声明式的,对方的将军要作的是维护一套数据集,以及设定好一套布军计划(WidgetTree),而且为Widget“绑定”数据集中的某个数据,根据这个数据来渲染。 例如当须要变动文案时,便改变数据集中的数据,而后直接触发WidgetTree的从新渲染。这样Flutter的将军再也不须要关注每个士兵,大部分的精力都用来维护核心数据便可。
若是每一次操做都消耗一点将军的精力值,又恰好有同一个数据“绑定”到了多个View或Widget上。命令式的编程须要作的事情是 命令N个View发生变动,消耗N点精力值;
声明式编程须要作的事情是 变动数据+触发WidgetTree重绘,消耗2点精力值;对精力的解放,也是Flutter能够快速招揽到那么多将军的缘由之一。
将军:”但每次数据变动,都会触发WidgetTree的重绘,消耗的资源未免也太大了吧,我如今虽然多消耗些精力,但不会存在大量对象建立的状况“。
谋士:这也是立刻要讲的第二点不一样。由于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有可能仅是update一下,也有可能会被替换,Element被替换以后,与之对应的RenderObject也就被替换了。
能够看到WidgetTree所有被替换了,但ElementTree和RenderObjectTree只替换了变化的部分。
差点忘了讲 PipelineOwner, PipelineOwner相似于Android中的ViewRootImpl,管理着真正须要绘制的View, 最后PipelineOwner会对RenderObjectTree中发生变化节点的进行flush操做,最后交给底层引擎渲染。
将军:“我大概明白了,看来保证声明式编程性能稳定的核心在于这个Element和BuildOwner。但我看这里还有两个问题,RenderObject好像少了一个节点?你画图画错了吗?还有能给我讲下他是怎么把Widget和RenderObject连接起来,以及发生变化时,BuildOwner是如何作到元素Diff的吗?”
首先,每个Widget家族的老长辈Widget赋予了全部的Widget子类三个关键的能力:保证自身惟一以及定位的Key, 建立Element的 createElement, 和 canUpdate。 canUpdate 的做用后面讲。
Widget子类里还有一批特别优秀强壮的,是在纸面上表明着有渲染能力的RenderObjectWidget,它还有一个建立 RenderObject的 createRenderObject 方法。
从这里你也看出来了,Widget、Element、RenderObject的建立关系并非线性传递的,Element和RenderObject都是Widget建立出来的,也并非每个Widget都有与之对应的RenderObjectWidget。这也解释上面图中RenderObjectTree看起来和前面的WidgetTree缺乏了一些节点。
讲第一次建立,必定要从第一个被建立出来的士兵提及。咱们都知道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的。
紧接着又执行了attachToRenderTree
,这个方法,这个方法也很神圣,建立了第一个Element和RenderObject
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) { if (element == null) { owner.lockState(() { element = createElement(); //建立rootElement element.assignOwner(owner); //绑定BuildOwner }); owner.buildScope(element, () { //子widget的初始化从这里开始 element.mount(null, null); // 初始化子Widget前,先执行rootElement的mount方法 }); } else { ... } return element; } 复制代码
咱们解释一下上面的图片,Root的建立比较简单:
attachRootWidget(app)
方法建立了Root[Widget](也就是 RenderObjectToWidgetAdapter)attachToRenderTree
方法建立了 Root[Element]mount
方法将本身挂载到父Element上,由于本身就是root了,因此没有父Element,挂空了createRenderObject
,建立了 Root[RenderObject]它的child,也就是咱们传入的app是怎么挂载父控件上的呢?
owner.buildScope
,开始执行子Tree的建立以及挂载,敲黑板!!!这中间的流程和WidgetTree的刷新流程是如出一辙的,详细流程咱们后面讲!createElement
方法建立出Child[Element]mount
方法,将本身挂载到Root[Element]上,造成一棵树widget.createRenderObject
,建立Child[RenderObject]attachRenderObject
,完成和Root[RenderObject]的连接就这样,WidgetTree、ElementTree、RenderObject建立完成,并有各自的连接关系。
将军:“我想看一下这个mount
和attachRenderObject
的过程,看下究竟是怎么挂上去的”
abstract class Element: void mount(Element parent, dynamic newSlot) { _parent = parent; //持有父Element的引用 _slot = newSlot; _depth = _parent != null ? _parent.depth + 1 : 1;//当前节点的深度 _active = true; if (parent != null) // Only assign ownership if the parent is non-null _owner = parent.owner; //每一个Element的buildOwner,都来自父类的BuildOwner ... } 复制代码
咱们先看一下Element的挂载,就是让_parent
持有父Element的引用,很简单对不对~
由于RootElement 是没有父Element的,因此参数传了null:element.mount(null, null);
还有两个值得注意的地方:
abstract class RenderObjectElement:
@override
void attachRenderObject(dynamic newSlot) {
...
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
...
}
复制代码
RenderObject与父RenderObject的挂载稍微复杂了点。经过代码咱们能够看到须要先查询一下本身的AncestorRenderObject
,这是为何呢?
还记得以前咱们讲过,每个Widget都有一个对应的Element,但Element不必定会有对应的RenderObject。因此你的父Element并不一有RenderObject,这个时候就须要向上查找。
RenderObjectElement _findAncestorRenderObjectElement() { Element ancestor = _parent; while (ancestor != null && ancestor is! RenderObjectElement) ancestor = ancestor._parent; return ancestor; } 复制代码
经过代码咱们也能够看到,find方法在向上遍历Element,直到找到RenderObjectElement,RenderObjectElement确定是有对应的RenderObject了,这个时候在进行RenderObject子父间的挂载。
经过前面的了解,咱们知道了虽然createRenderObject方法的实现是在Widget当中,但持有RenderObject引用的倒是Element。忘记啦?那咱们再看看代码:
abstract class RenderObjectElement extends Element {
...
@override
RenderObjectWidget get widget => super.widget;
@override
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;
}
复制代码
Element同时持有二者,能够说,element就是Widget 和 RenderObject的中间商,它也确实在赚差价……
这个时候Root Widget,Root Element,Root RenderObject都已经建立完成而且三者连接成功。将军您看还有什么问题吗?
将军:“Flutter内部还有中间商赚差价呢?真腐败!诶你说说他是怎么赚差价的啊?说不定我也能够学学~”
Flutter若是想要刷新界面,须要在StatefulWidget里调用setState()
方法,setState()
干啥了呢?
@protected void setState(VoidCallback fn) { ... _element.markNeedsBuild(); } 复制代码
将军咱们实际演练一下,假设Flutter派出了这么一个WidgetTree:
当对方想改变下方Text Widget的文案时,会在StatefulWidget内部调用setState((){_title="ttt"})
,以后该widget对应的element将自身标记为dirty
状态,并调用owner.scheduleBuildFor(this);
通知buildOwner进行处理。
后续StatefulWidget的build方法必定会被执行,执行后,会建立新的子Widget出来,原来的子Widget便被抛弃掉了(将军:“好好的一个对象就这么被浪费了,哎……如今的年轻人~”)。
原来的子Widget确定是没救了,但他们的Element大几率仍是有救的。
buildOwner会将全部dirty的Element添加到_dirtyElements当中,等待下一帧绘制时集中处理。
还会调用ui.window.scheduleFrame();
通知底层渲染引擎安排新的一帧处理。
这里很重要,因此用代码讲更清晰!
void buildScope(Element context, [VoidCallback callback]){
...
}
复制代码
buildScope!! 还记的吗?前面讲Root建立的时候,咱们就看到了Child的初次建立也是调用的buildScope方法!Tree的首帧建立和刷新是一套逻辑!
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(); ... } } 复制代码
为啥要排序呢?由于父Widget的build方法必然会触发子Widget的build,若是先build了子Widget,后面再build父Widget时,子Widget又要被build一次。因此这样排序以后,能够避免子Widget的重复build。
值得一提的是,遍历执行的过程当中,也有可能会有新的element被加入到_dirtyElements集合中,此时会根据dirtyElements集合的长度判断是否有新的元素进来了,若是有,就从新排序。
element的rebuild方法最终会调用performRebuild()
,而performRebuild()
不一样的Element有不一样的实现
performRebuild()不一样的Element有不一样的实现,咱们暂时只看最经常使用的两个Element:
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就是在这个地方赚差价的!
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有四种可能的状况:
2.若是child的widget和新build出来的同样(Widget复用了),就看下位置同样不,不同就更新下,同样就直接return了。Element仍是旧的Element
3.看下Widget是否能够update,Widget.canUpdate
的逻辑是判断key值和运行时类型是否相等。若是知足条件的话,就更新,并返回。
中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!
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。
@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的相对不变了吧~
在底层引擎最终回到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的管理,咱们简单介绍,详细的放在下期介绍。
@protected void drawFrame() { assert(renderView != null); pipelineOwner.flushLayout(); //布局须要被布局的RenderObject pipelineOwner.flushCompositingBits(); // 判断layer是否变化 pipelineOwner.flushPaint(); //绘制须要被绘制的RenderObject renderView.compositeFrame(); // this sends the bits to the GPU 将画好的layer传给engine绘制 pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. 一些语义场景须要 } 复制代码
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就没有咱们灵活。由于Flutter是声明式的,想要在运行中随时向WidgetTree插入一个Widget,目前尚未成熟接口。
但相信随着Flutter开发者对Flutter内部原理愈来愈熟悉,这种问题很快就会被解决的。