Skip to main content

Flutter 架构概述

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

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

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

本概述分为几个部分:

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

架构层

#

Flutter被设计成一个可扩展的分层系统。它存在于一系列独立的库中,每个库都依赖于底层。没有一层对下层拥有特权访问,框架级别的每一部分都被设计成可选且可替换的。

架构图

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

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

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

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

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

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

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

应用剖析

#

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

由"flutter create"创建的Flutter应用程序的各层:Dart应用程序、框架、引擎、嵌入器、运行器

Dart应用程序

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

框架 (源代码)

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

引擎 (源代码)

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

嵌入器 (源代码)

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

运行器

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

响应式用户界面

#

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

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

颜色选择器对话框

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

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

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

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

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

UI = f(state)

build() 方法的设计目标是快速执行且没有副作用,允许框架在需要时调用它(可能每个渲染帧调用一次)。

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

小部件

#

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

小部件基于组合形成层次结构。每个小部件嵌套在其父级内部,并可以从父级接收上下文。此结构一直延伸到根小部件(承载 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('我的主页'),
        ),
        body: Center(
          child: Builder(
            builder: (context) {
              return Column(
                children: [
                  const Text('你好,世界'),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      print('点击!');
                    },
                    child: const Text('一个按钮'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

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

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

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

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

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

组合

#

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

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

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

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

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

构建小部件

#

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

小部件的构建函数不应有副作用。无论何时要求函数构建,小部件都应返回新的树状小部件[1],而不管小部件以前返回什么。框架完成了繁重的工作,以根据渲染对象树(稍后详细描述)确定需要调用哪些构建方法。有关此过程的更多信息,请参阅Flutter 内部主题

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

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

小部件状态

#

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

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

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

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

拥有单独的状态和 widget 对象可以让其他 widget 以完全相同的方式处理无状态和有状态 widget,而无需担心丢失状态。与其需要保留一个子 widget 来保留其状态,不如让父 widget 随时创建子 widget 的新实例,而不会丢失子 widget 的持久状态。框架会在适当的时候完成查找和重用现有状态对象的所有工作。

状态管理

#

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

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

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

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

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

Inherited widgets

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

dart
final studentState = StudentState.of(context);

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

Flutter 本身广泛使用 InheritedWidget 作为框架的一部分来共享状态,

例如应用程序的 视觉主题 ,其中包括诸如颜色和样式之类的属性,这些属性贯穿整个应用程序。MaterialAppbuild() 方法在其构建时在树中插入一个主题,然后在层次结构更深处,小部件可以使用 .of() 方法查找相关的主题数据。

例如:

dart
Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    '带有背景颜色的文本',
    style: Theme.of(context).textTheme.titleLarge,
  ),
);

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

渲染和布局

#

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

Flutter 的渲染模型

#

您可能想知道:如果 Flutter 是一个跨平台框架,那么它如何才能提供与单平台框架相当的性能呢?

首先考虑传统 Android 应用的工作方式很有用。绘制时,您首先调用 Android 框架的 Java 代码。Android 系统库提供负责将自身绘制到 Canvas 对象的组件,然后 Android 可以使用 Skia 渲染这些组件,Skia 是一个用 C/C++ 编写的图形引擎,它调用 CPU 或 GPU 来完成设备上的绘制。

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

相比之下,Flutter 通过绕过系统 UI 小部件库而支持其自身的小部件集,从而最大限度地减少了这些抽象。绘制 Flutter 可视化效果的 Dart 代码被编译成原生代码,该代码使用 Skia(或将来使用 Impeller)进行渲染。Flutter 还将 Skia 的自身副本嵌入到引擎中,允许开发人员升级他们的应用程序以保持与最新性能改进的同步,即使手机没有使用新的 Android 版本更新也是如此。对于其他原生平台(如 Windows 或 macOS)上的 Flutter 也是如此。

从用户输入到 GPU

#

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

渲染管道时序图

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

构建:从小部件到元素

#

考虑一下这段演示小部件层次结构的代码片段:

dart
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,您可以看到如果颜色不为空,它会插入一个表示颜色的 ColoredBox

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

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

渲染管道时序图

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

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

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

渲染管道时序图

RenderObjectElement 是其小部件模拟和底层 RenderObject(我们稍后会讨论)之间的中间体。

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

因为小部件是不可变的,包括节点之间的父子关系,所以对小部件树的任何更改(例如,在前面的示例中将 Text('A') 更改为 Text('B'))都会导致返回一组新的 widget 对象。但这并不意味着必须重建底层表示。元素树在帧与帧之间是持久的,因此它扮演着关键的性能角色,允许 Flutter 充当小部件层次结构完全可处置但缓存其底层表示的方式。通过仅遍历已更改的小部件,Flutter 只能重建需要重新配置的元素树的部分。

布局和渲染

#

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

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

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

小部件层次结构与元素和渲染树之间的区别

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

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

约束向下传递,大小向上传递

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

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

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

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

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 用户界面不是被转换成等效的 OS 小部件,而是由 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 渲染,这是一个将 OpenGL API 调用转换为 DirectX 11 等效项的库。

与其他代码集成

#

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

平台通道

#

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

平台通道如何允许 Flutter 与主机代码通信

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

dart
// Dart 端
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'; // 包含 .toNativeUtf16() 扩展方法

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, // 没有所有者窗口
    '测试消息'.toNativeUtf16(), // 消息
    '窗口标题'.toNativeUtf16(), // 窗口标题
    0, // 只有确定按钮
  );
}

在 Flutter 应用中渲染原生控件

#

因为 Flutter 内容被绘制到纹理上,并且其小部件树完全是内部的,所以 Android 视图之类的内容无法存在于 Flutter 的内部模型中,也无法在 Flutter 小部件内交错渲染。对于希望在其 Flutter 应用中包含现有平台组件(例如浏览器控件)的开发人员来说,这是一个问题。

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

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

不可避免地,与这种同步相关联的是一定数量的开销。因此,一般来说,这种方法最适合于像 Google 地图这样复杂控件,在这些控件中,在 Flutter 中重新实现是不切实际的。

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

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

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

在父应用程序中托管 Flutter 内容

#

前面场景的相反情况是将 Flutter 小部件嵌入到现有的 Android 或 iOS 应用中。如前面部分所述,在移动设备上运行的新创建的 Flutter 应用托管在 Android 活动或 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 在标准浏览器 API 之上提供了引擎的重新实现。我们目前有两个选择:

在 Web 上渲染 Flutter 内容有两种方式:HTML 和 WebGL。在 HTML 模式下,Flutter 使用 HTML、CSS、Canvas 和 SVG。为了渲染到 WebGL,Flutter 使用一个编译到 WebAssembly 的 Skia 版本,称为 CanvasKit。虽然 HTML 模式提供了最佳的代码大小特性,但 CanvasKit 提供了到浏览器图形堆栈的最快路径,并且与原生移动目标相比,具有更高的图形保真度[4]

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

Flutter Web 架构

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

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

更多信息

#

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


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

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

  3. 这种方法有一些限制,例如,对于平台视图而言,透明度与其他 Flutter 小部件的组合方式不同。 ↩︎

  4. 一个例子是阴影,它必须使用 DOM 等效的基元进行近似,代价是一些保真度损失。 ↩︎