[译]Flutter中的层级结构

Flutter是如何使用Widgets、Elements和RenderObjects来实现如此使人惊艳的视觉效果的呢?

本文已经获得做者的容许,将其原文The Layer Cake翻译成中文。鉴于本人的英语能力以及表达能力有限,请英语水平足够的朋友前往原文地址去阅读=。=。 算法

delicious cake
Flutter是一个优秀的UI框架,它可以帮助咱们快速的构建出漂亮的用户界面。只须要不多的代码和热重载,你的APP就可以拥有高达120fps的流畅性。可是,你是否想过Flutter是如何作到这一切的呢?Flutter使用了什么样的魔法来实现这一切的呢?或者说Flutter内部是如何工做的呢?咱们将会探索这一切,请拿杯茶或者咖啡而后继续阅读下去吧。 也许你已经听过Flutter中一切皆为 Widget。你的APP是个 Widget、Text是个 WidgetWidget周围的 padding也是 Widget,甚至 recognise gestures(手势识别)也是一个 Widget。可是这些并非所有的事实。若是我告诉你 Widget的确很棒,可以帮助你快速的构建出APP,可是我不使用任何一个 Widget就可以完成App的构建你相信吗?让咱们先来深刻框架来看看如何作到这一切吧。

The Four Layers

也许你已经在一些相似于‘Flutter入门介绍’的文章中对Flutter有了比较大体的了解。可是你并无可以真正的理解这些层级所表明的概念。也许你像我同样看着这张图看了20s殊不知道怎么理解。不用担忧,我会帮助你的。看下下面的这个图吧。 canvas

four layers
Flutter framework由许多抽象的层级组成。在这些层级的最顶端是咱们常常用到的 MaterialCupertino Widgets。咱们大多数状况下使用的就是这两类Widget。 在Widget层下面,你会发现 Rendering层。 Rendering层简化了布局和绘制过程。它是 dart:ui的的抽象化。 dart:ui是框架的最底层,它负责处理与 Engine层的交流沟通。 简而言之,等级越高的层越容易使用,可是等级越低的层,暴露出来的api越多,越可以增长自定义功能。

1. The dart:ui library

dart:ui library暴露出最底层的服务,这些服务被用来引导Application,例如用来驱动输入、绘制文字、布局和渲染子系统。api

因此你能够仅仅经过使用实例化dart:ui库中的类(例如CanvasPaintTextField)来构建一个Flutter App。可是若是你对于直接在canvas上绘制比较熟悉,就会知道使用这些底层api绘制一个图案是既难又繁琐的。 接下来考虑一些不是绘制的东西吧,例如布局和命中测试。 这些意味着什么呢? 这意味着你必须手动的计算全部在你布局中使用的坐标。而后混合一些绘制和命中测试来捕获用户的输入。对每一帧进行上述操做并追踪它们。这个方法对于那些比较简单的APP,好比一个在蓝色区域内展现文字这种比较适用。若是对于那些比较复杂的APP或者简单的游戏来讲可够你受的了。更不用说产品经理最喜好的动画、滚动和一些酷炫的UI效果了。用我多年的开发经验告诉你,这些是开发者无穷无尽的梦魇。缓存

2. The Rendering library

Flutter的Rendering tree(渲染树)。RenderObject的层级结构被Flutter Widgets库使用来实现其布局和后台的绘制。一般来讲,尽管你可能会使用RenderBox来在你的应用中实现自定义的效果,可是大多数状况下咱们惟一与RenderObject的交互就是在调试布局信息的时候。框架

Rendering librarydart:ui library上第一个抽象层。它替你作了全部繁重的数学计算工做(例如跟踪须要不断计算的坐标)。它使用RenderObjects来处理这些工做。你能够把RenderObjects想象成一个汽车的发动机,它承担了全部把你的APP展现到屏幕的工做。Rendering tree中的全部RenderObjects都会被Flutter分层和绘制。为了优化这个复杂的过程,Flutter使用了一个智能算法来缓存这些实例化很耗费性能的对象从而实如今性能最优化。 大多数状况,你会发现Flutter使用RenderBox而不是RenderObject。这是由于项目的构建者发现使用一个简单和盒布局约束就可以成功的构建出有效稳定的UI。想象一下全部的Widget都被放置在它们的盒中。这个盒中的相关参数都计算好了,而后被放置到其余已经整理好的盒中间。因此若是在你的布局中仅有一个Widget改变了,只须要装载其的盒被系统从新计算便可。less

3. The Widget library

Flutter Widgets框架工具

Widget库或许是最有意思的库。它是另一个用来提供开箱即用的Widget的抽象层。这个库中全部的Widget都属于如下三种使用适当的RenderObject处理的Widget之一。布局

  1. Layout 例如ColumnRow Widgets用来帮助咱们轻松的处理其余Widget的布局。
  2. Painting 例如TextImage Widgets容许咱们展现(绘制)一些内容在屏幕上。
  3. Hit-Testing 例如GestureDetector容许咱们识别出不一样的手势,例如点击和滑动。

大多数状况下咱们会使用一些“基础”Widget来组成咱们须要的Widget。例如咱们使用GestureDetec来包裹ContainerContainer中包裹Button来处理按钮点击。这叫作组合而不是继承。 然而除了本身构建每一个UI组件,Flutter团队还建立了两个包含经常使用的MaterialCupertino风格的Widgets的库。性能

4. The Material & Cupertino library

