跳至主要内容

Flutter 架构概述

本文旨在提供 Flutter 架构的高级概述,包括构成其设计的核心原则和概念。

Flutter 是一款跨平台 UI 工具包,旨在允许在 iOS 和 Android 等操作系统之间重用代码,同时还允许应用程序直接与底层平台服务交互。其目标是使开发人员能够交付在不同平台上感觉自然的、高性能的应用程序,在存在差异的地方拥抱差异,同时尽可能地共享代码。

在开发过程中,Flutter 应用程序在提供状态热重载的 VM 中运行,无需完全重新编译即可更改。对于发布版本,Flutter 应用程序会直接编译为机器代码,无论是 Intel x64 或 ARM 指令,还是针对 Web 时编译为 JavaScript。该框架是开源的,具有宽松的 BSD 许可证,并且拥有一个蓬勃发展的第三方包生态系统,可以补充核心库功能。

此概述分为多个部分

  1. **分层模型**:构建 Flutter 的组件。
  2. **响应式用户界面**:Flutter 用户界面开发的核心概念。
  3. **Widget** 简介:Flutter 用户界面的基本构建块。
  4. **渲染过程**:Flutter 如何将 UI 代码转换为像素。
  5. **平台嵌入器**概述:允许移动和桌面操作系统执行 Flutter 应用程序的代码。
  6. **将 Flutter 与其他代码集成**:有关 Flutter 应用程序可用的不同技术的相关信息。
  7. **对 Web 的支持**:关于浏览器环境中 Flutter 特性的结论性说明。

架构层

#

Flutter 被设计为一个可扩展的分层系统。它以一系列独立的库的形式存在,每个库都依赖于底层。没有任何一层可以访问下一层,并且框架级别的每个部分都设计为可选且可替换的。

Architectural
diagram

对于底层操作系统,Flutter 应用程序的打包方式与任何其他原生应用程序相同。平台特定的嵌入器提供了一个入口点;协调与底层操作系统的交互以访问渲染表面、辅助功能和输入等服务;并管理消息事件循环。嵌入器是用适合平台的语言编写的:目前 Android 使用 Java 和 C++,iOS 和 macOS 使用 Objective-C/Objective-C++,Windows 和 Linux 使用 C++。使用嵌入器,Flutter 代码可以作为模块集成到现有应用程序中,或者代码可能是应用程序的全部内容。Flutter 包括许多针对常见目标平台的嵌入器,但也存在其他嵌入器

Flutter 的核心是**Flutter 引擎**,它主要用 C++ 编写,并支持所有 Flutter 应用程序所需的基元。引擎负责在需要绘制新帧时光栅化合成场景。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Impeller 在 iOS 上,并即将推出 Android 和 macOS 版本,以及在其他平台上使用 Skia)、文本布局、文件和网络 I/O、辅助功能支持、插件架构以及 Dart 运行时和编译工具链。

引擎通过 dart:ui 公开给 Flutter 框架,该框架将底层 C++ 代码包装在 Dart 类中。此库公开了最低级的基元,例如用于驱动输入、图形和文本渲染子系统的类。

通常,开发人员通过**Flutter 框架**与 Flutter 交互,该框架提供了一个用 Dart 语言编写的现代响应式框架。它包含一组丰富的平台、布局和基础库,由一系列层组成。从下到上,我们有

  • 基本的**基础**类和构建块服务,例如**动画绘制手势**,它们提供了对底层基础的常用抽象。
  • **渲染层**提供了一个用于处理布局的抽象。使用此层,您可以构建一个可渲染对象的树。您可以动态地操作这些对象,树会自动更新布局以反映您的更改。
  • **Widget 层**是一个组合抽象。渲染层中的每个渲染对象在 Widget 层中都有一个对应的类。此外,Widget 层允许您定义可以重用的类的组合。这是引入响应式编程模型的层。
  • **Material** 和 **Cupertino** 库提供了一套全面的控件,这些控件使用 Widget 层的组合基元来实现 Material 或 iOS 设计语言。

Flutter 框架相对较小;开发人员可能使用的许多更高级别的功能都作为包实现,包括平台插件,如 相机WebView,以及平台无关的功能,如 字符Http动画,它们构建在核心 Dart 和 Flutter 库之上。其中一些包来自更广泛的生态系统,涵盖诸如 应用内支付Apple 身份验证动画 等服务。

