什么是Flutter?
Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。同时 Flutter还使用 Native引擎渲染视图,这无疑能为用户提供良好的体验。
前端基本绘图原理
我们都知道显示器以固定的频率刷新。当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync),所以 60Hz的屏幕就会一秒内发出 60次这样的信号。
并且一般地来说,计算机系统中,CPU、GPU和显示器以一种特定的方式协作:CPU将计算好的显示内容提交给 GPU,GPU渲染后放入帧缓冲区,然后视频控制器按照 VSync信号从帧缓冲区取帧数据传递给显示器显示。
所以,Android、iOS的 App在显示 UI时是如此,Flutter也不例外,也遵循了这种模式。所以从这里可以看出 Flutter和 React-Native之众的本质区别:React-Native之类只是扩展调用 OEM组件,而 Flutter是自己渲染。
在 Flutter Architecture的解释中,Google还提供了一张更为详尽的图来解释 Flutter的原理:
这张图清晰地解释了:Flutter只关心向 GPU提供视图数据,GPU的 VSync信号同步到 UI线程,UI线程使用 Dart来构建抽象的视图结构,这份数据结构在 GPU线程进行图层合成,视图数据提供给 Skia引擎渲染为 GPU数据,这些数据通过 OpenGL或者 Vulkan提供给 GPU。
所以 Flutter并不关心显示器、视频控制器以及 GPU具体工作,它只关心 GPU发出的 VSync信号,尽可能快地在两个 VSync信号之间计算并合成视图数据,并且把数据提供给 GPU。
在了解 Flutter的基本概念后,我们来看下Flutter是如何被设计的。
Flutter是如何设计的?
Flutter的整个架构设计图:
Flutter Framework
这是一个纯 Dart实现的 SDK。它实现了一套基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。这个纯 Dart实现的 SDK被封装为了一个叫作 dart:ui的 Dart库。我们在使用 Flutter写 App的时候,直接导入这个库即可使用组件等功能。
Flutter Engine
这是一个纯 C++实现的 SDK,其中囊括了 Skia引擎、Dart运行时、文字排版引擎等。不过说白了,它就是 Dart的一个运行时,它可以以 JIT、JIT Snapshot 或者 AOT的模式运行 Dart代码。在代码调用 dart:ui库时,提供 dart:ui库中 Native Binding 实现。 这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。
UI框架绘图流程
流程包括7个步骤:
首先是获取到用户的操作,然后你的应用会因此显示一些动画,接着 Flutter 开始构建 Widget 对象。
Widget 对象构建完成后进入渲染阶段,这个阶段主要包括三步:
布局元素:决定页面元素在屏幕上的位置和大小;
绘制阶段:将页面元素绘制成它们应有的样式;
合成阶段:按照绘制规则将之前两个步骤的产物组合在一起。
最后的光栅化由 Engine 层来完成。
在渲染阶段,控件树(widget)会转换成对应的渲染对象(RenderObject)树,在 Rendering 层进行布局和绘制。
布局的计算
在布局时 Flutter 深度优先遍历渲染对象树。数据流的传递方式是从上到下传递约束,从下到上传递大小。也就是说,父节点会将自己的约束传递给子节点,子节点根据接收到的约束来计算自己的大小,然后将自己的尺寸返回给父节点。整个过程中,位置信息由父节点来控制,子节点并不关心自己所在的位置,而父节点也不关心子节点具体长什么样子。
性能优化
为了防止因子节点发生变化而导致的整个控件树重绘,Flutter 加入了一个机制——Relayout Boundary,在一些特定的情形下 Relayout Boundary 会被自动创建,不需要开发者手动添加。
边界:Flutter使用边界标记需要重新布局和重新绘制的节点部分,这样就可以避免其他节点被污染或者触发重建。就是控件大小不会影响其他控件时,就没必要重新布局整个控件树。有了这个机制后,无论子树发生什么样的变化,处理范围都只在子树上。
缓存:要提升性能表现,缓存也是少不了的。在 Flutter中,几乎所有的 Element都会具有一个 key,这个 key是唯一的。当子树重建后,只会刷新 key不同的部分。而节点数据的复用就是依靠 key来从缓存中取得。
在确定每个空间的位置和大小之后,就进入绘制阶段。绘制节点的时候也是深度遍历绘制节点树,然后把不同的 RenderObject 绘制到不同的图层上。
绘制
这时有可能出现一种特殊情况,如下图所示节点 2 在绘制子节点 4 时,由于其节点 4 需要单独绘制到一个图层上,因此绿色图层上面多了个黄色的图层。之后再需要绘制其他内容(节点 5)就需要再增加一个图层(红色)。再接下来要绘制节点 1 的右子树(节点 6),也会被绘制到红色图层上。所以如果 节点2发生改变就会改变红色图层上的内容,因此也影响到了毫不相干的节点6。
为了避免这种情况,Flutter 的设计者这里基于 Relayout Boundary 的思想增加了 Repaint Boundary。在绘制页面时候如果遇见 Repaint Boundary 就会强制切换图层。
如下图所示,在从上到下遍历控件树遇到 Repaint Boundary 会重新绘制到新的图层(深蓝色),在从下到上返回的时候又遇到 Repaint Boundary,于是又增加一个新的图层(浅蓝色)。这样,即使发生重绘也不会对其他子树产生影响。
整个UI框架流程大致分析如此。接下来再看下视图的结构。
视图结构
底层的UI框架在向绘制引擎提供视图数据都需要一份结构化的视图数据,俗称为“视图树”(View Tree)。Flutter 的视图结构的抽象分为三部分:Widget, Element, RenderObject。
Widget
Widget是 Flutter中控件实现的基本单位,其意义类似于 Cocoa Touch中的 UIView。Widget里面存储了一个视图的配置信息,包括布局、属性等待。所以它只是一份轻量的,可直接使用的数据结构。在构建为结构树,甚至重新创建和销毁结构树时都不存在明显的性能问题。
Element
Element是 Widget的抽象,它其实承载了视图构建的上下文数据。构建系统通过遍历 Element树来构建 RenderObject数据,比如视图更新时,只会标记 dirty Element,而不会标记 dirty Widget。所以 Widget“无状态”,而 Element“有状态” (这个状态指框架层的构建状态)。
RenderObject
在 RenderObject树中会发生 Layout、Paint的绘制事件,所以 Flutter中大部分的绘图性能优化发生在这里。RenderObject树构建的数据会被加入到 Engine所需的 LayerTree中,Engine通过 LayerTree进行视图合成并光栅化,提交给 GPU。
Flutter通过这三种概念,把原本比较复杂的视图树状态、数据的维护和构建拆分得更单一、易于集中治理。
作者:gofun成都技术中心-micck