本文旨在提供 Flutter 架构的高层概览,包括构成其设计的核心原则和概念。如果您对如何构建 Flutter 应用的架构感兴趣,请查阅 构建 Flutter 应用

Flutter 是一个跨平台 UI 工具包,旨在实现跨 iOS、Android、Web 和桌面等操作系统的代码重用,同时允许应用程序直接与底层平台服务进行交互。其目标是使开发者能够交付高性能的应用,这些应用在不同平台上都能表现得自然,拥抱它们之间的差异,同时尽可能多地共享代码。

在开发过程中,Flutter 应用运行在一个 VM 中,该 VM 提供有状态的热重载功能,无需完全重新编译即可更改。 (在 Web 上,Flutter 支持热重启和 带标志的热重载。)对于发布,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 是 Swift 和 Objective-C/Objective-C++,Windows 和 Linux 是 C++。使用嵌入器,Flutter 代码可以作为模块集成到现有应用中,或者代码可以构成应用的全部内容。Flutter 包含许多针对常见目标平台的嵌入器,但也存在其他嵌入器。

Flutter 的核心是Flutter 引擎,它主要用 C++ 编写,并支持所有 Flutter 应用所需的原始基元。引擎负责在需要绘制新帧时栅格化合成场景。它提供了 Flutter 核心 API 的底层实现,包括图形(在 iOS、Android 和桌面(需启用标志)上通过 Impeller,在其他平台上通过 Skia)、文本布局、文件和网络 I/O、可访问性支持、插件架构以及 Dart 运行时和编译工具链。

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

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

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

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

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

应用 Anatomy

#

下图概述了由 flutter create 生成的常规 Flutter 应用的组成部分。它展示了 Flutter 引擎在此堆栈中的位置,突出了 API 边界,并确定了各个部分所在的存储库。下面的图例解释了描述 Flutter 应用组成部分的常用术语。

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

Dart App

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

框架 (源代码)

  • 提供更高级别的 API 来构建高质量的应用(例如,Widget、命中测试、手势检测、可访问性、文本输入)。
  • 将应用的 Widget 树组合成一个场景。

引擎 (源代码)

  • 负责栅格化合成场景。
  • 提供 Flutter 核心 API 的底层实现(例如,图形、文本布局、Dart 运行时)。
  • 使用dart:ui API向框架公开其功能。
  • 使用引擎的Embedder 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 特别适合这项任务

组件

#

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

Widget 通过组合形成一个层级结构。每个 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。

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

Flutter 拥有自己的 UI 控件实现,而不是依赖系统提供的控件:例如,它为 iOS 的 Toggle 控件 Android 对应控件都提供了纯 Dart 实现

这种方法提供了几个好处

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

组合

#

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

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

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

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

例如,Container,一个常用的 Widget,由几个负责布局、绘制、定位和大小调整的 Widget 组成。具体来说,ContainerLimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform Widget 组成,您可以通过阅读其源代码来查看。Flutter 的一个决定性特征是,您可以深入到任何 Widget 的源代码并进行检查。因此,与其通过子类化 Container 来产生定制效果,不如以新颖的方式组合它和其他 Widget,或者只是以 Container 为灵感创建一个新 Widget。

构建 Widget

#

如前所述,您通过重写 build() 函数来声明 Widget 的视觉表示,以返回一个新的元素树。这棵树以更具体的方式表示 Widget 在用户界面中的部分。例如,工具栏 Widget 可能有一个 build 函数,该函数返回一些 文本各种 按钮水平布局。根据需要,框架会递归地要求每个 Widget 进行构建,直到树完全由 具体的渲染对象 描述。然后,框架将渲染对象缝合成一个渲染对象树。

Widget 的 build 函数应不含副作用。每当函数被要求构建时,Widget 都应该返回一个新的 Widget 树[1],无论 Widget 之前返回什么。框架通过(稍后更详细地描述)基于渲染对象树确定需要调用哪些构建方法来完成繁重的工作。有关此过程的更多信息可以在 Inside 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 方法;相反,它们的 UI 是通过其 State 对象构建的。

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

拥有独立的状态对象和 Widget 对象,可以让其他 Widget 以完全相同的方式处理无状态和有状态 Widget,而无需担心丢失状态。父 Widget 不需要持有子 Widget 来保留其状态,而是可以随时创建子 Widget 的新实例,而不会丢失子 Widget 的持久状态。框架会尽一切努力在适当的时候查找和重用现有的状态对象。

状态管理

#

那么,如果许多 Widget 可以包含状态,那么状态是如何在系统中管理和传递的呢?

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

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

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

然而,随着 Widget 树的加深,在树层级上下传递状态信息变得繁琐。因此,第三种 Widget 类型,InheritedWidget,提供了一种方便的方法来从共享的祖先获取数据。您可以使用 InheritedWidget 创建一个包装 Widget 树中常见祖先的状态 Widget,如下面的示例所示