此概述的其余部分大致从 UI 开发的响应式范式开始,向下遍历各层。然后,我们描述 Widget 如何组合在一起并转换为可以作为应用程序一部分渲染的对象。我们描述了 Flutter 如何在平台级别与其他代码进行互操作,然后简要总结了 Flutter 的 Web 支持与其他目标有何不同。

应用的结构

#

下图概述了由 flutter create 生成的常规 Flutter 应用组成的部分。它显示了 Flutter 引擎在此堆栈中的位置,突出了 API 边界,并标识了各个部分所在的存储库。下面的图例阐明了一些用于描述 Flutter 应用部分的常用术语。

The layers of a Flutter app created by "flutter create": Dart app, framework, engine, embedder, runner

Dart 应用

  • 将 Widget 组合成所需的 UI。
  • 实现业务逻辑。
  • 由应用开发人员拥有。

**框架** (源代码)

  • 提供更高级别的 API 以构建高质量的应用(例如,Widget、点击测试、手势检测、辅助功能、文本输入)。
  • 将应用的 Widget 树合成到场景中。

**引擎** (源代码)

  • 负责光栅化合成场景。
  • 提供 Flutter 核心 API 的底层实现(例如,图形、文本布局、Dart 运行时)。
  • 使用**dart:ui API** 将其功能公开给框架。
  • 使用引擎的**嵌入器 API** 与特定平台集成。

**嵌入器** (源代码)

  • 协调与底层操作系统的交互以访问渲染表面、辅助功能和输入等服务。
  • 管理事件循环。
  • 公开**平台特定的 API** 以将嵌入器集成到应用中。

运行器

  • 将嵌入器的平台特定 API 公开的组件组合到目标平台上可运行的应用包中。
  • flutter create 生成的应用模板的一部分,由应用开发人员拥有。

反应式用户界面

#

从表面上看,Flutter 是一个响应式、声明式 UI 框架,其中开发人员提供应用程序状态到界面状态的映射,并且框架承担在运行时应用程序状态发生变化时更新界面的任务。此模型受到 Facebook 为其自己的 React 框架所做的工作 的启发,其中包括对许多传统设计原则的重新思考。

在大多数传统的 UI 框架中,用户界面的初始状态被描述一次,然后由用户代码在运行时响应事件单独更新。这种方法的一个挑战是,随着应用程序复杂性的增加,开发人员需要了解状态变化如何在整个 UI 中级联。例如,考虑以下 UI

Color picker dialog

有许多地方可以更改状态:颜色框、色相滑块、单选按钮。当用户与 UI 交互时,更改必须反映在其他每个地方。更糟糕的是,除非小心谨慎,否则对用户界面一部分的微小更改会导致级联效应,影响到看似无关的代码部分。

对此的一种解决方案是类似 MVC 的方法,您通过控制器将数据更改推送到模型,然后模型通过控制器将新状态推送到视图。但是,这也有问题,因为创建和更新 UI 元素是两个独立的步骤,很容易不同步。

Flutter 以及其他响应式框架对这个问题采用了另一种方法,通过明确地将用户界面与其底层状态解耦。使用 React 风格的 API,您只需创建 UI 描述,框架就会负责使用该配置来创建和/或更新用户界面。

在 Flutter 中,Widget(类似于 React 中的组件)由不可变的类表示,这些类用于配置对象树。这些 Widget 用于管理一个用于布局的单独对象树,然后该树用于管理一个用于合成的单独对象树。Flutter 本质上是一系列机制,用于有效地遍历树的修改部分,将对象树转换为更低级别的对象树,并在这些树之间传播更改。

Widget 通过覆盖 build() 方法声明其用户界面,该方法是一个将状态转换为 UI 的函数

UI = f(state)

build() 方法设计为快速执行并且不应有任何副作用,允许框架在需要时(可能在每个渲染帧中调用一次)调用它。

这种方法依赖于语言运行时的某些特性(特别是快速的对象实例化和删除)。幸运的是,Dart 特别适合此任务

Widgets

#

如前所述,Flutter 强调 Widget 作为组合单元。Widget 是 Flutter 应用用户界面的构建块,每个 Widget 都是用户界面一部分的不可变声明。

Widget 基于组合形成层次结构。每个 Widget 嵌套在其父级内部,并可以从父级接收上下文。这种结构一直延续到根 Widget(承载 Flutter 应用的容器,通常是 MaterialAppCupertinoApp),如以下简单示例所示

