夹层蛋糕一样的Flutter

英文地址:The Layer Cake

Flutter 是一个极棒的UI框架,能够让你愉悦地以光速构建优美的用户界面。仅仅写下几行代码,点击保存[神奇地插入进去,亚秒级别的热重载]。你的应用能运行的十分流畅,能达到120fps的速度。然而,你是否询问过自己为何Flutter如此之快?它的秘方是什么?或者Flutter实际上是如何工作的?那么,别再去找其他的了!饮一杯新鲜的茶或咖啡,继续往下阅读吧。

也许你可能听说过Flutter全部都是关于组件的。你的应用是一个组件,一个text是一个组件,包裹组件的padding也是一个组件,你甚至可以通过一个小部件识别手势。好吧,这不是全部的事实,假若我告诉你widgets让Flutter的开发又快又简单,但是你也可以甚至不需要使用一个小部件来构建一个完整的Flutter应用程序的话。让我们更深入地研究这个框架去了解它。

四层结构(The Four Layers)

也许你在一些”Flutter简介“中已经知道了Flutter架构的概述。但是不知怎么的,你当时还准备好去理解这这些不同层次背后强大的概念。也许你和我一样,只是盯着那个被投放到墙上的幻灯片中抽象的图表看了20秒。不用担心,我来帮忙,请看下图:

flutter不同的层次结构,更多信息前往https://flutter.io/docs/resources/technical-overview

Flutter 本身包含了很多抽象层次:最顶层通常使用 Material 和 Cupertino 组件,接着是更加通用的 Widget 层,大多数情况下,你使用的大多数组件都来自该两层,这是完全没问题的。你可能已经见过(使用过)这个繁多的组件中一些(Material库中的FloatingActionButtonScaffold,或来自widgets库中的ColumnGestureDetector)组件的概率是很高的,这些都是十分常用的。

在往下,是渲染层简化了布局和绘制的处理过程,是对底层的dart:ui库另一种抽象。dart:ui是dart最后一层,主要处理与flutter引擎的通信。

简单地可以这样说,较高层次更容易处理一些,但是较低层级提供更细粒度的控制,并增加了复杂性。

1.dart:ui库 (The dart:ui library)

dart:ui 库提供了为flutter框架一些用于引导应用程序的底层服务,诸如这些类:驱动输入,图形文本,布局,渲染子系统。

所以从根本上来将,你可以仅仅通过实例化dart:ui库的相关类来编写“Flutter 应用”(例如:CanvasPaintTextBox)。然而,如果你对直接在canvas上绘画熟悉的话,你就会知道,除了绘制一幅静止的简笔人物画之外,任何事情都是难以处理的。且之后,不仅要考虑到绘图,还要考虑编排布局以及触碰测试应用程序的元素。

这到底是什么意思呢?这意味着您必须手动计算布局中使用的所有坐标。然后加入绘图和触碰测试以捕捉用户输入。对每一帧都这样做,并进行追踪。只要你计划建立一个简单的应用程序,只是显示一些文本居中在一个蓝色的框,这种方法可能是可管理的。但如果你试图建立更复杂的布局,如购物应用程序,或一个小游戏,甚至不敢想加入动画、滚动或其他大家都喜欢的UI,根据我自己的经验,我告诉你,这是开发人员无尽的噩梦。

2. 渲染库(The rendering library)

Flutter Widgets库使用RenderObject层次结构实现其布局和绘制后端。通常,虽然您可以在应用程序中为实现特定的效果而使用定制的RenderBox类,但大多数时候,您与RenderObject层次结构的唯一交互将是在调试布局问题。

渲染库是dart:ui库上的第一层抽象,为你做所有的繁杂的数学运算(例如,跟踪计算的坐标等等)。为此,它使用了所谓的Renderobjects。你可以讲RenderObjects对比成汽车的引擎,它们是你实际上将你的应用带到屏幕上的组件。这颗由RenderObjects组成的树随后被Flutter布局绘制出来。为了优化这个复杂的流程,Flutter使用了一种智能算法,能聪明方式去积极地缓存这些昂贵的计算。,以保证每次迭代工作量最小化。

大多数情况下,你会发现Flutter使用的是RenderBox,而不是RenderObject。这是因为项目背后那些人认为简单的box布局协议,可以更好地构建高性能的UI。想想每个被计算的组件放置在自己的盒子中,然后与其他预先分配好的盒子一起排列。因此,如果你的布局中只有一个小组件发生了变化(例如,一个开关,一个按钮),那么系统仅仅会计算这个相对较小的盒子。

3. 组件树 (The widgets library)

组件库,可能是最有趣的层,是另一层抽象,提供了一些现成的组件,可以用在我们的应用中。你会发现该库中所有的组件分为三类,会由恰当的RenderObject为你处理:

  1. 布局(Layout)例如,ColumnRow组件,能够使得我们轻松地水平或垂直排列它们。
  2. 绘制(Painting) 例如,TextImage组件,能够让我们展示(绘制)一些内容在屏幕上。
  3. 触碰测试(Hit-Testing) 例如,GestureDetector允许我们识别不同的手势,如轻击(用于检测按钮的按下)和拖动(用于在列表中滑动)。

通常情况下,您将使用许多这些“基本”小部件,并用它们组合您自己的小部件。例如,您可以从Container中构建一个按钮,并将其封装到手势GestureDetector中以检测按钮的按下。这被称作组合优于继承。

