纷争再起:Flutter-UI绘制解析,Android开发谈

后续BuildOwner每次收到新的计划表就与上一次的进行对比,在ElementTree上只更新变化的部分,Element有可能仅是update一下,也有可能会被替换,Element被替换之后,与之对应的RenderObject也就被替换了。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以看到WidgetTree全部被替换了,但ElementTree和RenderObjectTree只替换了变化的部分。

差点忘了讲 PipelineOwnerPipelineOwner类似于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创建完成,并有各自的链接关系。

将军:“我想看一下这个mountattachRenderObject的过程,看下到底是怎么挂上去的”

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移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

O-1711024756427)]
[外链图片转存中…(img-4fxg5Xoj-1711024756427)]
[外链图片转存中…(img-MAY3KZBr-1711024756428)]
[外链图片转存中…(img-M1tQTGth-1711024756428)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-IgzTyspz-1711024756429)]

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值