Inherited widgets

每当 ExamWidgetGradeWidget 对象需要来自 StudentState 的数据时,它现在都可以通过类似如下命令访问它

dart
final studentState = StudentState.of(context);

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

Flutter 本身广泛使用 InheritedWidget 作为共享状态的框架一部分,例如应用程序的视觉主题,其中包括颜色和类型样式等属性,这些属性在整个应用程序中无处不在。MaterialAppbuild() 方法在构建时在树中插入一个主题,然后在此层级的更深处,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 可以使用用 C/C++ 编写的图形引擎 Skia 进行渲染,Skia 会调用 CPU 或 GPU 在设备上完成绘制。

跨平台框架通常通过在底层原生 Android 和 iOS UI 库之上创建抽象层来工作,试图消除每个平台表示的不一致性。应用代码通常用 JavaScript 等解释型语言编写,而 JavaScript 反过来必须与基于 Java 的 Android 或基于 Objective-C 的 iOS 系统库交互才能显示 UI。所有这些都会增加开销,尤其是在 UI 和应用逻辑之间存在大量交互时,开销可能很大。

相比之下,Flutter 最小化了这些抽象,绕过了系统 UI Widget 库,转而使用自己的 Widget 集。用于绘制 Flutter 图形的 Dart 代码被编译为原生代码,该代码使用 Impeller 进行渲染。Impeller 与应用程序一起打包,允许开发者更新其应用以跟上最新的性能改进,即使手机没有更新到新的 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源代码来看,您可以看到如果颜色不为空,它会插入一个表示颜色的 ColoredBox

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

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

Render pipeline sequencing diagram

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

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

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

Render pipeline sequencing diagram

RenderObjectElement 是它们与 Widget 模拟体以及底层 RenderObject(我们稍后将讨论)之间的中间件。

任何 Widget 的 Element 都可以通过其 BuildContext 引用,BuildContext 是 Widget 在树中位置的句柄。这就是 Theme.of(context) 等函数调用中的 context,它作为参数提供给 build() 方法。

由于 Widget 是不可变的,包括节点之间的父/子关系,因此对 Widget 树的任何更改(例如,在前一个示例中将 Text('A') 更改为 Text('B'))都会导致返回一组新的 Widget 对象。但这并不意味着底层表示必须被重建。元素树从帧到帧都是持久的,因此起着关键的性能作用,使 Flutter 能够像 Widget 层级完全可处置一样运作,同时缓存其底层表示。通过只遍历已更改的 Widget,Flutter 可以只重建需要重新配置的元素树的部分。

布局与渲染

#

很少有应用程序只绘制一个 Widget。因此,任何 UI 框架的一个重要部分是能够有效地布局 Widget 层级,在将每个元素渲染到屏幕之前确定其大小和位置。

所有 RenderObject 的基类是 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 代表二维笛卡尔空间中固定大小的 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 或因为纹理解压缩/上传完成),会调用 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 会话的此工作示例此 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 进行渲染,该库将 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、广告到相机和蓝牙等设备硬件的许多常见场景。

外部函数接口 (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 的代码片段

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 树完全是内部的,因此像 Android 视图这样的东西无法存在于 Flutter 的内部模型中或在 Flutter Widget 中进行交错渲染。对于希望在其 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 内容

#

与前一种情况相反的是,将 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 隔离区,并将渲染表面附加到 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 提供两个渲染器

渲染器编译目标
CanvasKitJavaScript
SkwasmWebAssembly

构建模式是命令行选项,用于指定运行应用时可用的渲染器。

Flutter 提供两种构建模式

构建模式可用渲染器
默认CanvasKit
`--wasm`Skwasm(首选),CanvasKit(备用)

默认模式仅提供 CanvasKit 渲染器。--wasm 选项同时提供两种渲染器,并根据浏览器功能选择引擎:如果浏览器能够运行 Skwasm,则首选 Skwasm,否则回退到 CanvasKit。

Flutter web architecture

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

在开发时,Flutter Web 使用 dartdevc,一个支持增量编译的编译器,因此允许热重启和带标志的热重载。相反,当您准备好为 Web 创建生产应用时,将使用 Dart 的高度优化的生产 JavaScript 编译器 dart2js,将 Flutter 核心和框架以及您的应用程序打包到一个可以部署到任何 Web 服务器的最小化源文件中。代码可以以单个文件提供,也可以通过延迟导入拆分成多个文件。

有关 Flutter Web 的更多信息,请查看 Flutter Web 支持Web 渲染器

更多信息

#

对于那些对 Flutter 内部机制感兴趣的人,Inside Flutter 白皮书为框架的设计理念提供了有用的指南。


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

  2. 为了便于阅读,这只是一个简化的说法。实际上,树可能会更复杂。↩︎

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