使用Material和Cupertino设计规范的Widgets库。学习

Flutter为了减小开发者的负担,建立了这个拥有MaterialCupertino风格的Widgets层。

Put it all Together

RenderObject是如何与Widgets链接起来的呢?Flutter是如何建立布局?Element又是什么呢? 已经说的够多了,让咱们在实践中学习吧。考虑以下Widgets树。

在现实世界中,相似于Text这种Widget是由其余一些Widgets组成而来的,为了简化这些,咱们引用phantasmal SimpleContainer和SimpleText。
咱们构建的这个APP是很是简单的。它由三个 Stateless Widget组成: SimpleAppSimpleContainerSimpleText。因此若是咱们调用Flutter的runApp()方法会发生什么呢? 当 runApp()被调用时,第一时间会在后台发生如下事件。

  1. Flutter会构建包含这三个Widget的Widgets树。
  2. Flutter遍历Widget树,而后根据其中的Widget调用createElement()来建立相应的Element对象,最后将这些对象组建成Element树。
  3. 第三个树被建立,这个树中包含了与Widget对应的Element经过createRenderObject()建立的RenderObject。 下图是Flutter通过这三个步骤后的状态:

Flutter建立了三个不一样的树,一个对应着Widget,一个对应着Element,一个对应着RenderObject每个Element中都有着相对应的WidgetRenderObject的引用。

那什么又是RenderObject呢? RenderObject中包含了全部用来渲染实例Widget的逻辑。它负责layoutpaintinghit-testing。它的生成十分耗费性能,因此咱们应该尽量的缓存它。咱们把它在内存中尽量的保存更长的时间,甚至回收利用它们(由于它们的实例化真的很耗费资源)。这个时候Element就登场了。Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element擅长比较两个Object,在Flutter里面就是WidgetRenderObject。它的做用是配置好Widget在树中的位置,而且保持对于相对应的RenderObjectWidget的引用。 为何使用三个树而不是一个树呢? 简而言之是为了性能。当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的RenderObject树。若是某一个位置的WidgetRenderObject类型不一致,才须要从新建立RenderObject。若是其余位置的WidgetRenderObject类型一致,则只须要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工做了。由于Widget是很是轻量级的,实例化耗费的性能不多,因此它是描述APP的状态(也就是configuration)的最好工具。重量级的RenderObject(建立十分耗费性能)则须要尽量少的建立,并尽量的复用。就像Simon所说:整个Flutter APP就像是一个RecycleView。 然而,在框架中,Element是被抽离开来的,因此你不须要常常和它们打交道。每一个Widget的build(BuildContext context)方法中传递的context就是实现了BuildContext接口的Element,这也就是为何相同类别的单个Widget不一样的缘由。

Computer the Next Frame

由于Widget是不可变的,当某个Widget的配置改变的时候,整个Widget树都须要被重建。例如当咱们改变一个Container的颜色为红色的时候,框架就会触发一个重建整个Widget树的动做。而后在Element的帮助下,Flutter比较新的Widget树中的第一个Widget类型和RenderObject树中第一个RenderObject的类型。接下来比较Widget树中第二个WidgetRenderObject树中第二个RenderObject的类型,以此类推,直到Widget树和RendObject树比较完成。

改变Container的颜色
Flutter遵循一个最基本的原则: 判断新的Widget和老的Widget是不是同一个类型。 若是不是同一个类型,那就把 WidgetElementRenderObject分别从它们的树(包括它们的子树)上移除,而后建立新的对象。 若是是一个类型,那就仅仅修改 RenderObject中的配置,而后继续向下遍历。 在咱们的例子中, SimpleApp Widget是和原来同样的类型,它的配置也是和原来的 SimpleAppRender同样的,因此什么都不会发生。下一个item在Widget树中是 SimpleContainer Widget,它的类型和原来是同样的,可是它的颜色变化了, RenderObject的配置发生变化了。由于 SimpleObject仍然须要一个 SimpleContainerRender来渲染,Flutter只是更新了 SimpleContainerRender的颜色属性,而后要求它从新渲染。其余的对象都保持不变。
注意这三个树,配置发生改变以后,Element和RenderObject实例没有发生变化。
这个过程是很是快的,由于Flutter很是擅长建立那些轻量级的 Widgets。那些重量级的 RenderObject则是保持不变,直到与其相对应类型的 WidgetWidget树中被移除。那若是Widget的类型发生改变了会发生什么呢?
SimpleText被SimpButton替代
和原来同样,Flutter会对Widget树的顶端向下遍历,与 RenderObject树中的 RenderObject类型进行对比。
新的Widget树,SimpleText Widget和与之对应的Element、RenderObject都从其树上消失了。
由于 SimpleButton的类型与 Element树中相对应位置的 Element的类型不一样(实际上仍是与 RenderObject的类型进行比较),Flutter将会从各自的树上删除这个 Element和相对应的 SimpleTextRender。而后Flutter将会重建与 SimpleButton相对应的 ElementRenderObject
最终的树
而后新的 RenderObject树已经被重建,并将会计算布局,而后绘制在屏幕上面。Flutter内部使用了不少优化方法和缓存策略来处理,因此你不须要手动处理这个。

Conclusion

如今你应该对Flutter为何能以如此快的速度渲染复杂布局有了大体的了解了。我但愿这篇文章可以帮助你更好的理解Flutter内部的设计理念。个人Twitter是 Frederik Schweiger,期待与你的交流。

相关文章
相关标签/搜索