dart
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。

应用程序通过告诉框架用另一个小部件替换层次结构中的一个小部件来响应事件(例如用户交互)更新其用户界面。然后,框架比较新旧小部件,并有效地更新用户界面。

Flutter 拥有自己每个 UI 控件的实现,而不是依赖系统提供的实现:例如,它有一个纯 Dart 实现,分别用于 iOS 切换控件Android 的 等效控件

这种方法提供了几个好处

  • 提供无限的可扩展性。想要 Switch 控件变体的开发者可以用任何任意方式创建它,并且不受操作系统提供的扩展点的限制。
  • 通过允许 Flutter 一次合成整个场景来避免重大的性能瓶颈,而无需在 Flutter 代码和平台代码之间来回切换。
  • 将应用程序行为与任何操作系统依赖项分离。即使操作系统更改了其控件的实现,应用程序在所有版本的 OS 上的外观和感觉都相同。

组合

#

小部件通常由许多其他小型、单用途的小部件组成,这些小部件组合在一起产生强大的效果。

在可能的情况下,设计概念的数量保持最少,同时允许总词汇量很大。例如,在小部件层,Flutter 使用相同的核心概念(一个 Widget)来表示绘制到屏幕、布局(定位和大小)、用户交互、状态管理、主题、动画和导航。在动画层,一对概念 AnimationTween 涵盖了大部分设计空间。在渲染层,RenderObject 用于描述布局、绘制、命中测试和辅助功能。在每种情况下,相应的词汇量最终都会很大:有数百个小部件和渲染对象,以及数十种动画和补间类型。

类层次结构故意设计得浅而广,以最大化可能的组合数量,专注于小型、可组合的小部件,每个小部件都能做好一件事。核心功能是抽象的,即使像填充和对齐这样的基本功能也是作为单独的组件实现的,而不是内置到核心功能中。(这与更传统的 API 形成对比,在更传统的 API 中,像填充这样的功能内置于每个布局组件的通用核心。)因此,例如,要居中一个小部件,而不是调整名义上的 Align 属性,你需要将其包装在一个 Center 小部件中。

有用于填充、对齐、行、列和网格的小部件。这些布局小部件本身没有视觉表示。相反,它们的唯一目的是控制另一个小部件布局的某些方面。Flutter 还包含利用这种组合方法的实用程序小部件。

例如,Container(一个常用的小部件)由几个负责布局、绘制、定位和大小的小部件组成。具体来说,Container 由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 小部件组成,如阅读其源代码所示。Flutter 的一个定义特征是,您可以深入了解任何小部件的源代码并检查它。因此,与其子类化 Container 以产生自定义效果,不如以新颖的方式组合它和其他小部件,或者仅使用 Container 作为灵感创建一个新小部件。

构建 Widget

#

如前所述,您可以通过覆盖 build() 函数以返回一个新的元素树来确定小部件的视觉表示。此树以更具体的术语表示小部件的用户界面的一部分。例如,工具栏小部件可能有一个 build 函数,该函数返回一些 文本各种 按钮水平布局。根据需要,框架递归地要求每个小部件构建,直到树完全由 具体的可渲染对象 描述。然后,框架将可渲染对象拼接成一个可渲染对象树。

小部件的 build 函数应没有副作用。无论何时请求该函数构建,小部件都应返回一个新的 widget 树[1],而不管小部件之前返回了什么。框架承担了繁重的任务,根据渲染对象树(稍后详细介绍)确定需要调用哪些构建方法。有关此过程的更多信息,请参阅 Flutter 内部主题

在每个渲染帧上,Flutter 可以通过调用该小部件的 build() 方法仅重新创建状态已更改的 UI 部分。因此,重要的是构建方法应快速返回,并且繁重的计算工作应以某种异步方式完成,然后作为状态的一部分存储以供构建方法使用。

虽然方法相对简单,但这种自动比较非常有效,可以实现高性能的交互式应用程序。而且,build 函数的设计通过专注于声明小部件的构成,而不是更新用户界面从一种状态到另一种状态的复杂性,从而简化了代码。

Widget 状态

#

框架引入了两种主要的小部件类别:有状态和小部件和无状态小部件。

许多小部件没有可变状态:它们没有任何随时间变化的属性(例如,图标或标签)。这些小部件是 StatelessWidget 的子类。

