Flutter 架构概述

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

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

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

本概述分为以下几个部分

  1. 层模型:构成 Flutter 的部分。
  2. 响应式用户界面:Flutter 用户界面开发的核心概念。
  3. 小部件简介:Flutter 用户界面的基本构建模块。
  4. 渲染过程:Flutter 如何将 UI 代码转换为像素。
  5. 平台嵌入器概述:允许移动和桌面操作系统执行 Flutter 应用的代码。
  6. 将 Flutter 与其他代码集成:有关 Flutter 应用可用的不同技术的信息。
  7. 对网页的支持:关于浏览器环境中 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 的底层实现,包括图形(通过 iOS 上的Impeller和即将登陆 Android 的 Impeller,以及其他平台上的Skia)、文本布局、文件和网络 I/O、辅助功能支持、插件架构以及 Dart 运行时和编译工具链。

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

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

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

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

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

应用程序的解剖

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

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

Dart 应用程序

  • 将小部件组合成所需的 UI。
  • 实现业务逻辑。
  • 由应用程序开发人员所有。

框架 (源代码)

  • 提供用于构建高质量应用程序的高级 API(例如,小部件、命中测试、手势检测、辅助功能、文本输入)。
  • 将应用程序的小部件树合成到场景中。

引擎 (源代码)

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

Embedder (源代码)

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

运行器

  • 将 Embedder 的特定于平台的 API 公开的组件组合成可运行于目标平台的应用包。
  • 由应用开发者拥有的 flutter create 生成的应用模板的一部分。

响应式用户界面

从表面上看,Flutter 是 一个响应式、声明式 UI 框架,其中开发者提供从应用状态到界面状态的映射,而框架在应用状态发生变化时负责更新运行时的界面。此模型的灵感来自 Facebook 为其自己的 React 框架开展的工作,其中包括对许多传统设计原则的重新思考。

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

Color picker dialog

有许多地方可以更改状态:颜色框、色相滑块、单选按钮。当用户与 UI 交互时,必须在其他每个地方反映更改。更糟糕的是,除非小心,用户界面的一部分的微小更改可能会对看似不相关的代码部分造成连锁反应。

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

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

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

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

UI = f(state)

build() 方法在设计上执行速度快,并且应该没有副作用,允许框架在需要时随时调用它(可能每渲染一帧调用一次)。

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

控件

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

控件基于组合形成层次结构。每个控件都嵌套在其父级中,并且可以从父级接收上下文。此结构一直延伸到根控件(托管 Flutter 应用程序的容器,通常为 MaterialAppCupertinoApp),如下面的简单示例所示

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'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

在前面的代码中,所有实例化的类都是控件。

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

Flutter 具有自己的每个 UI 控件实现,而不是推迟到系统提供的控件:例如,有 Dart 实现,既有 iOS 切换控件,也有 Android 等效控件

这种方法提供了多种好处

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

组合

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

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

类层次结构故意浅而宽,以最大化可能的组合数量,重点是小型、可组合的小部件,每个小部件都能很好地完成一项任务。核心功能是抽象的,甚至像内边距和对齐这样的基本功能也作为单独的组件实现,而不是内置到核心当中。(这也与更传统 API 形成对比,在更传统 API 中,内边距等功能内置于每个布局组件的公共核心当中。)因此,例如,要使小部件居中,而不是调整一个名义上的 Align 属性,您可以将其包装在 Center 小部件中。

有用于内边距、对齐、行、列和网格的小部件。这些布局小部件没有自己的可视表示。相反,它们的唯一目的是控制另一个小部件布局的某个方面。Flutter 还包括利用这种组合方法的实用小部件。

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

构建组件

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

小组件的构建函数应无副作用。每当要求函数构建时,小组件都应返回一个新的树形小组件1,无论小组件之前返回什么。框架会做大量的工作来确定根据渲染对象树(稍后会更详细地描述)需要调用哪些构建方法。有关此过程的更多信息,请参阅Inside Flutter 主题

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

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

小组件状态

框架引入了两大类小组件:有状态和小组件和无状态小组件。

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

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

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

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

状态管理

那么,如果许多窗口小部件都可以包含状态,状态是如何管理并在系统中传递的?

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

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

然而,随着窗口小部件树的加深,在树层次结构中传递状态信息变得很麻烦。因此,第三种窗口小部件类型 InheritedWidget 提供了一种从共享祖先获取数据的方法。你可以使用 InheritedWidget 创建一个状态窗口小部件,它将窗口小部件树中的一个公共祖先包装起来,如下例所示

Inherited widgets

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

final studentState = StudentState.of(context);

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

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

Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.titleLarge,
  ),
);

随着应用程序的增长,更高级的状态管理方法变得更具吸引力,这些方法减少了创建和使用有状态窗口小部件的繁琐工作。许多 Flutter 应用程序使用 provider 等实用程序包,它提供了 InheritedWidget 的包装器。Flutter 的分层架构还支持实现将状态转换为 UI 的替代方法,例如 flutter_hooks 包。

渲染和布局

本部分介绍渲染管道,这是 Flutter 将小部件层次结构转换为实际绘制到屏幕上的像素的一系列步骤。

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 小部件库,转而使用自己的小部件集。用于绘制 Flutter 视觉效果的 Dart 代码被编译成原生代码,该代码使用 Skia(或将来使用 Impeller)进行渲染。Flutter 还将 Skia 的副本嵌入到引擎中,允许开发者升级他们的应用以跟上最新的性能改进,即使手机尚未更新到新的 Android 版本。对于其他原生平台(如 Windows 或 macOS)上的 Flutter 也是如此。