然而,相反不是自己创建每个UI组件,Flutter团队已经创建了两个库包含了常用的组件,Material 和Cupertino(类iOS)风格。

4. Material和Cupertino库 (The Material&Cupertino library)

Flutter为你进行了抽象,它使得你的开发变得更加轻松。这个第四个级别,包含了来自Material设计规范预建元素和一些iOS风格的重建组件。你可以想想AlertDialogSwitch,和FloatingActionButton。如果是iOS用户,您应该熟悉CupertinoAlertDialogCupertinoButtonCupertinoSwitch

把它们放在一起(Putting it all Together)

RenderObjects是如何连接到Widgets?Flutter是如何创建这个布局的?或者什么是Element?

目前我们正在构建的应用程序非常简单。它只由三个stateless组件构成:SimpleAppSimpleContainerSimpleText,那么当我们将它传入给Flutter的runApp(SimpleApp())方法的时候发生了什么?

当第一个调用runApp()的时候,在后台发生了很多事情:

  1. Flutter 将会构建组件树,包含了我们的三个stateless组件。
  2. Flutter 该组件树向下并通过调用组件的createElement()来创建第二颗树,包含了对应的元素对象。
  3. 创建第三课树,使用恰当的RenderOjbects来进行填充,这些是通过对用组件上的元素触发createRenderObject()创建的。

下面是在Flutter经过上面描述的三个步骤的现况图:

Flutter框架创建了三个不同的树结构:组件树,元素树,渲染对象树。每个Element保存了对WidgetRenderObject的引用,你可能会有疑问:“我知道组件,但是什么是元素和渲染对象呢?”。

这个RenderObject包含了对应实际组件的所有的渲染逻辑,实例化开销是十分大的。它负责布局,绘制,点击测试。尽可能最好将这些对象保存在内存中,可能话的甚至能够重复利用他们(因为实例化开销很大)。从根本上讲,元素是不可变的Widget和可变的RenderObject 的粘合剂。Elements主要是这些这样的对象,擅长两个对象之间的对比。在这里,使widget和render对象。它们表示使用一个组件配置在树结构中特定的位置,同时也保存了相关组件和渲染对象的引用。

为什么使用三颗树而不是使用一颗树使一个好主意呢?简而言之,就是性能。每次组件树发生变化,Flutter就是使用Element树来比对比新的组件树和已经存在的RenderObjects。当一个组件的类型与之前是相同的。Flutter没必要重新创建新的开销昂贵的RenderObject,仅仅是更它可变的信息。因为Widgets是非常轻量的,实例化开销不大,也十分适合描述当前的状态(也被称作配置)。这个“笨重”的RenderObjects不是每次都会重新创建,尽可能重用,正如Simon所指出的,“The whole app acts like a huge RecyclerView”(整个应用程序就像一个巨大的可重复利用视图)。

然而,在框架中,这些元素被很好的抽象掉了,所以你不必经常处理他们。在每个build(BuildContext context)方法中传入的BuildContext实际上就是对应的封装的Element,被封装到了BuildContext接口,这就是为什么每单个组件的不同的原因。

计算下一帧 (Computing the Next Frame)

因为Widgets是不可变的,组件树每次配置的更新都需要重新构建。当我们更改我们容器的颜色为红色,框架将会触发重新构建,将会重新创建整个控件树,这正是因为它是不可变的。接下来,在元素树元素的帮助下,Flutter会将组件树第一个和渲染树的一个进行对比比较,接下来是第二个……。

Flutter遵循一个基本规则:检查这个旧的和新的组件是否源自同一类型。如果不是,将会从树中移除WidgetElementRenderObject(包括子树),创建新的对象。如果它们是同一类型,仅更新RenderObject的配置,呈现展示组件新的配置,并向下遍历。

在我们这个例子中,这个SimpleApp组件和之前的类型相同,也与对应SimpleAppRender对象配置相同,所以不会更改任何东西。组件树中下一个是SimpleContainer组件,但是拥有不同的颜色配置。因为SimpleContainer依然需一个SimpleContainerRender对象来进行绘制,Flutter仅会更新SimpleContainerRender对象的颜色属性并寻求重新绘制,其他对象保持不变。

在重新构建小部件树并更新RenderObjects的配置之后,这三棵树的状态。注意,element和renderobject仍然是相同的实例。

这个处理是很快的,因为flutter真的很擅长创建这些简单的组件,来呈现当前的应用的配置。这些“笨重”的对象会保持不变,直到对应的组件类型从组件树移除。如果组件的类型发生变化会发生什么呢?

SimpleText被SimpleButton替换。

同样的,Flutter将会迭代新创建的组件树,比较渲染树中RenderObjects对象的类型与组件的类型。

新的组件树,SimpleText,对应的Element和SimpleTextRender将会从树中移除。

由于SimpleButton与元素树中对应位置的元素类型不匹配,Flutter将会从其他两颗树中移除对应的ElementSimpleTextRender,然后继续向下遍历新建的组件树,实例化合适的ElementsRenderObjects

新的渲染树已经构建好了,现在将会被绘制到屏幕上,对于Flutter来说,大使用大量的优化和主动缓存策略,因为你不需要手动地去处理它。很酷,不是吗?