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 要支持新的平台,则需要针对该新的平台编写一个嵌入层。
通常来说,开发者不需要感知到Engine和Embedder的存在(如果不需要调用平台的系统服务),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
类继承自DiagnosticableTree
,DiagnosticableTree
即 “诊断树”,主要作用是提供调试信息。Key
: 这个key
属性类似于React/Vue
中的key
,主要的作用是决定是否在下一次build
时复用旧的widget
,决定的条件在canUpdate()
方法中。createElement()
:正如前文所述“一个widget
可以对应多个Element
”;Flutter 框架在构建UI树时,会先调用此方法生成对应节点的Element
对象。此方法是 Flutter 框架隐式调用的,在我们开发过程中基本不会调用到。debugFillProperties(...)
复写父类的方法,主要是设置诊断树的一些特性。canUpdate(...)
是一个静态方法,它主要用于 在 widget 树重新build时复用旧的 widget ,其实具体来说,应该是:是否用新的 widget 对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidget
与oldWidget
的runtimeType
和key
同时相等时就会用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 框架的的处理流程是这样的:
- 根据
Widget
树生成一个Element
树,Element
树中的节点都继承自Element
类。 - 根据
Element
树生成Render
树(渲染树),渲染树中的节点都继承自RenderObject
类。 - 根据渲染树生成
Layer
树,然后上屏显示,Layer
树中的节点都继承自Layer
类。
真正的布局和渲染逻辑在 Render
树中,Element
是 Widget
和 RenderObject
的粘合剂,可以理解为一个中间代理。
其中三棵树的各自的作用是:
Widget
:负责配置,为Element
描述UI的配置信息,拥有公开的Api,这部分也是开发者可以直接感知和使用的部分。Element
:Flutter Virtual DOM 的管理者,它管理widget
的生命周期,代表了内存中真实存在的UI数据,它负责树中特定位置的widget
的实例化,持有widget
的引用,管理树中的父子关系,实际上Widget Tree
和RenderObject Tree
都是由Element Tree
驱动生成的。RenderObject
:负责处理大小、布局和绘制工作,它将绘制自身,摆放子节点等。
这里需要注意:
- 三棵树中,
Element
和Widget
是 一 一 对应的,但Element
和RenderObject
不是 一 一 对应的。比如StatelessWidget
和StatefulWidget
都没有对应的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
,那么child
或children
参数通常应被放在参数列表的最后。同样是按照惯例,widget
的属性应尽可能的被声明为final
,防止被意外改变。
Context
build
方法有一个context
参数,它是BuildContext
类的一个实例,表示当前 widget
在 widget
树中的上下文,每一个 widget
都会对应一个 context
对象。
实际上,context
是当前 widget
在 widget
树中位置中执行”相关操作“的一个句柄(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