但是,如果小部件的唯一特征需要根据用户交互或其他因素发生变化,则该小部件就是有状态的。例如,如果一个小部件有一个计数器,每当用户点击按钮时都会递增,则计数器的值就是该小部件的状态。当该值发生变化时,需要重建小部件以更新其 UI 部分。这些小部件是 StatefulWidget 的子类,并且(因为小部件本身是不可变的),它们将可变状态存储在一个是 State 的子类的单独类中。StatefulWidget 没有 build 方法;相反,它们的用户界面是通过它们的 State 对象构建的。

每当您更改 State 对象(例如,通过递增计数器)时,都必须调用 setState() 以向框架发出信号,要求通过再次调用 State 的 build 方法来更新用户界面。

拥有单独的状态和小部件对象可以让其他小部件以完全相同的方式处理无状态和小部件,而不必担心丢失状态。父级无需保留子级以保持其状态,而可以在任何时候创建子级的新实例,而不会丢失子级的持久状态。框架在适当的时候完成了查找和重用现有状态对象的所有工作。

状态管理

#

因此,如果许多小部件可以包含状态,那么如何管理和传递状态呢?

与任何其他类一样,您可以在小部件中使用构造函数来初始化其数据,因此 build() 方法可以确保任何子小部件都使用其需要的数据实例化

dart
@override
Widget build(BuildContext context) {
   return ContentWidget(importantState);
}

其中 importantState 是包含对 Widget 重要的状态的类的占位符。

但是,随着 widget 树的加深,在树层次结构中上下传递状态信息变得很麻烦。因此,第三种 widget 类型 InheritedWidget 提供了一种简单的方法来获取来自共享祖先的数据。您可以使用 InheritedWidget 创建一个状态 widget,该 widget 包装 widget 树中的一个公共祖先,如本例所示

Inherited widgets

每当 ExamWidgetGradeWidget 对象之一需要来自 StudentState 的数据时,它现在可以使用以下命令访问它:

dart
final studentState = StudentState.of(context);

of(context) 调用获取构建上下文(对当前 widget 位置的句柄),并返回 树中与 StudentState 类型匹配的最近的祖先InheritedWidget 还提供了一个 updateShouldNotify() 方法,Flutter 调用该方法来确定状态更改是否应触发使用它的子 widget 的重建。

Flutter 本身广泛使用 InheritedWidget 作为框架的一部分来共享状态,例如应用程序的视觉主题,其中包括 颜色和类型样式等属性,这些属性在整个应用程序中都很普遍。MaterialApp build() 方法在构建时在树中插入一个主题,然后在层次结构中更深的位置,widget 可以使用 .of() 方法查找相关的主题数据。

例如

dart
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 等解释性语言编写的,而 JavaScript 又必须与基于 Java 的 Android 或基于 Objective-C 的 iOS 系统库交互以显示 UI。所有这些都会增加开销,而开销可能很大,尤其是在 UI 与应用程序逻辑之间存在大量交互的情况下。

相比之下,Flutter 最小化了这些抽象,绕过了系统 UI widget 库,转而使用自己的 widget 集。绘制 Flutter 视觉效果的 Dart 代码编译成原生代码,该代码使用 Skia(或将来使用 Impeller)进行渲染。Flutter 还将自己的 Skia 副本嵌入到引擎中,允许开发者升级他们的应用程序以保持与最新性能改进的同步,即使手机尚未更新为新的 Android 版本也是如此。对于其他原生平台上的 Flutter(例如 Windows 或 macOS)也是如此。

从用户输入到 GPU

#

Flutter 应用于其渲染管线的首要原则是:**简单即快速**。Flutter 拥有一个关于数据如何流向系统的简单明了的管道,如下面的时序图所示。

Render pipeline sequencing
diagram

让我们更详细地了解其中的一些阶段。

构建:从 Widget 到 Element

#

考虑以下代码片段,它演示了一个 widget 层次结构。

dart
Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当 Flutter 需要渲染此片段时,它会调用 build() 方法,该方法返回一个 widget 子树,该子树根据当前应用程序状态渲染 UI。在此过程中,build() 方法可以根据其状态必要时引入新的 widget。例如,在前面的代码片段中,Container 具有 colorchild 属性。从查看 Container源代码,您可以看到,如果颜色不为 null,它会插入一个表示颜色的 ColoredBox

