英文地址:The Layer Cake
Flutter 是一个极棒的UI框架,能够让你愉悦地以光速构建优美的用户界面。仅仅写下几行代码,点击保存[神奇地插入进去,亚秒级别的热重载]。你的应用能运行的十分流畅,能达到120fps的速度。然而,你是否询问过自己为何Flutter如此之快?它的秘方是什么?或者Flutter实际上是如何工作的?那么,别再去找其他的了!饮一杯新鲜的茶或咖啡,继续往下阅读吧。
也许你可能听说过Flutter全部都是关于组件的。你的应用是一个组件,一个text是一个组件,包裹组件的padding也是一个组件,你甚至可以通过一个小部件识别手势。好吧,这不是全部的事实,假若我告诉你widgets让Flutter的开发又快又简单,但是你也可以甚至不需要使用一个小部件来构建一个完整的Flutter应用程序的话。让我们更深入地研究这个框架去了解它。
四层结构(The Four Layers)
也许你在一些”Flutter简介“中已经知道了Flutter架构的概述。但是不知怎么的,你当时还准备好去理解这这些不同层次背后强大的概念。也许你和我一样,只是盯着那个被投放到墙上的幻灯片中抽象的图表看了20秒。不用担心,我来帮忙,请看下图:
Flutter 本身包含了很多抽象层次:最顶层通常使用 Material 和 Cupertino 组件,接着是更加通用的 Widget 层,大多数情况下,你使用的大多数组件都来自该两层,这是完全没问题的。你可能已经见过(使用过)这个繁多的组件中一些(Material库中的FloatingActionButton和Scaffold,或来自widgets库中的Column和GestureDetector)组件的概率是很高的,这些都是十分常用的。
在往下,是渲染层简化了布局和绘制的处理过程,是对底层的dart:ui库另一种抽象。dart:ui是dart最后一层,主要处理与flutter引擎的通信。
简单地可以这样说,较高层次更容易处理一些,但是较低层级提供更细粒度的控制,并增加了复杂性。
1.dart:ui库 (The dart:ui library)
dart:ui 库提供了为flutter框架一些用于引导应用程序的底层服务,诸如这些类:驱动输入,图形文本,布局,渲染子系统。
所以从根本上来将,你可以仅仅通过实例化dart:ui库的相关类来编写“Flutter 应用”(例如:Canvas
,Paint
和 TextBox
)。然而,如果你对直接在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
为你处理:
- 布局(Layout)例如,
Column
和Row
组件,能够使得我们轻松地水平或垂直排列它们。 - 绘制(Painting) 例如,
Text
和Image
组件,能够让我们展示(绘制)一些内容在屏幕上。 - 触碰测试(Hit-Testing) 例如,
GestureDetector
允许我们识别不同的手势,如轻击(用于检测按钮的按下)和拖动(用于在列表中滑动)。
通常情况下,您将使用许多这些“基本”小部件,并用它们组合您自己的小部件。例如,您可以从Container
中构建一个按钮,并将其封装到手势GestureDetector
中以检测按钮的按下。这被称作组合优于继承。
然而,相反不是自己创建每个UI组件,Flutter团队已经创建了两个库包含了常用的组件,Material 和Cupertino(类iOS)风格。
4. Material和Cupertino库 (The Material&Cupertino library)
Flutter为你进行了抽象,它使得你的开发变得更加轻松。这个第四个级别,包含了来自Material设计规范预建元素和一些iOS风格的重建组件。你可以想想AlertDialog
,Switch
,和FloatingActionButton
。如果是iOS用户,您应该熟悉CupertinoAlertDialog
,CupertinoButton
和CupertinoSwitch
。
把它们放在一起(Putting it all Together)
RenderObjects
是如何连接到Widgets
?Flutter是如何创建这个布局的?或者什么是Element
?
目前我们正在构建的应用程序非常简单。它只由三个stateless组件构成:SimpleApp
,SimpleContainer
,SimpleText
,那么当我们将它传入给Flutter的runApp(SimpleApp())方法的时候发生了什么?
当第一个调用runApp()
的时候,在后台发生了很多事情:
- Flutter 将会构建组件树,包含了我们的三个
stateless
组件。 - Flutter 该组件树向下并通过调用组件的
createElement()
来创建第二颗树,包含了对应的元素对象。 - 创建第三课树,使用恰当的
RenderOjbects
来进行填充,这些是通过对用组件上的元素触发createRenderObject()
创建的。
下面是在Flutter经过上面描述的三个步骤的现况图:
Flutter框架创建了三个不同的树结构:组件树,元素树,渲染对象树。每个Element
保存了对Widget
和RenderObject
的引用,你可能会有疑问:“我知道组件,但是什么是元素和渲染对象呢?”。
这个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遵循一个基本规则:检查这个旧的和新的组件是否源自同一类型。如果不是,将会从树中移除Widget
,Element
,RenderObject
(包括子树),创建新的对象。如果它们是同一类型,仅更新RenderObject
的配置,呈现展示组件新的配置,并向下遍历。
在我们这个例子中,这个SimpleApp
组件和之前的类型相同,也与对应SimpleAppRender
对象配置相同,所以不会更改任何东西。组件树中下一个是SimpleContainer
组件,但是拥有不同的颜色配置。因为SimpleContainer
依然需一个SimpleContainerRender
对象来进行绘制,Flutter仅会更新SimpleContainerRender
对象的颜色属性并寻求重新绘制,其他对象保持不变。
这个处理是很快的,因为flutter真的很擅长创建这些简单的组件,来呈现当前的应用的配置。这些“笨重”的对象会保持不变,直到对应的组件类型从组件树移除。如果组件的类型发生变化会发生什么呢?
同样的,Flutter将会迭代新创建的组件树,比较渲染树中RenderObjects
对象的类型与组件的类型。
由于SimpleButton
与元素树中对应位置的元素类型不匹配,Flutter将会从其他两颗树中移除对应的Element
和SimpleTextRender
,然后继续向下遍历新建的组件树,实例化合适的Elements
和RenderObjects
。
新的渲染树已经构建好了,现在将会被绘制到屏幕上,对于Flutter来说,大使用大量的优化和主动缓存策略,因为你不需要手动地去处理它。很酷,不是吗?