[译]Flutter中的层级结构
Flutter是如何使⽤Widgets、Elements和RenderObjects来实现如此令⼈惊艳的视觉效果的呢?
本⽂已经得到作者的允许,将其原⽂翻译成中⽂。鉴于本⼈的英语能⼒以及表达能⼒有限,请英语⽔平⾜够的朋友前往原⽂地址去阅读=。=。
Flutter是⼀个优秀的UI框架,它能够帮助我们快速的构建出漂亮的⽤户界⾯。只需要很少的代码和热重载,你的APP就能够拥有⾼达120fps的流畅性。但是,你是否想过Flutter是如何做到这⼀切的呢?Flutter使⽤了什么样的魔法来实现这⼀切的呢?或者说Flutter 内部是如何⼯作的呢?我们将会探索这⼀切,请拿杯茶或者咖啡然后继续阅读下去吧。 也许你已经听过Flutter中⼀切皆为 Widget。你的APP是个 Widget、Text是个 Widget, Widget周围的 padding也是 Widget,甚⾄ recognise gestures(⼿势识别)也是⼀个 Widget。但是这些并不是全部的事实。如果我告诉你 Widget的确很棒,能够帮助你快速的构建出APP,但是我不使⽤任何⼀个 Widget就能够完成App的构建你相信吗?让我们先来深⼊框架来看看如何做到这⼀切吧。
The Four Layers
也许你已经在⼀些类似于‘Flutter⼊门介绍’的⽂章中对Flutter有了⽐较⼤致的了解。但是你并没有能够真正
的理解这些层级所代表的概念。也许你像我⼀样看着这张图看了20s却不知道怎么理解。不⽤担⼼,我会帮助你的。看下下⾯的这个图吧。
Flutter framework由许多抽象的层级组成。在这些层级的最顶端是我们经常⽤到的 Material和 Cupertino Widgets。我们⼤多数情况下使⽤的就是这两类Widget。 在Widget层下⾯,你会发现 Rendering层。 Rendering层简化了布局和绘制过程。它是 dart:ui的的抽象化。 dart:ui是框架的最底层,它负责处理与 Engine层的交流沟通。 简⽽⾔之,等级越⾼的层越容易使⽤,但是等级越低的层,暴露出来的api越多,越能够增加⾃定义功能。
1. The dart:ui library
dart:ui library暴露出最底层的服务,这些服务被⽤来引导Application,例如⽤来驱动输⼊、绘制⽂字、布局和渲染⼦系统。
所以你可以仅仅通过使⽤实例化dart:ui库中的类(例如Canvas、Paint和TextField)来构建⼀个Flutter App。但是如果你对于直接
在canvas上绘制⽐较熟悉,就会知道使⽤这些底层api绘制⼀个图案是既难⼜繁琐的。 接下来考虑⼀些不是绘制的东西吧,例如布局和命中测试。 这些意味着什么呢? 这意味着你必须⼿动的计算所有在你
布局中使⽤的坐标。然后混合⼀些绘制和命中测试来捕获⽤户的输⼊。对每⼀帧进⾏上述操作并追踪它们。这个⽅法对于那些⽐较简单的APP,⽐如⼀个在蓝⾊区域内展⽰⽂字这种⽐较适⽤。如果对于那些⽐较复杂的APP或者简单的游戏来说可够你受的了。更不⽤说产品经理最喜爱的动画、滚动和⼀些酷炫的UI效果了。⽤我多年的开发经验告诉你,这些是开发者⽆穷⽆尽的梦魇。
2. The Rendering library
Flutter的Rendering tree(渲染树)。RenderObject的层级结构被Flutter Widgets库使⽤来实现其布局和后台的绘制。通常来说,尽管你可能会使⽤RenderBox来在你的应⽤中实现⾃定义的效果,但是⼤多数情况下我们唯⼀与RenderObject的交互就是在调试布局信息的时候。
Rendering library是dart:ui library上第⼀个抽象层。它替你做了所有繁重的数学计算⼯作(例如跟踪需要不断计算的坐标)。它使
flutter开发app⽤RenderObjects来处理这些⼯作。你可以把RenderObjects想象成⼀个汽车的发动机,它承担了所有把你的APP展⽰到屏幕的⼯
作。Rendering tree中的所有RenderObjects都会被Flutter分层和绘制。为了优化这个复杂的过程,Flutter使⽤了⼀个智能算法来缓存这些实例化很耗费性能的对象从⽽实现在性能最优化。 ⼤多数情况,
你会发现Flutter使⽤RenderBox⽽不是RenderObject。这是因为项⽬的构建者发现使⽤⼀个简单和盒布局约束就能够成功的构建出有效稳定的UI。想象⼀下所有的Widget都被放置在它们的盒中。这个盒中的相关参数都计算好了,然后被放置到其他已经整理好的盒中间。所以如果在你的布局中仅有⼀个Widget改变了,只需要装载其的盒被系统重新计算即可。
3. The Widget library
Flutter Widgets框架
Widget库或许是最有意思的库。它是另外⼀个⽤来提供开箱即⽤的Widget的抽象层。这个库中所有的Widget都属于以下三种使⽤适当
的RenderObject处理的Widget之⼀。
1. Layout 例如Column和Row Widgets⽤来帮助我们轻松的处理其他Widget的布局。
2. Painting 例如Text和Image Widgets允许我们展⽰(绘制)⼀些内容在屏幕上。
3. Hit-Testing 例如GestureDetector允许我们识别出不同的⼿势,例如点击和滑动。
⼤多数情况下我们会使⽤⼀些“基础”Widget来组成我们需要的Widget。例如我们使⽤GestureDetec来包裹Container,Container中包裹Button来处理按钮点击。这叫做组合⽽不是继承。 然⽽除了⾃⼰构建每个UI组件,Flutter团队还创建了两个包含常⽤
的Material和Cupertino风格的Widgets的库。
4. The Material & Cupertino library
使⽤Material和Cupertino设计规范的Widgets库。
Flutter为了减少开发者的负担,创建了这个拥有Material和Cupertino风格的Widgets层。
Put it all Together
RenderObject是如何与Widgets连接起来的呢?Flutter是如何创建布局?Element⼜是什么呢? 已经说的够多了,让我们在实践中学习吧。考虑如下Widgets树。
我们构建的这个APP是⾮常简单的。它由三个 Stateless Widget组成: SimpleApp、 SimpleContainer、 SimpleText。所以如果我们调⽤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中都有着相对应
的Widget和RenderObject的引⽤。
那什么⼜是RenderObject呢? RenderObject中包含了所有⽤来渲染实例Widget的逻辑。它负责layout、painting和hit-testing。它的⽣成⼗分耗费性能,所以我们应该尽可能的缓存它。我们把它在内存中尽可能的保存更长的时间,甚⾄回收利⽤它们(因为它们的实例化真的很耗费资源)。这个时候Element就登场了。Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。Element擅长⽐较两
个Object,在Flutter⾥⾯就是Widget和RenderObject。它的作⽤是配置好Widget在树中的位置,并且保持对于相对应
的RenderObject和Widget的引⽤。 为什么使⽤三个树⽽不是⼀个树呢? 简⽽⾔之是为了性能。当Widget树改变的时候,Flutter使
⽤Element树来⽐较新的Widget树和原来的RenderObject树。如果某⼀个位置的Widget和RenderObject类型不⼀致,才需要重新创
建RenderObject。如果其他位置的Widget和RenderObject类型⼀致,则只需要修改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树中第⼆个Widget和RenderObject树中第⼆个RenderObject的类型,以此类推,直到Widget树和RendObject树⽐较完成。
Flutter遵循⼀个最基本的原则: 判断新的Widget和⽼的Widget是否是同⼀个类型。 如果不是同⼀个类型,那就把 Widget、Element、 RenderObject分别从它们的树(包括它们的⼦树)上移除,然后创建新的对象。 如果是⼀个类型,那就仅仅修改
RenderObject中的配置,然后继续向下遍历。 在我们的例⼦中, SimpleApp Widget是和原来⼀样的类型,它的配置也是和原来的SimpleAppRender⼀样的,所以什么都不会发⽣。下⼀个item在Widget树中是 SimpleContainer Widget,它的类型和原来是⼀样的,但是它的颜⾊变化了, RenderObject的配置发⽣变化了。因为 SimpleObject仍然需要⼀个 SimpleContainerRender来渲染,Flutter只是更新了SimpleContainerRender的颜⾊属性,然后要求它重新渲染。其他的对象都保持不变。  这个过程是⾮常快的,因为Flutter⾮常擅长创建那些轻量级的 Widgets。那些重量级的 RenderObject则是保持不变,直到与其相对应类型的 Widget从 Widget树中被移除。那如果Widget的类型发⽣改变了会发⽣什么呢?  和
原来⼀样,Flutter会对Widget树的顶端向下遍历,与 RenderObject树中的RenderObject类型进⾏对⽐。  因为 SimpleButton的类型与 Element树中相对应位置的 Element的类型不同(实际上还是与RenderObject的类型进⾏⽐较),Flutter将会从各⾃的树上删除这个 Element和相对应的 SimpleTextRender。然后Flutter将会重建与SimpleButton相对应的 Element和 RenderObject。  然后新的 RenderObject树已经被重建,并将会计算布局,然后绘制在屏幕上⾯。Flutter内部使⽤了很多优化⽅法和缓存策略来处理,所以你不需要⼿动处理这个。
Conclusion
现在你应该对Flutter为什么能以如此快的速度渲染复杂布局有了⼤致的了解了。我希望这篇⽂章能够帮助你更好的理解Flutter内部的设计理念。我的Twitter是 ,期待与你的交流。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。