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却不知道怎么理解。不用担心,我会帮助你的。看下下面的这个图吧。

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,例如用来驱动输入、绘制文字、布局和渲染子系统。

所以你可以仅仅通过使用实例化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改变了,只需要装载其的盒被系统重新计算即可。

  1. 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的库。

  1. 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,期待与你的交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 有一种痛,叫智齿 大学时期,三千长了一颗智齿,整天喊牙疼 不由得让我想起,无心法师中,岳绮罗说的,张显宗,我牙疼。...
    王也m阅读 247评论 0 1
  • 小长假最后一天,儿子与几个同学约好前往其中一家聚会。这可是一个长久之前的约定,上一次,同学们约着来我家,结果因为我...
    如心1976阅读 410评论 2 0
  • 天地不仁,以万物为刍狗;圣人不仁,以百姓为刍狗。天地之间,其犹橐龠乎?虚而不屈,动而俞出。多闻数穷,不若守于中。 ...
    JINYANL阅读 156评论 0 0
  • /** TODO:裁剪图片 @param originalImage 原始图片 @param size 目标siz...
    紫wy苏阅读 199评论 0 0