Flutter 架构概述
对 Flutter 架构的高级概述,包括其设计背后的核心原则和概念。
本文旨在提供 Flutter 架构的高级概述,包括其设计背后的核心原则和概念。如果您对如何构建 Flutter 应用的架构感兴趣,请查看 构建 Flutter 应用架构。
Flutter 是一个跨平台 UI 工具包,旨在实现 iOS、Android、Web 和桌面等操作系统之间的代码复用,同时允许应用程序直接与底层平台服务进行交互。其目标是使开发人员能够交付高性能的应用程序,这些应用在不同平台上拥有自然的原生体验,既能兼顾各平台的差异,又能最大限度地共享代码。
在开发过程中,Flutter 应用运行在提供有状态热重载(stateful hot reload)的虚拟机(VM)中,无需完全重新编译即可看到变更。发布时,Flutter 应用会直接编译为机器码(Intel x64 或 ARM 指令集),若针对 Web 平台则编译为 JavaScript。该框架是开源的,采用宽松的 BSD 许可证,并拥有繁荣的第三方包生态系统,以补充核心库的功能。
本概述分为以下几个部分:
- 层模型:构建 Flutter 的各个部分。
- 响应式用户界面:Flutter 用户界面开发的核心概念。
- Widget 简介:Flutter 用户界面的基本构建块。
- 渲染过程:Flutter 如何将 UI 代码转换为像素。
- 平台嵌入层(Embedders)概述:让移动端和桌面端操作系统执行 Flutter 应用的代码。
- Flutter 与其他代码集成:关于 Flutter 应用可用的不同技术的信息。
- 对 Web 的支持:关于 Flutter 在浏览器环境中特性的总结性说明。
架构分层
#Flutter 被设计为一个可扩展的分层系统。它由一系列独立的库组成,每个库都依赖于其下方的层。没有任何一层拥有访问更底层层的特权,框架的每一部分都被设计为可选和可替换的。
对于底层操作系统,Flutter 应用程序的打包方式与任何其他原生应用程序相同。平台特定的嵌入层(Embedder)提供入口点;与底层操作系统协调以访问渲染表面、辅助功能和输入等服务;并管理消息事件循环。嵌入层使用适合该平台的语言编写:目前 Android 为 Java 和 C++,iOS 和 macOS 为 Swift 和 Objective-C/Objective-C++,Windows 和 Linux 为 C++。通过使用嵌入层,Flutter 代码可以作为模块集成到现有应用程序中,也可以作为应用程序的全部内容。Flutter 包含了针对常见目标平台的多个嵌入层,同时也存在其他嵌入层。
Flutter 的核心是 Flutter 引擎,它主要由 C++ 编写,提供了支持所有 Flutter 应用程序所需的基元。引擎负责在需要绘制新帧时对组合后的场景进行光栅化。它提供了 Flutter 核心 API 的底层实现,包括图形文本布局、文件和网络 I/O、Dart 运行时和编译工具链。
引擎通过 dart:ui 向 Flutter 框架公开,该库将底层的 C++ 代码封装为 Dart 类。该库公开了最低级别的基元,例如用于驱动输入、图形和文本渲染子系统的类。
通常,开发人员通过 Flutter 框架与 Flutter 进行交互,该框架提供了用 Dart 语言编写的现代响应式框架。它包含了一套丰富的平台、布局和基础库,由一系列层组成。从下到上依次为:
- 基础 基础类(Foundation),以及诸如 动画(animation)、绘制(painting) 和 手势(gestures) 等构建块服务,它们在底层基础上提供了常用的抽象。
- 渲染层(rendering layer) 提供了处理布局的抽象。使用此层,您可以构建一个可渲染对象的树。您可以动态操作这些对象,树会自动更新布局以反映您的更改。
- Widgets 层(widgets layer) 是一种组合抽象。渲染层中的每个渲染对象在 Widgets 层都有一个对应的类。此外,Widgets 层允许您定义可重用的类组合。这是引入响应式编程模型的层级。
- Material 和 Cupertino 库提供了一套全面的控件,它们使用 Widget 层的组合基元来实现 Material 或 iOS 设计语言。
Flutter 框架相对较小;开发人员可能使用的许多高级功能都实现为包,包括诸如 camera 和 webview 等平台插件,以及诸如 characters、http 和 animations 等基于 Dart 和 Flutter 核心库构建的平台无关功能。其中一些包来自更广泛的生态系统,涵盖了诸如 应用内支付、Apple 身份验证 和 动画 等服务。
本概述的其余部分将大致从 UI 开发的响应式范式开始,由下而上地深入各层。然后,我们将描述 Widget 是如何组合在一起并转换为可作为应用程序一部分进行渲染的对象的。我们将描述 Flutter 如何在平台层面与其他代码交互,最后简要总结 Flutter 的 Web 支持与其他目标平台的区别。
应用剖析
#下图概述了由 flutter create 生成的常规 Flutter 应用的组成部分。它展示了 Flutter 引擎在此堆栈中的位置,标注了 API 边界,并标识了各个组件所在的仓库。下方的图例说明了一些常用于描述 Flutter 应用组件的术语。
Dart 应用
- 将 Widget 组合成所需的 UI。
- 实现业务逻辑。
- 由应用开发者所有。
框架(Framework) (源代码)
- 提供用于构建高质量应用的高级 API(例如:Widget、命中测试、手势检测、辅助功能、文本输入)。
- 将应用的 Widget 树组合成一个场景。
引擎(Engine) (源代码)
- 负责对组合后的场景进行光栅化。
- 提供 Flutter 核心 API 的底层实现(例如:图形、文本布局、Dart 运行时)。
- 使用 dart:ui API 向框架公开其功能。
- 使用引擎的 Embedder API 与特定平台集成。
嵌入层(Embedder) (源代码)
- 与底层操作系统协调以访问渲染表面、辅助功能和输入等服务。
- 管理事件循环。
- 公开 平台特定 API 以将嵌入层集成到应用中。
Runner
- 将嵌入层平台特定 API 公开的部分组合成一个可在目标平台上运行的应用包。
- 由
flutter create生成的应用模板的一部分,由应用开发者所有。
响应式用户界面
#表面上看,Flutter 是 一种响应式、声明式的 UI 框架,开发者在其中提供从应用程序状态到接口状态的映射,当应用程序状态发生变化时,框架负责在运行时更新接口。该模型受到 Facebook React 框架相关工作 的启发,其中包括对许多传统设计原则的重新思考。
在大多数传统 UI 框架中,用户界面的初始状态被描述一次,然后在运行时响应事件时由用户代码单独更新。这种方法的一个挑战是,随着应用程序复杂性的增加,开发者需要了解状态变化如何在整个 UI 中级联。例如,考虑以下 UI:
有很多地方可以更改状态:颜色框、色相滑块、单选按钮。当用户与 UI 交互时,这些更改必须反映在所有其他地方。更糟糕的是,如果不小心,用户界面某一部分的微小更改可能会导致看似不相关的代码片段产生连锁反应。
解决这个问题的一种方法是 MVC 模式,通过控制器将数据更改推送到模型,然后模型通过控制器将新状态推送到视图。然而,这也有问题,因为创建和更新 UI 元素是两个独立的步骤,很容易导致不同步。
Flutter 与其他响应式框架一样,通过显式地将用户界面与其底层状态解耦来解决这个问题。使用 React 风格的 API,您只需创建 UI 描述,框架就会负责使用该配置来根据需要创建和/或更新用户界面。
在 Flutter 中,Widget(类似于 React 中的组件)由用于配置对象树的不可变类表示。这些 Widget 用于管理一个单独的布局对象树,该树随后又用于管理一个单独的组合对象树。Flutter 的核心是一系列用于高效遍历树中已修改部分的机制,将对象树转换为更低级别的对象树,并跨这些树传播更改。
Widget 通过重写 build() 方法来声明其用户界面,这是一个将状态转换为 UI 的函数:
UI = f(state)
build() 方法在设计上执行速度很快,且应无副作用,从而允许框架在需要时(潜在频率可高达每渲染帧一次)调用它。
这种方法依赖于语言运行时的某些特性(特别是快速的对象实例化和删除)。幸运的是,Dart 非常适合此任务。
组件
#如前所述,Flutter 强调 Widget 作为组合单元。Widget 是 Flutter 应用用户界面的构建块,每个 Widget 都是用户界面某一部分的不可变声明。
Widget 基于组合形成层级结构。每个 Widget 都嵌套在其父级内部,并可以从父级接收上下文。此结构一直延伸到根 Widget(托管 Flutter 应用的容器,通常是 MaterialApp 或 CupertinoApp),正如这个简单的示例所示:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My Home Page')),
body: Center(
child: Builder(
builder: (context) {
return Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Click!');
},
child: const Text('A button'),
),
],
);
},
),
),
),
);
}
}
在上述代码中,所有实例化的类都是 Widget。
应用通过告诉框架用另一个 Widget 替换层级结构中的某个 Widget 来响应事件(如用户交互)从而更新其用户界面。然后,框架会比较新旧 Widget,并高效地更新用户界面。
Flutter 对每个 UI 控件都有自己的实现,而不是推迟给系统提供的控件:例如,iOS 的 Toggle 控件和 Android 的等效控件都有纯 Dart 实现。
这种方法提供了几个好处:
- 提供了无限的可扩展性。想要 Switch 控件变体的开发者可以以任何方式创建它,而不受操作系统提供的扩展点的限制。
- 通过允许 Flutter 一次性组合整个场景,避免了在 Flutter 代码和平台代码之间来回切换的显著性能瓶颈。
- 将应用程序行为与任何操作系统依赖项解耦。无论操作系统如何更改其控件实现,应用程序在所有操作系统版本上看起来和感觉都一样。
组合
#Widget 通常由许多其他小型、单一用途的 Widget 组成,它们组合在一起产生强大的效果。
在可能的情况下,设计概念的数量保持在最低限度,同时允许总词汇量较大。例如,在 Widgets 层中,Flutter 使用相同的核心概念(Widget)来表示屏幕绘制、布局(定位和尺寸)、用户交互、状态管理、主题、动画和导航。在动画层中,Animation 和 Tween 这对概念涵盖了大部分设计空间。在渲染层中,RenderObject 用于描述布局、绘制、命中测试和辅助功能。在每种情况下,相应的词汇量最终都很大:有数百个 Widget 和渲染对象,以及数十种动画和 Tween 类型。
类层次结构故意设计得浅而广,以最大化可能的组合数量,专注于每个都做好一件事的小型、可组合的 Widget。核心功能是抽象的,即使像填充(padding)和对齐(alignment)这样的基本功能也是作为单独的组件实现的,而不是内置到核心中。(这也与更传统的 API 形成对比,在那些 API 中,填充等功能内置于每个布局组件的通用核心中。)因此,例如,要居中一个 Widget,您需要将其包装在 Center Widget 中,而不是调整概念上的 Align 属性。
有用于填充、对齐、行、列和网格的 Widget。这些布局 Widget 本身没有视觉表现。相反,它们唯一的目的是控制其他 Widget 布局的某个方面。 Flutter 还包括利用这种组合方法的实用 Widget。
例如,Container 这个常用的 Widget 由几个负责布局、绘制、定位和尺寸调整的 Widget 组成。具体来说,通过阅读源代码可以看到,Container 由 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform Widget 组成。Flutter 的一个定义特征是您可以深入任何 Widget 的源代码进行检查。因此,您无需子类化 Container 来生成自定义效果,而是可以以新颖的方式组合它和其他 Widget,或者仅使用 Container 作为灵感来创建新的 Widget。
构建 Widget
#如前所述,您通过重写 build() 函数以返回新的元素树来确定 Widget 的视觉表现。该树以更具体的术语表示 Widget 的用户界面部分。例如,工具栏 Widget 可能有一个 build 函数,返回一些 文本 和 各种 按钮 的 水平布局。根据需要,框架递归地要求每个 Widget 构建,直到树完全由 具体可渲染对象 描述。然后,框架将这些可渲染对象缝合在一起,形成一个可渲染对象树。
Widget 的 build 函数应无副作用。无论 Widget 先前返回了什么,每当函数被要求构建时,它都应该返回一个新的 Widget 树1。框架会完成繁重的工作,根据渲染对象树(稍后会详细描述)确定需要调用哪些 build 方法。有关此过程的更多信息,可以在 深入 Flutter 主题 中找到。
在每一渲染帧上,Flutter 都可以通过调用该 Widget 的 build() 方法,仅重新创建状态发生变化的那部分 UI。因此,build 方法必须能够快速返回,而繁重的计算工作应以异步方式完成,然后存储为状态的一部分,以供 build 方法使用。
虽然这种方法相对简单,但这种自动比较非常有效,实现了高性能、交互式的应用。此外,build 函数的设计通过专注于声明 Widget 的组成,而不是从一种状态更新到另一种状态的复杂性,简化了您的代码。
Widget 状态
#该框架引入了两类主要的 Widget:有状态(stateful) 和 无状态(stateless) Widget。
许多 Widget 没有可变状态:它们没有任何随时间变化的属性(例如图标或标签)。这些 Widget 子类化 StatelessWidget。
然而,如果 Widget 的独特特性需要根据用户交互或其他因素发生变化,则该 Widget 是 有状态的。例如,如果一个 Widget 有一个在用户点击按钮时递增的计数器,那么该计数器的值就是该 Widget 的状态。当该值发生变化时,Widget 需要被重建以更新其 UI 部分。这些 Widget 子类化 StatefulWidget,并且(因为 Widget 本身是不可变的)它们将可变状态存储在一个单独的子类化 State 的类中。 StatefulWidget 没有 build 方法;相反,它们的用户界面是通过其 State 对象构建的。
每当您修改 State 对象时(例如,通过递增计数器),您必须调用 setState(),以通知框架通过再次调用 State 的 build 方法来更新用户界面。
拥有单独的状态和 Widget 对象可以让其他 Widget 以完全相同的方式处理无状态和有状态 Widget,而不必担心丢失状态。父级无需保留子级以保持其状态,可以在任何时间创建子级的新实例,而不会丢失子级的持久状态。框架负责在适当时查找和重用现有状态对象的所有工作。
状态管理
#那么,如果许多 Widget 可以包含状态,状态是如何管理和在系统中传递的呢?
与其他任何类一样,您可以在 Widget 中使用构造函数来初始化其数据,因此 build() 方法可以确保任何子 Widget 都在实例化时使用它所需的数据:
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
其中 importantState 是包含对 Widget 很重要的状态的类的占位符。
然而,随着 Widget 树变得越来越深,在树层级结构中上下传递状态信息变得非常繁琐。因此,第三种 Widget 类型 InheritedWidget 提供了一种从共享祖先获取数据的简便方法。您可以使用 InheritedWidget 创建一个状态 Widget,将通用祖先包装在 Widget 树中,如本例所示:
每当 ExamWidget 或 GradeWidget 对象需要来自 StudentState 的数据时,它现在可以使用如下命令访问它:
final studentState = StudentState.of(context);
of(context) 调用会获取构建上下文(指向当前 Widget 位置的句柄),并返回 树中匹配 StudentState 类型的最近祖先。 InheritedWidget 还提供了一个 updateShouldNotify() 方法,Flutter 会调用它来确定状态更改是否应触发使用该数据的子 Widget 重建。
Flutter 本身在框架中广泛使用 InheritedWidget 来处理共享状态,例如应用程序的 视觉主题,其中包括在整个应用程序中普遍存在的 颜色和类型样式等属性。 MaterialApp 的 build() 方法在构建时将主题插入树中,然后在层级结构的更深处,Widget 可以使用 .of() 方法查找相关的主题数据。
例如
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.titleLarge,
),
);
随着应用程序的发展,减少创建和使用有状态 Widget 的仪式感的高级状态管理方法变得更具吸引力。许多 Flutter 应用使用诸如 provider 之类的实用包,它提供了 InheritedWidget 的封装。Flutter 的分层架构还支持将状态转换为 UI 的替代方法,例如 flutter_hooks 包。
渲染与布局
#本节描述了渲染流水线,这是 Flutter 将 Widget 层级结构转换为屏幕上绘制的实际像素所采取的一系列步骤。
Flutter 的渲染模型
#您可能在想:如果 Flutter 是一个跨平台框架,那么它如何提供与单平台框架相当的性能?
从传统 Android 应用的工作方式开始思考是很有用的。绘制时,首先调用 Android 框架的 Java 代码。Android 系统库提供负责将自身绘制到 Canvas 对象的组件,然后 Android 可以使用 Skia(一个用 C/C++ 编写的图形引擎,调用 CPU 或 GPU 在设备上完成绘制)进行渲染。
跨平台框架 通常 通过在底层原生 Android 和 iOS UI 库上创建抽象层来工作,试图平滑每个平台表示的差异。应用代码通常用 JavaScript 等解释型语言编写,它反过来必须与基于 Java 的 Android 或基于 Objective-C 的 iOS 系统库交互以显示 UI。所有这些都会增加开销,在 UI 和应用逻辑之间有大量交互时,这种开销可能会很显著。
相比之下,Flutter 最小化了这些抽象,绕过系统 UI Widget 库,转而使用自己的 Widget 集。绘制 Flutter 视觉效果的 Dart 代码被编译为原生代码,该代码使用 Impeller 进行渲染。Impeller 随应用程序一起发布,允许开发者升级他们的应用以保持最新的性能改进,即使手机没有更新到新的 Android 版本。对于 Flutter 在其他原生平台(如 Windows 或 macOS)上的情况也是如此。
从用户输入到 GPU
#Flutter 应用于其渲染流水线的首要原则是:简单即快。Flutter 有一条直接的流水线来决定数据如何流向系统,如下图所示:
让我们更详细地了解一下其中的一些阶段。
构建:从 Widget 到 Element
#考虑这个演示 Widget 层级结构的代码片段:
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
当 Flutter 需要渲染此片段时,它会调用 build() 方法,该方法返回一个基于当前应用状态渲染 UI 的 Widget 子树。在此过程中,build() 方法可以根据状态按需引入新的 Widget。作为一个例子,在上述代码片段中,Container 具有 color 和 child 属性。通过查看 Container 的 源代码,您可以看到,如果颜色不为 null,它会插入一个表示该颜色的 ColoredBox。
if (color != null)
current = ColoredBox(color: color!, child: current);
相应地,Image 和 Text Widget 可能会在构建过程中插入诸如 RawImage 和 RichText 的子 Widget。因此,最终的 Widget 层级结构可能会比代码表示的更深,如下所示2:
这就是为什么当您通过调试工具(如作为 Flutter/Dart DevTools 一部分的 Flutter inspector)检查树时,可能会看到比原始代码中深得多的结构。
在构建阶段,Flutter 将代码中表达的 Widget 转换为对应的 元素树(element tree),每个 Widget 对应一个元素。每个元素代表树层级中给定位置的 Widget 的特定实例。元素有两种基本类型:
ComponentElement:其他元素的宿主。-
RenderObjectElement:参与布局或绘制阶段的元素。
RenderObjectElement 是其 Widget 模拟对象与底层 RenderObject(我们稍后会提到)之间的中介。
任何 Widget 的元素都可以通过其 BuildContext 引用,这是 Widget 在树中位置的句柄。这就是函数调用(如 Theme.of(context))中的 context,并作为参数提供给 build() 方法。
由于 Widget 是不可变的(包括节点之间的父/子关系),对 Widget 树的任何更改(例如在前面的例子中将 Text('A') 更改为 Text('B'))都会导致返回一组新的 Widget 对象。但这并不意味着必须重建底层表示。元素树在帧与帧之间是持久的,因此起到了关键的性能作用,允许 Flutter 表现得好像 Widget 层级结构是完全可丢弃的,同时缓存其底层表示。通过仅遍历已更改的 Widget,Flutter 可以仅重建元素树中需要重新配置的部分。
布局与渲染
#只绘制单个 Widget 的应用程序很少见。因此,任何 UI 框架的重要部分都是能够高效地布局 Widget 层级结构,在它们在屏幕上渲染之前确定每个元素的尺寸和位置。
渲染树中每个节点的基类是 RenderObject,它定义了布局和绘制的抽象模型。这是极其通用的:它不承诺固定的维度数量,甚至不承诺笛卡尔坐标系(如 这个极坐标系的示例 所演示)。每个 RenderObject 都知道它的父级,但对子级知之甚少,除了如何 访问 它们及其约束。这为 RenderObject 提供了足够的抽象能力来处理各种用例。
在构建阶段,Flutter 会为元素树中的每个 RenderObjectElement 创建或更新一个继承自 RenderObject 的对象。 RenderObject 是基元:RenderParagraph 渲染文本,RenderImage 渲染图像,而 RenderTransform 在绘制其子级之前应用转换。
大多数 Flutter Widget 由继承自 RenderBox 子类的对象渲染,该子类表示 2D 笛卡尔空间中固定尺寸的 RenderObject。 RenderBox 提供了 盒子约束模型(box constraint model) 的基础,为每个要渲染的 Widget 建立了最小和最大宽度及高度。
为了执行布局,Flutter 以深度优先遍历方式遍历渲染树,并将 尺寸约束(size constraints) 从父级传递给子级。在确定其尺寸时,子级 必须 尊重父级给出的约束。子级通过在父级建立的约束范围内 向上传递尺寸 来响应。
在完成对树的单次遍历后,每个对象在父级的约束内都有一个定义的尺寸,并准备通过调用 paint() 方法进行绘制。
盒子约束模型作为一种以 O(n) 时间布局对象的方式非常强大:
- 父级可以通过将最大和最小约束设置为相同的值来指定子对象的大小。例如,手机应用中最顶层的渲染对象会将子级限制为屏幕大小。(子级可以选择如何使用该空间。例如,它们可能只是将想要渲染的内容居中在指定的约束内。)
- 父级可以规定子级的宽度,但在高度上给予子级灵活性(或者规定高度但在宽度上提供灵活性)。一个现实世界的例子是流式文本,它可能必须符合水平约束,但根据文本数量垂直变化。
即使子对象需要知道它有多少可用空间来决定如何渲染其内容,此模型也能工作。通过使用 LayoutBuilder Widget,子对象可以检查传递下来的约束并使用它们来决定如何使用它们,例如:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return const OneColumnLayout();
} else {
return const TwoColumnLayout();
}
},
);
}
关于约束和布局系统的更多信息,以及工作示例,可以在 理解约束 主题中找到。
所有 RenderObject 的根是 RenderView,它代表渲染树的总输出。当平台要求渲染新帧时(例如,因为 vsync 或因为纹理解压/上传完成),会调用渲染树根处的 RenderView 对象中 compositeFrame() 方法。这将创建一个 SceneBuilder 来触发场景的更新。当场景完成后,RenderView 对象将组合后的场景传递给 dart:ui 中的 Window.render() 方法,该方法将控制权交给 GPU 进行渲染。
流水线组合和光栅化阶段的进一步细节超出了这篇高级文章的范围,但可以在 这次关于 Flutter 渲染流水线的讲座 中找到更多信息。
平台嵌入层
#正如我们所见,Flutter 用户界面不是被转换为等效的 OS Widget,而是由 Flutter 本身构建、布局、组合和绘制的。获取纹理并参与底层操作系统应用生命周期的机制不可避免地根据该平台的独特考量而变化。引擎是与平台无关的,呈现了一个 稳定的 ABI (应用程序二进制接口),为 平台嵌入层 提供了设置和使用 Flutter 的方法。
平台嵌入层是托管所有 Flutter 内容的原生 OS 应用程序,充当宿主操作系统和 Flutter 之间的粘合剂。当您启动 Flutter 应用时,嵌入层提供入口点,初始化 Flutter 引擎,获取 UI 和光栅化线程,并创建 Flutter 可以写入的纹理。嵌入层还负责应用生命周期,包括输入手势(如鼠标、键盘、触摸)、窗口大小调整、线程管理和平台消息。Flutter 包含了适用于 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层;您还可以创建自定义平台嵌入层,如 这个支持通过 VNC 风格帧缓冲区远程处理 Flutter 会话的工作示例 或 这个树莓派工作示例。
每个平台都有自己的一套 API 和限制。一些简短的平台特定说明:
- 截至 Flutter 3.29,UI 和平台线程在 iOS 和 Android 上已合并。具体来说,UI 线程被移除,Dart 代码在原生平台线程上运行。有关更多信息,请参阅 伟大的线程合并 视频。
- 在 iOS 和 macOS 上,Flutter 分别作为
UIViewController或NSViewController加载到嵌入层中。平台嵌入层创建一个FlutterEngine,作为 Dart VM 和您的 Flutter 运行时的宿主;以及一个FlutterViewController,它连接到FlutterEngine以将 UIKit 或 Cocoa 输入事件传递到 Flutter 中,并使用 Metal 或 OpenGL 显示由FlutterEngine渲染的帧。 - 在 Android 上,默认情况下,Flutter 作为
Activity加载到嵌入层中。视图由FlutterView控制,它根据 Flutter 内容的组合和 z-order 要求,将 Flutter 内容渲染为视图或纹理。 - 在 Windows 上,Flutter 托管在传统的 Win32 应用中,内容使用 ANGLE 渲染,这是一个将 OpenGL API 调用转换为 DirectX 11 等效项的库。
与其他代码集成
#无论您是访问用 Kotlin 或 Swift 等语言编写的代码或 API、调用原生 C API、在 Flutter 应用中嵌入原生控件,还是将 Flutter 嵌入到现有应用程序中,Flutter 都提供了多种互操作性机制。
平台通道 (Platform channels)
#对于移动端和桌面端应用,Flutter 允许您通过 平台通道 (platform channel) 调用自定义代码,这是一种在 Dart 代码和宿主应用的平台特定代码之间进行通信的机制。通过创建公共通道(封装名称和编解码器),您可以在 Dart 和用 Kotlin 或 Swift 等语言编写的平台组件之间发送和接收消息。数据从 Dart 类型(如 Map)序列化为标准格式,然后反序列化为 Kotlin(如 HashMap)或 Swift(如 Dictionary)中的等效表示。
以下是一个短小的平台通道示例,展示了从 Dart 调用 Kotlin (Android) 或 Swift (iOS) 中的接收事件处理程序:
// Dart side
const channel = MethodChannel('foo');
final greeting = await channel.invokeMethod('bar', 'world') as String;
print(greeting);
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// iOS (Swift)
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}
使用平台通道的更多示例(包括桌面平台的示例)可以在 flutter/packages 仓库中找到。还有 数千个已有的 Flutter 插件 涵盖了许多常见场景,从 Firebase 到广告再到相机和蓝牙等设备硬件。
外部函数接口 (FFI)
#对于基于 C 的 API,包括可以为用 Rust 或 Go 等现代语言编写的代码生成的 API,Dart 提供了一种使用 dart:ffi 库绑定到原生代码的直接机制。外部函数接口 (FFI) 模型可能比平台通道快得多,因为传递数据不需要序列化。相反,Dart 运行时提供了在堆上分配由 Dart 对象支持的内存并调用静态或动态链接库的能力。FFI 可用于除 Web 之外的所有平台,在 Web 上,JS 互操作库 和 package:web 有类似用途。
要使用 FFI,您需要为每个 Dart 和非托管方法签名创建一个 typedef,并指示 Dart VM 在它们之间进行映射。作为一个例子,这里有一段调用传统 Win32 MessageBox() API 的代码片段:
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // contains .toNativeUtf16() extension method
typedef MessageBoxNative =
Int32 Function(
IntPtr hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
Int32 uType,
);
typedef MessageBoxDart =
int Function(
int hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
int uType,
);
void exampleFfi() {
final user32 = DynamicLibrary.open('user32.dll');
final messageBox = user32.lookupFunction<MessageBoxNative, MessageBoxDart>(
'MessageBoxW',
);
final result = messageBox(
0, // No owner window
'Test message'.toNativeUtf16(), // Message
'Window caption'.toNativeUtf16(), // Window title
0, // OK button only
);
}
在 Flutter 应用中渲染原生控件
#由于 Flutter 内容被绘制到纹理中,并且其 Widget 树完全是内部的,因此没有地方可以让 Android 视图之类的东西存在于 Flutter 的内部模型中或交错在 Flutter Widget 中。对于想要在 Flutter 应用中包含现有平台组件(例如浏览器控件)的开发者来说,这是一个问题。
Flutter 通过引入平台视图 Widget(AndroidView 和 UiKitView)解决了这个问题,让您可以在每个平台上嵌入此类内容。平台视图可以与其他 Flutter 内容集成3。每个此类 Widget 都是底层操作系统的中介。例如,在 Android 上,AndroidView 具有三个主要功能:
- 在每次绘制帧时,制作原生视图渲染的图形纹理副本,并将其呈现给 Flutter,以便作为 Flutter 渲染表面的一部分进行组合。
- 响应命中测试和输入手势,并将其转换为等效的原生输入。
- 创建辅助功能树的模拟,并在原生层和 Flutter 层之间传递命令和响应。
不可避免地,这种同步会带来一定的开销。因此,通常情况下,这种方法最适合 Google Maps 这样复杂的控件,因为在 Flutter 中重新实现它们是不切实际的。
通常,Flutter 应用会基于平台测试在 build() 方法中实例化这些 Widget。例如,来自 google_maps_flutter 插件的代码:
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin',
);
与 AndroidView 或 UiKitView 底层的原生代码通信通常使用如前所述的平台通道机制。
目前,平台视图在桌面平台上不可用,但这并非架构限制;未来可能会添加支持。
在宿主应用中托管 Flutter 内容
#前述场景的反面是将 Flutter Widget 嵌入到现有的 Android 或 iOS 应用中。如前一节所述,在移动设备上运行的新创建的 Flutter 应用托管在 Android Activity 或 iOS UIViewController 中。可以使用相同的嵌入 API 将 Flutter 内容嵌入到现有的 Android 或 iOS 应用中。
Flutter 模块模板旨在实现轻松嵌入;您可以将其作为源依赖项嵌入到现有的 Gradle 或 Xcode 构建定义中,也可以将其编译为 Android Archive 或 iOS Framework 二进制文件使用,而无需每个开发者都安装 Flutter。
Flutter 引擎初始化需要一点时间,因为它需要加载 Flutter 共享库、初始化 Dart 运行时、创建并运行 Dart 隔离区(isolate),并将渲染表面附加到 UI。为了最小化呈现 Flutter 内容时的任何 UI 延迟,最好在整体应用初始化序列期间初始化 Flutter 引擎,或者至少在第一个 Flutter 屏幕之前初始化,以便用户不会在加载第一个 Flutter 代码时突然停顿。此外,分离 Flutter 引擎允许它在多个 Flutter 屏幕之间重用,并分担加载必要库所涉及的内存开销。
有关如何将 Flutter 加载到现有 Android 或 iOS 应用中的更多信息,可以在 加载序列、性能和内存主题 中找到。
Flutter Web 支持
#虽然通用架构概念适用于 Flutter 支持的所有平台,但 Flutter 的 Web 支持有一些独特的特性值得一提。
只要该语言存在,Dart 就一直能够编译为 JavaScript,其工具链同时针对开发和生产目的进行了优化。许多重要的应用目前从 Dart 编译为 JavaScript 并在生产环境中运行,包括 Google Ads 的广告商工具。由于 Flutter 框架是用 Dart 编写的,将其编译为 JavaScript 相对直接。
然而,用 C++ 编写的 Flutter 引擎旨在与底层操作系统而不是 Web 浏览器交互。因此需要一种不同的方法。
在 Web 上,Flutter 提供两种渲染器:
| 渲染器 | 编译目标 |
|---|---|
| CanvasKit | JavaScript |
| Skwasm | WebAssembly |
构建模式(Build modes) 是命令行选项,用于决定运行应用时可用的渲染器。
Flutter 提供两种 构建 模式:
| 构建模式 | 可用渲染器 |
|---|---|
| default | CanvasKit |
| `--wasm` | Skwasm(首选),CanvasKit(回退) |
默认模式仅提供 CanvasKit 渲染器。 --wasm 选项使两种渲染器都可用,并根据浏览器能力选择引擎:如果浏览器有能力运行它,则首选 Skwasm,否则回退到 CanvasKit。
与 Flutter 运行的其他平台相比,最显著的区别可能是 Flutter 不需要提供 Dart 运行时。相反,Flutter 框架(以及您编写的任何代码)被编译为 JavaScript。还值得注意的是,Dart 在其所有模式(JIT 与 AOT,原生与 Web 编译)下的语言语义差异极小,大多数开发者永远不会编写一行会遇到这种差异的代码。
在开发阶段,Flutter Web 使用 dartdevc,这是一个支持增量编译并因此允许热重启(hot restart)和 [behind a flag 的热重载][] 的编译器。相反,当您准备好为 Web 创建生产应用时,将使用 Dart 高度优化的生产 JavaScript 编译器 dart2js,将 Flutter 核心和框架以及您的应用程序打包到一个可以部署到任何 Web 服务器的最小化源文件中。代码可以通过 延迟导入 在单个文件中提供或拆分为多个文件。
有关 Flutter Web 的更多信息,请查看 Flutter 的 Web 支持 和 Web 渲染器。
更多信息
#对于那些有兴趣了解更多关于 Flutter 内部机制的人,深入 Flutter 白皮书提供了该框架设计理念的有用指南。