dart
if (color != null)
  current = ColoredBox(color: color!, child: current);

相应地,ImageText widget 可能会在构建过程中插入子 widget,例如 RawImageRichText。因此,最终的 widget 层次结构可能比代码表示的更深,例如本例[2]

Render pipeline sequencing
diagram

这就解释了为什么当您通过调试工具(例如 Flutter 检查器,它是 Flutter/Dart DevTools 的一部分)检查树时,您可能会看到一个结构比原始代码中的结构深得多。

在构建阶段,Flutter 将代码中表达的 widget 转换为相应的元素树,每个 widget 对应一个元素。每个元素代表 widget 在树层次结构中特定位置的特定实例。元素有两种基本类型。

  • ComponentElement,其他元素的宿主。
  • RenderObjectElement,参与布局或绘制阶段的元素。

Render pipeline sequencing
diagram

RenderObjectElement 是其 widget 类似物和底层 RenderObject(我们稍后会讲到)之间的中间体。

任何 widget 的元素都可以通过其 BuildContext 来引用,BuildContext 是 widget 在树中位置的句柄。这是诸如 Theme.of(context) 之类的函数调用中的 context,并作为参数提供给 build() 方法。

由于 widget 是不可变的,包括节点之间父子关系,因此对 widget 树的任何更改(例如将前面的示例中的 Text('A') 更改为 Text('B'))都会导致返回一组新的 widget 对象。但这并不意味着必须重建底层表示。元素树在帧与帧之间是持久存在的,因此发挥着至关重要的性能作用,允许 Flutter 即使 widget 层次结构完全可丢弃,也能像缓存其底层表示一样工作。通过仅遍历发生更改的 widget,Flutter 可以仅重建需要重新配置的元素树部分。

布局和渲染

#

很少有应用程序只绘制单个 widget。因此,任何 UI 框架的重要部分都是能够有效地布局 widget 层次结构,在 widget 在屏幕上渲染之前确定每个元素的大小和位置。

渲染树中每个节点的基类是 RenderObject,它定义了布局和绘制的抽象模型。这非常通用:它不承诺使用固定数量的维度,甚至不承诺使用笛卡尔坐标系(由 此极坐标系示例 演示)。每个 RenderObject 都知道其父级,但对其子级知之甚少,只知道如何访问它们及其约束。这为 RenderObject 提供了足够的抽象,使其能够处理各种用例。

在构建阶段,Flutter 为元素树中的每个 RenderObjectElement 创建或更新一个继承自 RenderObject 的对象。RenderObject 是基本元素:RenderParagraph 渲染文本,RenderImage 渲染图像,RenderTransform 在绘制其子级之前应用转换。

Differences between the widgets hierarchy and the element and render
trees

大多数 Flutter widget 由继承自 RenderBox 子类的对象渲染,RenderBox 表示 2D 笛卡尔空间中固定大小的 RenderObjectRenderBox 提供了盒约束模型的基础,为要渲染的每个 widget 建立最小和最大宽度和高度。

为了执行布局,Flutter 以深度优先遍历方式遍历渲染树并向下传递大小约束,从父级到子级。在确定其大小时,子级必须遵守其父级赋予它的约束。子级通过在父级建立的约束范围内向上传递大小来响应其父级对象。

Constraints go down, sizes go
up

在此次对树的单次遍历结束时,每个对象在其父级的约束范围内都有一个定义的大小,并准备通过调用 paint() 方法进行绘制。

盒约束模型作为一种以O(n)时间布局对象的方式非常强大。

  • 父级可以通过将最大和最小约束设置为相同的值来指示子对象的大小。例如,手机应用程序中最顶层的渲染对象将其子级约束为屏幕大小。(子级可以选择如何使用该空间。例如,它们可能只将其想要渲染的内容居中放置在指定的约束内。)
  • 父级可以指示子级的宽度,但给子级提供高度上的灵活性(或指示高度,但提供宽度上的灵活性)。一个真实的例子是流动文本,它可能必须适应水平约束,但根据文本数量在垂直方向上变化。

即使子对象需要知道它有多少可用空间来决定如何渲染其内容,此模型也能正常工作。通过使用 LayoutBuilder widget,子对象可以检查传递下来的约束并使用这些约束来确定如何使用它们,例如。

dart
Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

有关约束和布局系统的更多信息以及工作示例,请参阅 理解约束 主题。

