一、Flutter架构
众所周知,Flutter是由Google推出的开源的高性能跨平台框架,一个2D渲染引擎。在Flutter中,Widget是Flutter用户界面的基本构成单元,可以说一切皆Widget。与Weex和RN框架使用的JsCore转化的中间层不同,Flutter采用的是全新的架构方案,拥有自己的渲染引擎和Dart上层,每一层都建立在前一层的基础之上,并且上层比下层的使用频率更高,其框架架构如下图所示。
可以看到,自下而上,Flutter分为Embedder、Engine和Framework三层。其中,Embedder是操作系统适配层,主要负责Surface渲染设置,线程设置,以及平台插件等平台相关特性的适配;Engine层负责图形绘制、文字排版和提供Dart运行时,Engine层具有独立虚拟机,正是由于它的存在,Flutter程序才能运行在不同的平台上,实现跨平台运行;Framework层则是使用Dart编写的一套基础视图库,包含了动画、图形绘制和手势识别等功能,是使用频率最高的一层。
- Flutter Embedder:Embedder是Flutter的操作系统适配层,又称为嵌入层,通过该层可以把Flutter嵌入到各个不同的平台上去。Embedder的主要工作包括Surface渲染设置、线程设置、事件循环以及插件的平台适配等。
- Flutter Engine:纯 C++实现的 SDK,其中包括 Skia引擎、Dart运行时、文字排版引擎等。它是 Dart的一个运行时,它可以以 JIT 或者 AOT的模式运行 Dart代码。这个运行时还控制着 VSync信号的传递、GPU数据的填充等,并且还负责把客户端的事件传递到运行时中的代码。
- Flutter Framework:纯 Dart实现的 SDK,提供了一整套自底向上的基础库, 用于处理动画、绘图和手势。并且基于绘图封装了一套 UI组件库,然后根据 Material 和Cupertino两种视觉风格区分开来。在平时应用开发中,与开发者打交道最多的就是这一层,并且最多的就是各种Widget。
二、渲染流程
不管是什么渲染框架,其基本的原理都是:一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,然后显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,GPU渲染好交给显示器显示。
在Flutter中,渲染会用到很多的线程,主要是UI线程和GPU线程,下图是Flutter App线程的运作原理图。
下面重点看一下UI线程和GPU线程。
UI Task Runner
UI Task Runner用于执行Root Isolate代码,它运行在线程对应平台的线程上,属于子线程。同时,Root isolate在引擎启动时会绑定不少Flutter需要的函数方法,这些绑定的函数可以提交渲染帧给Engine层执行渲染操作,下图演示了Widgets生成Layer Tree的过程。
对于每一帧,引擎通过Root Isolate通知Flutter Engine有帧需要渲染,平台收到Flutter Engine通知后会创建对象和组件并生成一个Layer Tree,然后将生成的Layer Tree提交给Flutter Engine。此时,只生成了需要绘制的内容,并没有执行屏幕渲染,而Root Isolate就是负责将创建的Layer Tree绘制到屏幕上,因此如果线程过载会导致卡顿掉帧现象。
除了用于处理渲染之外,Root Isolate还需要处理来自Native Plugins的消息响应、Timers、MicroTasks和异步IO。如果确实有无法避免的繁重计算,建议将这些耗时的操作放到独立的Isolate去执行,从而避免应用UI卡顿问题。
GPU Task Runner
GPU Task Runner用于执行设备GPU指令,UI Task Runner创建的Layer Tree是跨平台的。也就是说,Layer Tree提供了绘制所需要的信息,但是由谁来完成绘制它是不关心的。
GPU Task Runner的主要责任就是负责将Layer Tree提供的信息转化为平台可执行的GPU指令,同时它也负责管理每一帧绘制所需要的GPU资源,包括平台Framebuffer的创建,Surface生命周期管理,以及Texture和Buffers的绘制时机等,下图GPU Task Runner的工作流程。
UI Runner和GPU Runner运行在不同的线程。GPU Runner会根据目前帧执行的进度去向UI Runner请求下一帧的数据,在任务繁重的时候还可能会出现UI Runner的延迟任务。不过这种调度机制的好处在于,确保GPU Runner不至于过载,同时也避免了UI Runner不必要的资源消耗。
GPU Runner可以导致UI Runner的帧调度的延迟,GPU Runner的过载会导致Flutter应用的卡顿,因此在实际使用过程中,建议为每一个Engine实例都新建一个专用的GPU Runner线程。
三、Widget、Element 和 RenderObject
要理解Flutter的渲染原理,那么就必须了解Widget、RenderObject 和 Element及其作用。总的来说,Flutter调用runApp(rootWidget),将rootWidget传给rootElement,做为rootElement的子节点,生成Element树,由Element树生成Render树,如下图所示。
从上面的介绍中,我们隐约知道了Widget、RenderObject 和 Element的作用,简单的介绍一下。
- Widget:Widget 的主要作用是用来保存 Element 信息的(包括布局、渲染属性、事件响应等信息),本身是不可变的,Element 也是根据 Widget 里面保存的配置信息来管理渲染树,以及决定自身是否需要执行渲染。
- RenderObject:RenderObject 做为渲染树中的对象存在,主要作用是处理布局、绘制相关的事情,而绘制的内容是Widget传入的内容。
- Element:Element 可以理解为是其关联的 Widget 的实例,存放视图构建的上下文数据,可以通过遍历Element来查看视图树,Element同时持有Widget和RenderObject对象。
Flutter通过Widget树中的每个控件创建不同类型的渲染对象,组成渲染对象树,而渲染对象树在Flutter中的展示分为四阶段:布局、绘制、合成及渲染。其中,布局和绘制由RenderObject负责完成,Flutter采用深度优先机制遍历渲染树对象,确定树中每个对象的位置和尺寸,并把他们绘制到不同的图层上,而合成及渲染则交给Skia完成。
下图展示了Widget、Element 和 RenderObject的关系。
3.1 Widget
在 Flutter 中,万物皆是 Widget,无论是可见的还是功能型的,下面是官方对Widget的介绍。
- Widget 的作用是用来保存 Element 的配置信息的。
- Widget 本身是不可变的。
- Element 根据 Widget 里面保存的配置信息来管理渲染树。
- Widget 可以多次的插入到 Widget 树中,每插入一次,Element 都要重新装载一遍 Widget 。
- Widget 里面的 key 属性用来决定依赖这个 Widget 的 Element 在 Element 树中是更新还是移除。
下面是Widget源码。
abstract class Widget extends DiagnosticableTree{
const Widget({ this.key });
final Key key;
@protected
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget有两个重要的方法,一个是通过 createElement 来创建 Element 对象的,一个是根据 key 来决定更新行为的 canUpdate 方法。
3.2 RenderObject
- RenderObject 是做为渲染树中的对象存在。
- RenderObject 不定义约束关系,也就是不会对子控件的布局位置、大小等进行管理。
- RenderObject 中有一个 parentData 属性,这个属性用来保存其孩子节点的特定信息,如子节点位置,这个属性对其孩子是透明的。
RenderObject的源码如下。
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
ParentData parentData;
Constraints _constraints;
void layout(Constraints constraints, { bool parentUsesSize = false }) {
}
void paint(PaintingContext context, Offset offset) { }
void performLayout();
void markNeedsPaint() {
}
}
可以看出,RenderObject 的主要作用就是绘制和布局。RenderObject 在 Flutter 中的作用分为四个阶段,即布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制在不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 完成。
3.2 Element
- Element 是关联的Widget 的实例,并且关联在 Widget 树的特定位置上。
- Widget 是不可变的,一个 Widget 可以同时用来配置多个子 Widget 树,而 Element 就用来代表特定位置的 Widget 。
- Widget 是不可变的,而 Element 是可变的,Element决定是否需要刷新界面。
- 一些 Element 只能有一个子节点,如 Container、Opacity、Center ,还有一些可以有多个子节点,如 Column、Row 和 ListView 等。
Element 拥有自己的生命周期:
- Flutter framework 通过 Widget.createElement 来创建一个 Element 。
- 每当 Widget 创建并插入到 Widget 树中时,framework 就会通过 mount 方法来把这个 widget 创建并关联的 Element 插入到 Element 树中。
- 通过 attachRenderObject 方法来将 render objects 来关联到 Render 树上,这时可以认为 Widget 已经显示在屏幕上了。
- 每当执行了 rebuid 方法,Widget 代表的配置信息改变时,framewrok 就会调用这个新的 Widget 的 update 方法执行重绘。
- 当 Element 的祖先想要移除一个子 Element 时,可以通过 deactivateChild 方法,先把这个 Element 从 树中移除,然后将这个 Element 加入到一个“不活跃元素列表”中,接着 framework 就会将这个 element 从屏幕移除。
总的来说,Flutter提出一切皆Widget,Widget 主要用来保存 Element 信息,而Element作用Widget 的实例,存放视图构建的上下文数据,并且同时持有Widget和RenderObject对象,RenderObject的主要作用是处理布局、绘制相关的事情,确定Element树中每个对象的位置和尺寸。