后续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、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
- 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(
container: renderView,
debugShortDescription: ‘[root]’,
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
我们最需要关注的是attachRootWidget(app)
这个方法,这个方法很神圣,很多的第一次就在这个方法里实现了!!(将军:“很神圣?你是不叛变了?”),app 是我们传入的自定义Widget,内部会创建RenderObjectToWidgetAdapter,并将app做为它的child的。
紧接着又执行了attachToRenderTree
,这个方法,这个方法也很神圣,创建了第一个Element和RenderObject
RenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement 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的创建比较简单:
- 1.
attachRootWidget(app)
方法创建了Root[Widget](也就是 RenderObjectToWidgetAdapter) - 2.紧接着调用
attachToRenderTree
方法创建了 Root[Element] - 3.Root[Element]尝试调用
mount
方法将自己挂载到父Element上,因为自己就是root了,所以没有父Element,挂空了 - 4.mount的过程中会调用Widget的
createRenderObject
,创建了 Root[RenderObject]
它的child,也就是我们传入的app是怎么挂载父控件上的呢?
- 5.我们将app作为参数传给了Root[Widget](也就是 RenderObjectToWidgetAdapter),app[Widget]也就成了为root[Widget]的child[Widget]
- 6.调用
owner.buildScope
,开始执行子Tree的创建以及挂载,敲黑板!!!这中间的流程和WidgetTree的刷新流程是一模一样的,详细流程我们后面讲! - 7.调用
createElement
方法创建出Child[Element] - 8.调用Element的
mount
方法,将自己挂载到Root[Element]上,形成一棵树 - 9.挂载的同时,调用
widget.createRenderObject
,创建Child[RenderObject] - 10.创建完成后,调用
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);
还有两个值得注意的地方:
- 节点的深度_depth 也是在这个时候计算的,深度对刷新很重要!先记下!
- 每个Element的buildOwner,都来自父类的BuildOwner,这样可以保证一个ElementTree,只由一个BuildOwner来维护。
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子父间的挂载。
Flutter的刷新流程:Element的复用
通过前面的了解,我们知道了虽然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:
刷新第1步:Element标记自身为dirty,并通知buildOwner处理
当对方想改变下方Text Widget的文案时,会在StatefulWidget内部调用setState((){_title="ttt"})
,之后该widget对应的element将自身标记为dirty
状态,并调用owner.scheduleBuildFor(this);
通知buildOwner进行处理。
后续StatefulWidget的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!! 还记的吗?前面讲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();
…
}
}
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()
最后
跳槽季整理面试题已经成了我多年的习惯!在这里我和身边一些朋友特意整理了一份快速进阶为Android高级工程师的系统且全面的学习资料。涵盖了Android初级——Android高级架构师进阶必备的一些学习技能。
附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)
本文在开源项目:【GitHub 】中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
O-1711024756427)]
[外链图片转存中…(img-4fxg5Xoj-1711024756427)]
[外链图片转存中…(img-MAY3KZBr-1711024756428)]
[外链图片转存中…(img-M1tQTGth-1711024756428)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-IgzTyspz-1711024756429)]