所有 RenderObject 的根是 RenderView,它表示渲染树的总输出。当平台需要渲染新帧时(例如,由于 vsync 或纹理解压缩/上传完成),就会调用 compositeFrame() 方法,该方法是渲染树根部的 RenderView 对象的一部分。这将创建一个 SceneBuilder 以触发场景的更新。场景完成后,RenderView 对象将合成场景传递给 dart:ui 中的 Window.render() 方法,该方法将控制权传递给 GPU 以对其进行渲染。

管道合成和光栅化阶段的更多详细信息超出了本文的高级范围,但可以在 此关于 Flutter 渲染管道的演讲 中找到更多信息。

平台嵌入

#

正如我们所看到的,Flutter 用户界面不是转换为等效的操作系统 widget,而是由 Flutter 本身构建、布局、合成和绘制的。获取纹理并参与底层操作系统的应用程序生命周期的机制不可避免地会因该平台的独特问题而异。引擎与平台无关,提供了一个 稳定的 ABI(应用程序二进制接口),使平台嵌入器能够设置和使用 Flutter。

平台嵌入器是托管所有 Flutter 内容的原生操作系统应用程序,充当主机操作系统和 Flutter 之间的粘合剂。启动 Flutter 应用程序时,嵌入器提供入口点,初始化 Flutter 引擎,获取 UI 和光栅化的线程,并创建 Flutter 可以写入的纹理。嵌入器还负责应用程序生命周期,包括输入手势(如鼠标、键盘、触摸)、窗口大小调整、线程管理和平台消息。Flutter 包括用于 Android、iOS、Windows、macOS 和 Linux 的平台嵌入器;您还可以创建自定义平台嵌入器,如 此支持通过 VNC 样式帧缓冲区远程控制 Flutter 会话的工作示例此 Raspberry Pi 工作示例

每个平台都有自己的一组 API 和约束。一些简短的平台特定说明。

  • 在 iOS 和 macOS 上,Flutter 分别加载到嵌入器中作为 UIViewControllerNSViewController。平台嵌入器创建一个 FlutterEngine,它充当 Dart VM 和您的 Flutter 运行时的主机,以及一个 FlutterViewController,它连接到 FlutterEngine 以将 UIKit 或 Cocoa 输入事件传递到 Flutter 并使用 Metal 或 OpenGL 显示 FlutterEngine 渲染的帧。
  • 在 Android 上,Flutter 默认情况下加载到嵌入器中作为 Activity。视图由 FlutterView 控制,该视图根据 Flutter 内容的合成和 z 顺序要求将 Flutter 内容呈现为视图或纹理。
  • 在 Windows 上,Flutter 托管在传统的 Win32 应用程序中,内容使用 ANGLE 渲染,ANGLE 是一个将 OpenGL API 调用转换为 DirectX 11 等效项的库。

与其他代码集成

#

Flutter 提供了各种互操作性机制,无论您是访问用 Kotlin 或 Swift 等语言编写的代码或 API,调用基于 C 的原生 API,在 Flutter 应用程序中嵌入原生控件,还是在现有应用程序中嵌入 Flutter。

平台通道

#

对于移动和桌面应用程序,Flutter 允许您通过平台通道调用自定义代码,平台通道是一种在您的 Dart 代码和主机应用程序的特定于平台的代码之间进行通信的机制。通过创建公共通道(封装名称和编解码器),您可以在 Dart 和用 Kotlin 或 Swift 等语言编写的平台组件之间发送和接收消息。数据从 Dart 类型(如 Map)序列化为标准格式,然后反序列化为 Kotlin(如 HashMap)或 Swift(如 Dictionary)中的等效表示形式。

How platform channels allow Flutter to communicate with host
code

以下是 Dart 调用 Kotlin(Android)或 Swift(iOS)中接收事件处理程序的简短平台通道示例。

dart
// Dart side
const channel = MethodChannel('foo');
final greeting = await channel.invokeMethod('bar', 'world') as String;
print(greeting);
kotlin
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    else -> result.notImplemented()
  }
}
swift
// 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 到广告到相机和蓝牙等设备硬件。

外部函数接口

#

对于基于 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

dart
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 树完全是内部的,因此在 Flutter 的内部模型或渲染中与 Flutter Widget 交织在一起的地方不存在类似 Android View 的东西。 对于希望在其 Flutter 应用程序中包含现有平台组件(例如浏览器控件)的开发人员来说,这是一个问题。

