美文网首页Android干货集flutter
帝国的纷争-Flutter-UI绘制解析

帝国的纷争-Flutter-UI绘制解析

作者: 十年开发程序员 | 来源:发表于2019-03-21 17:15 被阅读0次

    前言

    为避免传统的源码讲解方式的枯燥乏味,这一次,我尝试换一种方式,带着你以轻松的心态了解Flutter世界里的UI绘制流程,去探究Widget、Element、RenderObject的秘密。

    废话不多说,听故事!《帝国的纷争》

    故事

    十载干戈,移动端格局渐定,壁垒分明。

    北方草原金帐王朝Javascript虽内部纷争不断,但却一直窥视中原大陆,数年来袭扰不断,如今已夺得小片领土(ReactNative)。民间盛传:大前端融合之势已现!

    2018年冬,Android边境小城Flutter突然宣布立国!并对两个移动端帝国正式宣战!!短短几日,已攻下数城。

    image

    而今天我们要讲的故事,就发生在战火最严重的Android边陲重镇:View城。

    某日,Android View 城军事会议:

    镇边大将军对手下谋士道:“Flutter 最近对我们发起了数次进攻,已下数城,知己不知彼乃军家大忌!谁能给我说说这个Flutter和我们现在的View到底有什么区别?”

    下方谋士面面相窥,不得已终于一个谋士站了出来:“我愿意替将军前去打探一番!”

    数日后,谋士:“臣卧底归来,探明Flutter与我们View城的主要区别在于编程范式和视图逻辑单元不同”

    将军:“先讲编程范式如何不同?”

    Android/Flutter 编程范式

    将军,我们Android现在视图开发是命令式的,我们的每一个View都直接听从将军(Developer)的指挥,例如:想要变更界面某个文案,便要指明具体TextView调用他的setText方法命令文字发生变更;

    image

    而Flutter的视图开发是声明式的,对方的将军要做的是维护一套数据集,以及设定好一套布军计划(WidgetTree),并且为Widget“绑定”数据集中的某个数据,根据这个数据来渲染。
    例如当需要变更文案时,便改变数据集中的数据,然后直接触发WidgetTree的重新渲染。这样Flutter的将军不再需要关注每一个士兵,大部分的精力都用来维护核心数据即可。

    image

    如果每一次操作都消耗一点将军的精力值,又刚好有同一个数据“绑定”到了多个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。

    image

    后续BuildOwner每次收到新的计划表就与上一次的进行对比,在ElementTree上只更新变化的部分,Element变化之后,与之对应的RenderObject也就更新了。

    image

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

    差点忘了讲 PipelineOwnerPipelineOwner类似于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 方法。

    image

    从这里你也看出来了,Widget、Element、RenderObject的创建关系并不是线性传递的,Element和RenderObject都是Widget创建出来的****,也并不是每一个Widget都有与之对应的RenderObjectWidget。这也解释上面图中RenderObjectTree看起来和前面的WidgetTree缺少了一些节点。

    Widget、Element、RenderObject 的第一次创建与关联

    讲第一次创建,一定要从第一个被创建出来的士兵说起。我们都知道Android的ViewTree:

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">-PhoneWindow

    • DecorView
    • TitleView
    • ContentView
      </pre>

    |

    已经预先有这么多View了,相比Android的ViewTree,Flutter的WidgetTree则要简单的多,只有最底层的root widget。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">- RenderObjectToWidgetAdapter<RenderBox>

    • MyApp (自定义)
    • MyMaterialApp (自定义)
      </pre>

    |

    简单介绍一下RenderObjectToWidgetAdapter,不要被他的adapter名字迷惑了,RenderObjectToWidgetAdapter其实是一个RenderObjectWidget,他就是第一个优秀且强壮的Widget。

    这个时候就不得不搬出代码来看了,runApp源码:

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void runApp(Widget app) {
    WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
    }
    </pre>

    |

    WidgetsFlutterBinding ”迷信“了一系列的Binding,这些Binding持有了我们上面说的一些owner,比如BuildOwner,PipelineOwner,所以随着WidgetsFlutterBinding的初始化,其他的Binding也被初始化了,此时Flutter 的国家引擎开始转动了!

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget
    ).attachToRenderTree(buildOwner, renderViewElement);
    }
    </pre>

    |

    我们最需要关注的是attachRootWidget(app)这个方法,这个方法很神圣,很多的第一次就在这个方法里实现了!!(将军:“很神圣?你是不叛变了?”),app 是我们传入的自定义Widget,内部会创建RenderObjectToWidgetAdapter,并将app做为它的child的。

    紧接着又执行了attachToRenderTree,这个方法,这个方法也很神圣,创建了第一个Element和RenderObject

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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;
    }
    </pre>

    |

    image

    我们解释一下上面的图片,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),appp[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的过程,看下到底是怎么挂上去的”

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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
    ...
    }
    </pre>

    |

    我们先看一下Element的挂载,就是让_parent持有父Element的引用,很简单对不对~

    因为RootElement 是没有父Element的,所以参数传了null:element.mount(null, null);

    还有两个值得注意的地方:

    • 节点的深度_depth 也是在这个时候计算的,深度对刷新很重要!先记下!
    • 每个Element的buildOwner,都来自父类的BuildOwner,这样可以保证一个ElementTree,只由一个BuildOwner来维护。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">abstract class RenderObjectElement:

    @override
    void attachRenderObject(dynamic newSlot) {
    ...
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
    ...
    }
    </pre>

    |

    RenderObject与父RenderObject的挂载稍微复杂了点。通过代码我们可以看到需要先查询一下自己的AncestorRenderObject,这是为什么呢?

    还记得之前我们讲过,每一个Widget都有一个对应的Element,但Element不一定会有对应的RenderObject。所以你的父Element并不一有RenderObject,这个时候就需要向上查找。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">RenderObjectElement _findAncestorRenderObjectElement() {
    Element ancestor = _parent;
    while (ancestor != null && ancestor is! RenderObjectElement)
    ancestor = ancestor._parent;
    return ancestor;
    }
    </pre>

    |

    通过代码我们也可以看到,find方法在向上遍历Element,直到找到RenderObjectElement,RenderObjectElement肯定是有对应的RenderObject了,这个时候在进行RenderObject子父间的挂载。

    Flutter的刷新流程:Element的复用

    通过前面的了解,我们知道了虽然createRenderObject方法的实现是在Widget当中,但持有RenderObject引用的却是Element。忘记啦?那我们再看看代码:

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">abstract class RenderObjectElement extends Element {
    ...

    @override
    RenderObjectWidget get widget => super.widget;

    @override
    RenderObject get renderObject => _renderObject;
    RenderObject _renderObject;
    }
    </pre>

    |

    Element同时持有两者,可以说,element就是Widget 和 RenderObject的中间商,它也确实在赚差价……

    image

    这个时候Root Widget,Root Element,Root RenderObject都已经创建完成并且三者链接成功。将军您看还有什么问题吗?

    将军:“Flutter内部还有中间商赚差价呢?真腐败!诶你说说他是怎么赚差价的啊?说不定我也可以学学~”

    Flutter如果想要刷新界面,需要在StatefulWidget里调用setState()方法,setState()干啥了呢?

    @protected
    void setState(VoidCallback fn) {

    _element.markNeedsBuild();
    }

    将军我们实际演练一下,假设Flutter派出了这么一个WidgetTree:

    image

    刷新第1步:Element标记自身为dirty,并通知buildOwner处理

    当对方想改变下方Text Widget的文案时,会在MyStatefulWidget内部调用setState((){_title="ttt"}) ,之后该widget对应的element将自身标记为dirty状态,并调用owner.scheduleBuildFor(this);通知buildOwner进行处理。

    image

    后续MyStatefulWidget的build方法一定会被执行,执行后,会创建新的子Widget出来,原来的子Widget便被抛弃掉了(将军:“好好的一个对象就这么被浪费了,哎……现在的年轻人~”)。

    原来的子Widget肯定是没救了,但他们的Element大概率还是有救的。

    刷新第2步:buildOwner将element添加到集合_dirtyElements中,并通知ui.window安排新的一帧

    buildOwner会将所有dirty的Element添加到_dirtyElements当中,等待下一帧绘制时集中处理。

    还会调用ui.window.scheduleFrame();通知底层渲染引擎安排新的一帧处理。

    image

    刷新第3步:底层引擎最终回到Dart层,并执行buildOwner的buildScope方法

    这里很重要,所以用代码讲更清晰!

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void buildScope(Element context, [VoidCallback callback]){
    ...
    }
    </pre>

    |

    buildScope!! 还记的吗?前面讲Root创建的时候,我们就看到了Child的初次创建也是调用的buildScope方法!Tree的首帧创建和刷新是一套逻辑!

    buildScope需要传入一个Element的参数,这个方法通过字面意思我们应该能理解,大概就是对这个Element以下(包含)的范围rebuild。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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();
    ...
    }
    }
    </pre>

    |

    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()

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void performRebuild() {
    Widget built;
    try {
    built = build();
    }
    ...
    try {
    _child = updateChild(_child, built, slot);
    }
    ...
    }
    </pre>

    |

    执行element的build();,以StatefulElement的build方法为例:Widget build() => state.build(this);。 就是执行了我们复写的StatefulWidget的state的build方法啦~

    执行build方法build出来的是啥呢? 当然就是这个StatefulWidget的子Widget了。重点来了!敲黑板!!(将军:“又给我敲黑板??”)Element就是在这个地方赚差价的!

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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);
    }
    </pre>

    |

    参数child 是上一次Element挂载的child Element, newWidget 是刚刚build出来的。updateChild有四种可能的情况:

    • 1.如果刚build出来的widget等于null,说明这个控件被删除了,child Element可以被删除了。

    image
    • 2.如果child的widget和新build出来的一样(Widget复用了),就看下位置一样不,不一样就更新下,一样就直接return了。Element还是旧的Element

    image
    • 3.看下Widget是否可以update,Widget.canUpdate的逻辑是判断key值和运行时类型是否相等。如果满足条件的话,就更新,并返回。

    image

    中间商的差价哪来的呢?只要新build出来的Widget和上一次的类型和Key值相同,Element就会被复用!由此也就保证了虽然Widget在不停的新建,但只要不发生大的变化,那Element是相对稳定的,也就保证了RenderObject是稳定的!

    • 4.如果上述三个条件都没有满足的话,就调用 inflateWidget() 创建新的Element

    这里再看下inflateWidget()方法:

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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;
    }
    </pre>

    |

    首先会尝试通过GlobalKey去查找可复用的Element,复用失败就调用Widget的方法创建新的Element,然后调用mount方法,将自己挂载到父Element上去,mount之前我们也讲过,会在这个方法里创建新的RenderObject。

    image
    RenderObjectElement的performRebuild()

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">@override
    void performRebuild() {
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
    }
    </pre>

    |

    与ComponentElement的不同之处在于,没有去build,而是调用了updateRenderObject方法更新RenderObject。

    不同Widget也有不同的updateRenderObject实现,我们看一下最常用的RichText,也就是Text。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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);
    }
    </pre>

    |

    一些看起来比较熟悉的赋值操作,像不像Android的view呀? 要不怎么说RenderObject实际相当于Android里的View呢。

    到这里你基本就明白了Element是如何在中间应对Widget的多变,保障RenderObject的相对不变了吧~

    Flutter的刷新流程:PipelineOwner对RenderObject的管理

    在底层引擎最终回到Dart层,最终会执行WidgetsBinding 的drawFrame ()

    WidgetsBinding

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void drawFrame() {
    try {
    if (renderViewElement != null)
    buildOwner.buildScope(renderViewElement);
    super.drawFrame();
    buildOwner.finalizeTree();
    } finally {
    }
    ...
    }
    </pre>

    |

    buildOwner.buildScope(renderViewElement);就是我们上面讲过的。

    下面看一下super.drawFrame(); 主要是PipelineOwner对RenderObject的管理,我们简单介绍,详细的放在下期介绍。

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">@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. 一些语义场景需要
    }
    </pre>

    |

    Flutter的刷新流程:清理

    drawFrame方法在最后执行了buildOwner.finalizeTree();

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">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();
    }
    }
    </pre>

    |

    在做最后的清理工作。

    将军:“_inactiveElements”又是个啥?之前咋没见过?

    还记的前面讲Element赚差价的updateChild方法吗?所有没用的element都调用了deactivateChild方法进行回收:

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px 20px 1px 1px; color: rgb(102, 102, 102); background: rgb(247, 247, 247); line-height: 1.6; border: none; text-align: right;">1
    2
    3
    4
    5
    </pre>

    |

    <pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 13px; margin: 0px; padding: 1px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none;">void deactivateChild(Element child) {
    child._parent = null;
    child.detachRenderObject();
    owner._inactiveElements.add(child); // this eventually calls child.deactivate()
    }
    </pre>

    |

    也就在这里将被废弃的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内部原理越来越熟悉,这种问题很快就会被解决的。

    <footer class="post-footer" style="display: block; color: rgb(85, 85, 85); font-family: Lato, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

    #Flutter

    </footer>
    小编有准备一些安卓高级技能的学习资料。进群领取
    QQ群:4112676

    相关文章

      网友评论

        本文标题:帝国的纷争-Flutter-UI绘制解析

        本文链接:https://www.haomeiwen.com/subject/nkucvqtx.html