2

一、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 class

  • 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.3 Element

Element class

  • 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树中每个对象的位置和尺寸。


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》