Flutter 通过引入平台视图 Widget(AndroidViewUiKitView)来解决此问题,这些 Widget 允许您在每个平台上嵌入此类内容。 平台视图可以与其他 Flutter 内容集成[3]。 这些 Widget 中的每一个都充当底层操作系统的中间体。 例如,在 Android 上,AndroidView 执行三个主要功能

  • 每次绘制帧时,复制原生视图渲染的图形纹理,并将其呈现给 Flutter,作为 Flutter 渲染的表面的一部分进行合成。
  • 响应点击测试和输入手势,并将它们转换为等效的原生输入。
  • 创建辅助功能树的模拟,并在原生层和 Flutter 层之间传递命令和响应。

不可避免地,这种同步会带来一定的开销。 因此,通常情况下,这种方法最适合复杂控件(如 Google Maps),因为在 Flutter 中重新实现这些控件并不实用。

通常,Flutter 应用程序会基于平台测试在其build()方法中实例化这些 Widget。 例如,来自 google_maps_flutter 插件

dart
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');

AndroidViewUiKitView底层的原生代码通信通常使用前面描述的平台通道机制。

目前,平台视图不可用于桌面平台,但这并非架构上的限制;将来可能会添加支持。

在父应用中托管 Flutter 内容

#

前面场景的反面是在现有的 Android 或 iOS 应用程序中嵌入 Flutter Widget。 如前面部分所述,在移动设备上运行的新创建的 Flutter 应用程序托管在 Android 活动或 iOS UIViewController 中。 可以使用相同的嵌入 API 将 Flutter 内容嵌入到现有的 Android 或 iOS 应用程序中。

Flutter 模块模板旨在易于嵌入;您可以将其作为源依赖项嵌入到现有的 Gradle 或 Xcode 构建定义中,或者可以将其编译成 Android 归档文件或 iOS 框架二进制文件以供使用,而无需每个开发人员都安装 Flutter。

Flutter 引擎需要一段时间才能初始化,因为它需要加载 Flutter 共享库、初始化 Dart 运行时、创建并运行 Dart 隔离区,并将渲染表面附加到 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 在标准浏览器 API 的基础上重新实现了引擎。 目前,我们有两个选项可以在 Web 上渲染 Flutter 内容:HTML 和 WebGL。 在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG。 要渲染到 WebGL,Flutter 使用编译到 WebAssembly 的 Skia 版本,称为 CanvasKit。 虽然 HTML 模式提供了最佳的代码大小特性,但CanvasKit提供了到浏览器图形堆栈的最快路径,并且在图形保真度方面与原生移动目标略有提高[4]

架构层图的 Web 版本如下所示

Flutter web
architecture

与 Flutter 运行的其他平台相比,最显着的区别可能是 Flutter 无需提供 Dart 运行时。 相反,Flutter 框架(以及您编写的任何代码)都被编译成 JavaScript。 还值得注意的是,Dart 在所有模式(JIT 与 AOT、原生与 Web 编译)下几乎没有语言语义差异,并且大多数开发人员永远不会编写运行到这种差异的代码行。

在开发期间,Flutter Web 使用 dartdevc,这是一种支持增量编译的编译器,因此允许对应用程序进行热重启(尽管目前不支持热重载)。 相反,当您准备好为 Web 创建生产应用程序时,将使用 Dart 的高度优化的生产 JavaScript 编译器 dart2js,将 Flutter 核心和框架以及您的应用程序打包到一个可以部署到任何 Web 服务器的最小化源文件中。 代码可以存储在一个文件中,也可以通过 延迟导入 分割成多个文件。

更多信息

#

对于那些有兴趣了解 Flutter 内部工作原理的人,Flutter 内部 白皮书提供了框架设计理念的有用指南。


  1. 虽然build函数返回一个新的树,但只有在需要合并一些新的配置时,您才需要返回不同的内容。 如果配置实际上相同,则可以只返回相同的 Widget。 ↩︎

  2. 为了便于阅读,这里进行了轻微的简化。 在实践中,树可能更复杂。 ↩︎

  3. 这种方法有一些限制,例如,平台视图的透明度与其他 Flutter Widget 的透明度合成方式不同。 ↩︎

  4. 例如,阴影必须使用 DOM 等效的基元来近似,这会以牺牲一些保真度为代价。 ↩︎