Flutter 架构


简单来讲,Flutter 从上到下可以分为三层:框架层引擎层嵌入层,下面我们分别介绍:

1. 框架层

Flutter Framework,即框架层。这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上,我们来简单介绍一下:

  • 底下两层(Foundation 和 Animation、Painting、Gestures)在 Google 的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是 Flutter Engine 暴露的底层UI库,提供动画、手势及绘制能力。

  • Rendering 层,即渲染层,这一层是一个抽象的布局层,它依赖于 Dart UI 层,渲染层会构建一棵由可渲染对象的组成的渲染树,当动态更新这些对象时,渲染树会找出变化的部分,然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分,它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制(调用底层 dart:ui )。

  • Widgets 层是 Flutter 提供的的一套基础组件库,在基础组件库之上,Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库,它们分别实现了 Material 和 iOS 设计规范。

Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camera 和 webview ,以及和平台无关的功能,例如 animations 。

2. 引擎层

Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。

3. 嵌入层

Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层,假如以后 Flutter 要支持新的平台,则需要针对该新的平台编写一个嵌入层。

通常来说,开发者不需要感知到EngineEmbedder的存在(如果不需要调用平台的系统服务),Framework是开发者需要直接交互的,因而也在整个分层架构模型的最上层。

Widget 接口

在 Flutter 中, widget 的功能是“描述一个UI元素的配置信息”,它就是说, Widget 其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是 Widget 接收的参数,比如对于 Text 来讲,文本的内容、对齐方式、文本样式都是它的配置信息。下面我们先来看一下 Widget 类的声明:

 // 不可变的abstract class Widget extends DiagnosticableTree { const Widget({  this.key });final Key" />;Element createElement();String toStringShort() { final String type = objectRuntimeType(this, 'Widget');return key == null ? type : '$type-$key';}void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties);properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;}bool operator ==(Object other) => super == other;int get hashCode => super.hashCode;static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType&& oldWidget.key == newWidget.key;}...}
  • @immutable 代表 Widget 是不可变的,这会限制 Widget 中定义的属性(即配置信息)必须是不可变的(final),为什么不允许 Widget 中定义的属性变化呢?这是因为,Flutter 中 如果属性发生变化则会重新构建Widget树,即重新创建新的 Widget 实例来替换旧的 Widget 实例,所以允许 Widget 的属性变化是没有意义的,因为一旦 Widget 自己的属性变了自己就会被替换。这也是为什么 Widget 中定义的属性必须是 final 的原因。
  • Widget类继承自DiagnosticableTreeDiagnosticableTree“诊断树”,主要作用是提供调试信息。
  • Key: 这个key属性类似于 React/Vue 中的key,主要的作用是决定是否在下一次build时复用旧的 widget ,决定的条件在canUpdate()方法中。
  • createElement():正如前文所述“一个 widget 可以对应多个Element”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(...) 是一个静态方法,它主要用于 在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用new widget去更新Element对象的配置,否则就会创建新的Element

在Flutter中,可以说万事万物皆Widget。哪怕一个居中的能力,在传统的命令式UI开发中,通常会以一个属性的方式进行设置,而在Flutter中则抽象成一个名为Center的组件。此外,Flutter提倡激进式的组合开发,即尽可能通过一系列基础的Widget构造出你的目标Widget

基于以上两个特点,Flutter的代码中将充斥着各种Widget,每一帧UI的更新都意味着部分Widget的重建。你可能会担心,这种设计是否过于臃肿和低效,其实恰恰相反,这正是Flutter能够进行高性能渲染的基石。Flutter之所以要如此设计也是基于以下两点事实有意而为之:

  • Widget Tree上的Widget节点越多,通过Diff算法得到的需要重建的部分就越精确、范围越小,而UI渲染的主要性能瓶颈就是Widget节点的重建。

  • Dart语言的对象模型GC模型小对象的快读分配和回收做了优化,而Widget正是这种小对象

Flutter中的三棵树

既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?

Flutter 框架的的处理流程是这样的:

  1. 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  2. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  3. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

真正的布局和渲染逻辑在 Render 树中,ElementWidgetRenderObject 的粘合剂,可以理解为一个中间代理。



其中三棵树的各自的作用是:

  • Widget:负责配置,为Element描述UI的配置信息,拥有公开的Api,这部分也是开发者可以直接感知和使用的部分。
  • Element:Flutter Virtual DOM 的管理者,它管理widget的生命周期,代表了内存中真实存在的UI数据,它负责树中特定位置的widget的实例化,持有widget的引用,管理树中的父子关系,实际上Widget TreeRenderObject Tree 都是由Element Tree驱动生成的。
  • RenderObject:负责处理大小、布局和绘制工作,它将绘制自身,摆放子节点等。

这里需要注意:

  • 三棵树中, ElementWidget是 一 一 对应的,但 ElementRenderObject 不是 一 一 对应的。比如 StatelessWidgetStatefulWidget 都没有对应的 RenderObject
  • 渲染树在上屏前会生成一棵 Layer 树,因此在Flutter中实际上是有四棵树,但我们只需了解以上三棵树就行。

StatelessWidget

StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法:

StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其他 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget

下面是一个简单的例子:

class Echo extends StatelessWidget{ const Echo({ Key" />,required this.text,this.backgroundColor = Colors.grey, //默认为灰色}):super(key:key);final String text;final Color backgroundColor;Widget build(BuildContext context) { return Center(child: Container(color: backgroundColor,child: Text(text),),);}}

然后我们可以通过如下方式使用它:

 Widget build(BuildContext context) { return Echo(text: "hello world");}

按照惯例,widget构造函数参数应使用命名参数,命名参数中的必需要传的参数要添加required关键字,这样有利于静态代码分析器进行检查;在继承 widget 时,第一个参数通常应该是Key。另外,如果 widget 需要接收子 widget ,那么childchildren参数通常应被放在参数列表的最后。同样是按照惯例, widget属性应尽可能的被声明为final,防止被意外改变。

Context

build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widgetwidget 树中的上下文,每一个 widget 都会对应一个 context 对象。

实际上,context是当前 widgetwidget 树中位置中执行”相关操作“的一个句柄(handle), 比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。

下面是在子树中获取父级 widget 的一个示例:

class ContextRoute extends StatelessWidget{ Widget build(BuildContext context) { return Scaffold(appBar: AppBar(title: Text("Context测试"),),body: Container(child: Builder(builder: (context) { // 在 widget 树中向上查找最近的父级`Scaffold`widget Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();// 直接返回 AppBar的title, 此处实际上是Text("Context测试")return