从用户输入到 GPU

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

Render pipeline sequencing
diagram

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

构建:从 Widget 到 Element

考虑演示小组件层次结构的这段代码片段

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

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

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

相应地,ImageText 小组件可能会在构建过程中插入子小组件,例如 RawImageRichText。因此,最终的小组件层次结构可能比代码表示的更深,就像这种情况2

Render pipeline sequencing
diagram

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

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

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

Render pipeline sequencing
diagram

RenderObjectElement 是其小组件类似物和底层 RenderObject 之间的中介,我们稍后会介绍。

任何小组件的元素都可以通过其 BuildContext 进行引用,它是树中某个小组件位置的句柄。这是函数调用中的 context,例如 Theme.of(context),并作为参数提供给 build() 方法。

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

布局和渲染

仅绘制单个小组件的应用程序非常罕见。因此,任何 UI 框架的重要组成部分都是有效地布局小组件层次结构,在屏幕上渲染每个元素之前确定其大小和位置。

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

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

Differences between the widgets hierarchy and the element and render
trees

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

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

Constraints go down, sizes go
up

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

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

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

即使子级对象需要知道它有多少可用空间来决定如何渲染其内容,此模型仍然有效。通过使用 LayoutBuilder 小组件,子级对象可以检查传递下来的约束,并使用这些约束来确定它将如何使用它们,例如

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对象将合成场景传递给Window.render()方法(在dart:ui中),该方法将控制权传递给 GPU 以进行渲染。

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

平台嵌入

正如我们所见,Flutter 用户界面并非转换为等效的操作系统小部件,而是由 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 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 到广告再到相机和蓝牙等设备硬件的许多常见场景。

外部函数接口

对于基于 C 的 API(包括可为用 Rust 或 Go 等现代语言编写的代码生成的 API),Dart 提供了一种直接机制,可使用 dart:ffi 库绑定到本机代码。外部函数接口 (FFI) 模型可以比平台通道快得多,因为不需要序列化来传递数据。相反,Dart 运行时提供了在堆上分配由 Dart 对象支持的内存并调用静态或动态链接库的能力。FFI 可用于除 Web 以外的所有平台,其中 js 包 具有同等用途。

要使用 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 内容绘制到纹理并且其小部件树完全是内部的,因此在 Flutter 的内部模型中不存在 Android 视图之类的内容,或者在 Flutter 小部件中交错呈现。对于希望在其 Flutter 应用中包含现有平台组件(例如浏览器控件)的开发人员来说,这是一个问题。

Flutter 通过引入平台视图小部件(AndroidViewUiKitView)解决了此问题,这些小部件允许您在每个平台上嵌入此类内容。平台视图可以与其他 Flutter 内容集成3。每个小部件都充当与底层操作系统的中间媒介。例如,在 Android 上,AndroidView 具有三个主要功能

  • 制作原生视图呈现的图形纹理的副本,并在每次绘制帧时将其呈现给 Flutter 以作为 Flutter 呈现曲面的组成部分。
  • 响应点击测试和输入手势,并将这些内容转换为等效的原生输入。
  • 创建辅助功能树的模拟,并在原生层和 Flutter 层之间传递命令和响应。

不可避免地,此同步会产生一定量的开销。因此,通常情况下,此方法最适合像 Google 地图那样的复杂控件,因为在 Flutter 中重新实现它们不切实际。

通常,Flutter 应用会根据平台测试在 build() 方法中实例化这些小部件。例如,来自 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');

AndroidViewUiKitView 下层的原生代码进行通信通常使用平台通道机制,如前所述。

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

在父应用中托管 Flutter 内容

上述场景的相反情况是在现有 Android 或 iOS 应用中嵌入 Flutter 小部件。如前一节所述,在移动设备上运行的新创建的 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 网页支持

虽然一般的架构概念适用于 Flutter 支持的所有平台,但 Flutter 网页支持有一些值得评论的独特特性。

自 Dart 语言存在以来,它一直在编译为 JavaScript,并针对开发和生产目的进行了优化。许多重要的应用从 Dart 编译为 JavaScript,并在今天投入生产,包括Google Ads 的广告客户工具。由于 Flutter 框架是用 Dart 编写的,因此将其编译为 JavaScript 相对简单。

但是,用 C++ 编写的 Flutter 引擎旨在与底层操作系统而不是网络浏览器交互。因此需要不同的方法。在网络上,Flutter 提供了基于标准浏览器 API 的引擎重新实现。我们目前有两种在网络上渲染 Flutter 内容的选项:HTML 和 WebGL。在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG。为了渲染到 WebGL,Flutter 使用编译为 WebAssembly 的 Skia 版本,称为CanvasKit。虽然 HTML 模式提供了最佳的代码大小特性,但CanvasKit提供了到浏览器图形堆栈的最快速路径,并提供了与原生移动目标4稍高的图形保真度。

架构层图的网络版本如下

Flutter web
architecture

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

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

更多信息

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


脚注

1虽然 build 函数返回一个新树,但只有在需要合并一些新配置时才需要返回一个不同的东西。如果配置实际上是相同的,则可以只返回相同的部件。

2为了便于阅读,这是一个轻微的简化。实际上,树可能更复杂。

3这种方法有一些限制,例如,平台视图的透明度与其他 Flutter 部件的透明度复合方式不同。

4一个例子是阴影,它必须用 DOM 等效基元来近似,以牺牲一些保真度为代价。