本文旨在提供 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 框架,该库将底层 C++ 代码封装在 Dart 类中。此库暴露了最低级别的基元,例如用于驱动输入、图形和文本渲染子系统的类。

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

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

Flutter 框架相对较小;许多开发者可能使用的更高级功能都以包的形式实现,包括像 camerawebview 这样的平台插件,以及像 charactershttpanimations 这样构建于核心 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 向框架暴露其功能。
  • 使用引擎的 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(托管 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 控件实现,而不是依赖系统提供的控件:例如,Dart 纯实现iOS 切换控件Android 对应控件

这种方法提供了几个好处:

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

组合

#

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 之前返回了什么。框架会根据渲染对象树(稍后会更详细地描述)完成确定哪些 build 方法需要调用的繁重工作。有关此过程的更多信息,请参阅《深入 Flutter》主题

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

虽然方法相对朴素,但这种自动比较非常有效,能够实现高性能的交互式应用。并且,build 函数的设计通过专注于声明 Widget 由什么构成,而不是更新用户界面从一个状态到另一个状态的复杂性,从而简化了您的代码。

Widget 状态

#

该框架引入了两种主要的 Widget 类:有状态无状态 Widget。

许多 Widget 没有可变状态:它们没有任何随时间变化的属性(例如,图标或标签)。这些 Widget 继承自 StatelessWidget

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

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

拥有单独的状态和 Widget 对象使得其他 Widget 可以以完全相同的方式对待无状态和有状态 Widget,而不必担心丢失状态。父级不需要持有子级以保留其状态,而是可以随时创建子级的新实例而不会丢失子级的持久状态。框架会完成所有适当查找和重用现有状态对象的工作。

状态管理

#

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

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

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 来实现共享状态,例如应用程序的视觉主题,其中包括在整个应用程序中普遍存在的颜色和字体样式等属性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 可以使用 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 版本。对于 Windows 或 macOS 等其他原生平台上的 Flutter 也是如此。

从用户输入到 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 inspector(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 层次结构,在屏幕上渲染每个元素之前确定其大小和位置。

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

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

Differences between the widgets hierarchy and the element and render trees

大多数 Flutter Widget 都由继承自 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,它代表渲染树的总输出。当平台要求渲染新帧时(例如,由于 垂直同步 或纹理解压/上传完成),会调用 compositeFrame() 方法,该方法是渲染树根部的 RenderView 对象的一部分。这会创建一个 SceneBuilder 来触发场景更新。当场景完成时,RenderView 对象将合成的场景传递给 dart:ui 中的 Window.render() 方法,该方法将控制权传递给 GPU 来渲染它。

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

平台嵌入

#

正如我们所见,Flutter 用户界面不是被翻译成等效的 OS 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 等语言编写的平台组件之间发送和接收消息。数据从 Map 等 Dart 类型序列化为标准格式,然后反序列化为 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 树完全是内部的,所以在 Flutter 的内部模型中没有 Android 视图的存在空间,也无法与 Flutter Widget 交错渲染。这对于希望在 Flutter 应用中包含现有平台组件(例如浏览器控件)的开发者来说是一个问题。

Flutter 通过引入平台视图 Widget(AndroidViewUiKitView)来解决这个问题,这些 Widget 允许您在每个平台上嵌入此类内容。平台视图可以与其他 Flutter 内容集成[3]。这些 Widget 中的每一个都充当底层操作系统与 Flutter 之间的中介。例如,在 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 中。Flutter 内容可以使用相同的嵌入 API 嵌入到现有 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 提供两种渲染器:

渲染器编译目标
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 创建生产应用时,将使用 dart2js,Dart 的高度优化生产 JavaScript 编译器,它将 Flutter 核心和框架以及您的应用程序打包到一个可部署到任何 Web 服务器的最小化源文件中。代码可以以单个文件的形式提供,也可以通过延迟导入拆分为多个文件。

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

更多信息

#

对于对 Flutter 内部机制感兴趣的人,《深入 Flutter》白皮书为框架的设计理念提供了有用的指导。


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

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

  3. 这种方法有一些限制,例如,平台视图的透明度不会像其他 Flutter Widget 那样进行合成。↩︎