每个 Flutter 开发者都应该知道的框架总览

20,013 阅读24分钟

这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

本篇文章翻译自官方的👉总览文章,这篇文章基本上把 Flutter 介绍清楚了,如果想从总体上知道 Flutter 是咋回事,本篇文章是最好的教程了。

以下是正文


本文旨在从高层级提供一个 Flutter 框架结构的总体概览,介绍一些其设计上的核心原则和概念。

Flutter 是一个跨平台的 UI 工具包,目的是一份代码可以运行在不同的操作系统上,比如 Android、IOS等等,同时也可以让应用直接和底层的平台服务交互。我们的目标是:尽量用一份代码,开发者就可以在不用的平台上开发出高性能、高保真的 APP。拥抱差异,更少代码,更高性能

在开发阶段,Flutter 运行在虚拟机上,虚拟机提供了 hot reload 的功能,可以加载每次开发者改动的差异代码,而不需要全代码的编译。在正式版本上,Flutter 应用是直接编译成了机器码:Intel x64、ARM、JavaScript等等。这里说的开发阶段和正式版本是指 Flutter 产物的模式,Flutter 的产物有三种模式 debug、release、profile。Flutter 的 framework 是开源的,开源的协议是 BSD 协议,并且有活跃繁荣的第三方库社区,这些优秀的第三方库很好的补充了 Flutter 的能力。

本文的总览分为以下几个部分:

  1. 分层模型: Flutter 的组成部分
  2. 响应式 UI : Flutter UI 开发的核心概念
  3. Widgets 介绍: Flutter UI 代码构建的基础
  4. 渲染流程: Flutter 是如何将 UI 代码转化为屏幕像素点的
  5. 平台嵌入 的总览: 让移动端和桌面系统运行 Flutter 应用
  6. 用其他代码集成 Flutter: 介绍 Flutter 可用的不同的技术信息
  7. Web 的支持: 总结 Flutter 在浏览器环境中的特点

框架分层

从设计上来看,Flutter 框架是可扩展的、分层的。Flutter 由一系列的单独的依赖包组成,而且这些依赖包依赖底层。上层没有权限访问下层,并且框架层的每个部分都是可插拔的。

image.png

对于底层的操作系统来说,Flutter 应用被打成的应用包和其他的 Native 应用是一样。平台特定的嵌入层提供了一个入口:协调底层操作系统来访问一些底层的服务,比如渲染桌面、可访问性、输入等,管理事件循环。这个嵌入层是被特定的平台语言开发的,Android 系统是 Java 和 C++,iOS 和 macOS 系统是 Objective-C/Objective-C++,Windows 和 Linux 系统是 C++。正是由于这一层的存在,Flutter 代码可以集成进已经存在的应用,也可以直接使用 Flutter 代码打包整个应用。Flutter 为通用的平台提供了一些嵌入器,其他的嵌入器也是存在的

Flutter 的核心是 Flutter engine,engine 是 C++ 开发的,并且 Flutter 应用提供最 原始的支持,比如协议、通道等等。当新的一帧需要被绘制的时候,Flutter engine 就会栅格化代码合成的绘制信息。Engine 也为上层封装了访问底层的 API:图形图像化、文本布局、文件和网络 I/O、访问性支持、插件架构、Dart运行时、Dart编译工具链等等。

Flutter engine 暴漏是通过 dart:ui 这个库来暴漏给上一层的,这个库用 Dart 类包装了底层的 C++ 代码。像上面说的 engine 的功能,这个库包含了驱动输入、图形化、文本渲染系统等功能。

Typically, developers interact with Flutter through the Flutter framework, which provides a modern, reactive framework written in the Dart language. It includes a rich set of platform, layout, and foundational libraries, composed of a series of layers. Working from the bottom to the top, we have: 一般来说,开发者通过 Flutter framework 来和 Flutter 交互,这一层是 Dart 代码,提供了现代的、响应式的 Flutter 框架。这一层包括了和平台、布局、基础相关的库,并且也是分层的,自底向上以次有:

  • 必要的基础类以及常用的底层代码块的抽象,比如动画绘制手势

  • 处理布局的rendering layer,在这一层,可以构建一棵渲染对象的节点树,你也可以动态的操作这些节点,那么布局就会自动响应你的改变。

  • 合成抽象的 widgets layer,渲染层的每一个渲染对象在 Widget 层都会有一个 Widget 对象与之对应。另外,在这一层开发者也可以定义一些可以复用的组合类,就是这这一层引入了响应式框架。

  • [Material] 和 [Cupertino] 库, 提供了全套的 Material和 iOS 风格的原始组件。

Flutter 框架是相对来说比较小的,一些开发者用到的高级功能大多是以包的形式实现的,比如像 camerawebview 这样的平台插件,像 charactershttpanimations 这样的平台无关的包,平台无关的包可以完全依赖 Dart 和 Flutter依赖。这些高级包有一些是生态共建的,比如支付、苹果证书、动画等等。

下面就从响应式 UI 编程以此向下层展开描述。主要内容有,介绍 Widget 是怎么结合到一起的,Widget 是怎么转化为渲染对象的,介绍 Flutter 是怎么集成以及互操作平台代码的,最后简要总结Flutter 的 Web支持。

响应式 UI

总体上来说,Flutter 是一个响应式的非声明式的UI 框架,这意味着开发者仅仅需要提供程序状态与程序 UI 的映射,框架会在应用状态改变的时候自动完成 UI 的刷新。这种设计的灵感得益于 Facebook 的 React 框架,React 框架对很多传统的设计原则进行了重新思考。

在大多数传统的 UI 框架中,UI 的初始化状态只被描述一次,然后为了响应各种事件会单独的更新。这种方法的一个痛点是,随着应用复杂性的增长,开发者需要时刻注意状态的改变是怎么层叠地贯穿整个 UI 的。比如,考虑下面的 UI:

image.png

上面有许多状态可以改变的地方:Color box、色带 Slider、Radio按钮等等。只要用户和 UI 交互,那么改变必须被响应到每一个地方。更麻烦的是,页面一个很小的改动,比如拖动一下色带,可能会导致一系列连锁的反应,进而影响到很多看似不相干的代码。比如色带的拖动,文本框里面也要改变。

一种解决的方案是像 MVC 这样的代码开发架构,通过 controller 将数据的改变推送到 model,然后,model 通过 controller 将新的状态 推送给 view。但是,这样的方式其实也是有瑕疵的,因为创建和更新 UI 元素是两个单独的步骤,可能会导致不同步。

沿着其他响应式框架的脚步👣,Flutter 通过 UI 与底层状态彻底的解耦来解决这个问题。在响应式的 API 开发背景下,开发者仅仅创建 UI 的描述,framework 会在运行时使用我们的描述创建或者更新界面。

在 Flutter 中,我们所说的组件是 Widget,并且 Widget 是不可变的,可以形成 Widget 树形结构。这些组件用于管理独立的布局对象树,布局树用与管理独立的合成对象树。Widget 树到布局树再到合成树。Flutter的核心就是,确保可以有效的修改树中部分节点:把上层树转化成低层级的树(Widget到布局),并且在这些树上传递改变。

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

开发者需要在 Widget 的 build() 方法中将状态转化为 UI:

UI = f(state)

在 Flutter 设计中,build() 方法执行起来会很快,并且没啥副作用,framework 会在需要调用的时候调用它。

这种响应式的框架需要一些特定的语言特征(对象快速实例化和删除),而 Dart 就很符合干这件事

Widgets

正如前面所提,Flutter 着重强调 Widget 是合成的一个单元。Flutter 应用的 UI 就是 Widget 构建块,并且每一个 Widget 都是一份不可变的描述。

Widget 在组合的基础上形成一个体系结构。每一个 Widget 都嵌套在它的父节点里面,并且从父节点接受上下文。这种结构一直延伸到根 Widget,像下面的简单代码:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Home Page'),
        ),
        body: Center(
          child: Builder(
            builder: (BuildContext context) {
              return Column(
                children: [
                  const Text('Hello World'),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      print('Click!');
                    },
                    child: const Text('A button'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

在这个代码中,所有的类都是 Widget。

用户交互的时候会生成事件,App会更新 UI 来响应事件。更新的方式是告诉 framework 用另一个 Widget 来替换 Widget。framework 就会比较 新的和旧的 Widget,并且有效的更新 UI。

Flutter 有其自己的实现机制来控制 UI,而不是按照系统所提供的方式:比如说,这段代码是iOS Switch controlAndroid control纯 Dart 的实现。

这种方式有以下几点好处:

  • 无限的可扩展性。比如开发者想要一个 Switch 组件,那么可以用任意的方式来创建,不需要局限于操作系统所提供的

  • 避免性能瓶颈。这种方式运行 Flutter 一次就合成整个屏幕信息,而不需要在 Flutter 代码和 平台代码之间来回切换

  • 将应用的执行和操作系统的依赖解耦。Flutter 应用在操作系统的所有版本上运行的效果是一样的,即使操作系统改变了他自己的一些控件实现。

组合先与继承

Widget 通常由许多更小的 、用途更单一的 Widget 组成,组合起来的 Widget 往往可以产生更加有力的效果。

理想的效果,设计上概念的数量尽可能少,然而实际的总量表要尽可能大。比如说,在 Widget 层,概念只有一个,那就是 Widget,表示屏幕绘制、布局、用户交互、状态管理、主题定制、动画、路由导航等等。在动画层,只有两个概念:Animation 和 Tween。在渲染层,只有一个概念 RenderObject,用于描述布局、绘制、点击、可访问。而这些层级中,每一层都有大量的具体实现来具化概念,比如有几百个 widget 和 render对象,几十个动画和插值类型。

Flutter 有意地将类的继承体系设计的浅而宽,目的是最大化组合的数量。每一个小粒度的 可组合的Widget尽量聚焦做好自己的功能。核心的功能是抽象的,即使是像间距、对齐这样的基础功能,也被设计成具体的 Widget,而不是把这些功能添加到基础的 Widget 中。因此,假如我们想要居中一个组件,不是添加 Align 属性,而是使用 Center 组件包裹。

间距、对齐、横向排列、竖向排列,表格等等都是 Widget,布局类型的 Widget 并没有它自己本身可视的样子。但是呢,它们就是控制其他组件的布局。Flutter 也包括了一些功能性组件,这些功能组件也利用这种组合的方法。

比如,Container 是非常常用的组件,它本身是有负责布局、绘制、定位、尺寸的几个小 Widget 组成,具体来说,Container 由 LimitedBoxConstrainedBoxAlignPaddingDecoratedBoxTransform 组成。Flutter 的一个很明显的特征是,我们可以深入源码,去看去检查源码。因此,我们不需要泛化一个 Container 来实现自定义的效果,我们可以把它和另外一些 Widget 进行组合,或者参考 Container 写一个新的 Widget。

构建 Widget

正如前面提到的,build() 方法返回的内容就是页面上显示的内容,返回的元素会形成一颗 Widget 树,这个树以更具体的方式来表示 UI 的一部分。比如说,toolbar Widget 的 build 方法 构建了横向的布局,包括了 文本、按钮等等。根据需要,framework 会递归的 build 每一个 Widget 直到 Widget 树可以被更具化的渲染对象完全描述下来。framework 会将渲染对象拼合成一颗渲染树。

Widget 的 build 方法应该是无副作用的。只要方法被调用了,那么不管旧的 Widget 树是什么,一颗新的 Widget 树都会被创建。framework 做了大量的工作,来决定哪一个 Widget 的 build 方法需要被执行。具体的过程可以参考Inside Flutter topic

在每一个渲染帧,Flutter 仅仅会再次创建 UI 中需要创建的部分,这一部分就是状态变化的地方,创建的方式是执行 build 方法。所以,build 方法耗时应该非常小。一些比较重的计算应该放在异步中,将计算的结果作为状态的一部分,build 方法只是用数据。

虽然这种方式相对有点直白,但是这种自动比较的方式很高效,能够保正高性能、高保真。而且,build 方法的这种设计可以简化代码,让开发者聚焦在 Widget 的声明上,脱离状态与 UI 复杂的交互。

Widget state

框架里面有两个最主要的 Widget 类:StatefulWidget 和 StatelessWidget

许多 Widget 都是无状态的:它们的属性不随着时间改变。这样的 Widget 是 StatelessWidget 的子类。

但是呢,如果 Widget 的某个特征需要根据用户交互或者其他因素发生改变,那么这种 Widget 是 StatefulWidget。比如说,如果一个 Widget 有一个计数器,当用户点击按钮的时候,计数器需要变化,那么计数器就是 Widget 的 State。当值改变的时候,Widget 需要被重新构建来更新部分 UI(显示数字的那部分)。这样的 Widget 就是 StatefulWidget,因为 Widget 本身是不可变的,所以把状态存储在 可变的 State 子类中。StatefulWidget 没有 build 方法,相反,它的 UI 构建放到了 State 对象中。

只要想要改变 State 对象的状态,那么就调用 setState() 方法来告诉 framework : 你应该调用我的 build 方法来更新 UI 来。

虽然 StatefulWidget 既有 State 对象又有Widget 对象,但是其他 Widget 可以像使用 StatelessWidget 一样使用 StatefulWidget,担心状态丢失等问题。父节点在需要的时候可以随时创建子组件,不需要保留前一个 state 对象,framework 做了查找和重用状态对象的所有工作。

状态管理

因此,如果保持状态的 Widget 非常的多,那么状态是怎么管理的呢?是怎么更好的在应用内传递呢?

像其他的类一样,开发者可以在 Widget 构造方法中初始化它的数据, build() 方法可以确保其用的数据已经初始化了:

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

随着节点树越来越深,状态的向上向下查找就变的十分糟糕了。因此,另一种类型的 Widget —— InheritedWidget 就应运而生了。这种类型的 Widget 提供了一个很容易的方式来获取祖先节点的数据。可以使用 InheritedWidget 来创建一个 StatefulWidget 祖先节点,就像下面一样:

image.png

Whenever one of the ExamWidget or ExamWidget objects needs data from StudentState, it can now access it with a command such as: 只要 ExamWidget 或者 ExamWidget 需要 StudentState 的数据,那么可以使用下面的方式:

final studentState = StudentState.of(context);

of(context) 从 context 开始向上查找,找到最近的指定类型的祖先节点。这里的类型是StudentStateInheritedWidget 也提供了一个 updateShouldNotify() 方法,这个方法决定了当状态改变的时候,是否来触发使用数据的子节点的更新重建。

Flutter 本身就广泛的使用 InheritedWidget 来共享状态,比如我们熟知的主题。MaterialAppbuild() 方法中插入了一个 theme 节点,并为 theme 填充了数据,这样 比theme 节点更深的节点就可以通过 .of() 来找到 theme 节点,并使用数据。比如:

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

Navigator 也用了这种方式,我们经常使用 Navigator 的 of 方法来 push 或者 pop 路由。MediaQuery 也用这种方式让开发者可以很快捷的获取屏幕相关信息,尺寸、方向、密度、模式等等。

随着应用的增长,更加先进高级的状态管理方案更加符合生产环境的开发,可以减少 StatefulWidget 的使用。许多 Flutter 应用使用 provider 这样的开源库。前面提到 Flutter 的分层结构可以无限扩展,flutter_hooks 这个第三方库提供了另外一种将状态转为 UI 的方式。

渲染与布局

这一节主要描述渲染管线,渲染管线包括了几个重要的步骤,将 Widget 真正的转化为实际的绘制像素。

Flutter 渲染模型

你可能会好奇:既然 Flutter 是一个跨平台的框架,它是怎么做到和单平台框架相当的性能效果呢?

我们先想一下传统的 Android app 是怎么运行的。当需要绘制的时候,开发者需要首先调用 Android 框架的 Java 代码。Android 系统提供的组件负责在 Canvas 对象中绘制,Android 使用 Skia 进行渲染。Skia 是 C/C++ 开发的图形库,会调用 CPU 或者 GPU 完成设备屏幕的绘制。

跨平台框架通常的做法是:在原生的 Android and iOS UI 层上创建一个抽象层,来尝试磨平每个平台的差异性。应用的代码一般是解释语言——JavaScript,必须和Java/Objective-C反复的交互来显示 UI。这些都增加了高额的负担,尤其是 UI 层和逻辑层有大量交互的时候。

Flutter 另辟蹊径,Flutter 最小化了这些抽象,避开系统提供的 UI,它自己有丰富的 Widget 库。绘制 Flutter 的 Dart 代码最终转为 native 代码,而这些 native 代码会使用 Skia 进行渲染。 Flutter 把 Skia 作为 引擎的一部分,这样开发者可以始终让应用保持到最新版本,而 Android 设备不需要更新。IOS等其他的设备也是相同的道理。

从用户输入到 GPU

Flutter 渲染管线的总原则是:简单就是快,Flutter 有一个简单明了的数据传输管道,沿着这个通道用户的输入流到了系统。正如下面所示:

image.png

下面我们来看更多的细节。

Build: 从 Widget 到 Element

Consider this code fragment that demonstrates a widget hierarchy: 思考一下下面的 Widget 体系代码片段:

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 有 color 和 child 属性。但是在 Container 的源码中,如果 color 不是null,那么会插入一个代表颜色的 ColoredBox 组件:

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

同样地,Image 和 Text 组件也插入了 RawImage 和 RichText 组件在 build 过程中。所以最终的 Widget 树可能会比代码更深,比如:

image.png

这就解释了为啥我们在 Flutter inspector 看到的节点要远远深于我们的原始代码。

在 build 阶段,Flutter 会将 widget 树 转为 element 树,每一个 Element 都对应一个 Widget。每一个 Element 表示一个指定位置的特定 Widget 实例。有两个不同类型的 Element:

  • ComponentElement, 承载其他 Element 的 Element
  • RenderObjectElement, 参与布局和绘制阶段的 Element

image.png

RenderObjectElement 是它的 Widget 和 背后的 RenderObject 的中介,这个后面再说。

Widget 的 Element 可以通过 BuildContext 来引用到,同时 BuildContext 也表示树的位置信息。比如 Theme.of(context) 的参数就是 BuildContext,并且 BuildContext 是 build 方法的参数。

因为 Widget 是不变的,所以 Widget 树的任意改变都会导致一组新的 Widget 要被创建,即使是像 Text('A') 到 Text('B') 这样的变化。但是,Widget 的重新构建,并不意味着背后的 Element、RenderObject 也要重新构建。Element 树是持久化的在帧与帧之间,Flutter 的高性能很大一原因就是这个持久化的设计。Flutter 会缓存 Element等背后的对象,所以完全舍弃旧的Widget 也没啥问题。通过只遍历已经修改的 Widget,Flutter可以仅仅重建需要重建的 Element 树。

布局和渲染

仅仅绘制一个 Widget 的应用几乎是不存在的。因此,框架需要高效的布局 Widget 层次树,并且在渲染在屏幕上之前,也要高效的计算 Element 的尺寸,标定 Element 的位置。

渲染对象的基类是 RenderObject,这个类定义了布局和绘制的通用抽象模型,像多维、极坐标这样的需要自定义渲染模型。每个  RenderObject 知道它的父节点是谁,但是子节点的信息知道的很少,仅仅知道怎么去 visit 子节点和子节点布局约束。但是对于抽象来说这就够了,RenderObject 可以处理各种用例。

在 build 阶段,Flutter 会创建或者更新 element 树上每一个 RenderObjectElement 背后的 RenderObject 对象。 RenderObject 是原始的抽象类:RenderParagraph 渲染文本,RenderImage 渲染图像,RenderTransform 会在子节点绘制之前应用位置等信息。

image.png

大多数 Flutter Widget 背后的渲染对象是 RenderBox 的子类,RenderBox 将模型定义为盒子模型——固定大小的二维笛卡尔坐标。RenderBox 提供基本的 盒子约束,每一个 Widget 都放在由最大最小宽度、最大最小高度限制的盒子内。

为了执行布局过程,Flutter 向下👇传递约束。向上👆传递尺寸。父节点设置位置

image.png

布局的遍历完成之后,每个对象都有了符合父节点约束的尺寸,就会调用 paint() 方法进行绘制。

盒子约束模型非常棒,布局的过程时间复杂度仅是*O(n)*的:

  • 父节点可以将最大值和最小值设置为相同的,这样子节点的大小就是固定的了。比如说,最根部的节点就强制其子节点为屏幕大小。(子节点可以选择如何使用这一部分空间,比如,子节点可以在空间内居中摆放)

  • 父节点可以让设置子节点的宽度,但是让高度灵活可变。或者设置高度,让宽度灵活可变。比如文本组件,文本组件的宽度是灵活可变的,高度是固定的。

除了上述描述:子节点要根据可用空间的多少来展示自己的显示也是可行的。使用 LayoutBuilder 可以达到这样的效果,子节点检查父节点传进来的约束,然后使用约束的信息来展示自己的内容,比如:

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

布局和约束更加详细的内容可以看这一篇文章👉深入理解Flutter布局约束

所有 RenderObject 的根节点是 RenderView,它就代表一颗渲染树。当平台决定绘制新的一帧时,就会调用 RenderViewcompositeFrame() 方法,方法内部创建了 SceneBuilder 去更新 scene。当 scene 准备完成之后,RenderView 对象会将合成的 scene 传递给 dart:ui 内的 Window.render() 方法,然后 GPU 就会渲染合成信息。

合成和光栅化阶段的更多细节可以参考 👉Flutter 渲染管线

Platform embedding

正如我们所示,Flutter 不像 React Native,把自己的组件转为 OS 的组件,让 OS 去渲染,它 是自己完成 build、布局、合成、绘制。获取纹理和 App 生命周期的机制也会因为平台的原因有所不同,比如 Android 的纹理和 IOS 的纹理在实现上就有所不同。Flutter 引擎是平台无关的,表现出来是 👉应用二进制接口,提供了一种平台嵌入器,可以安装和使用 Flutter。

平台嵌入器是一个原生系统的应用程序,承载着 Flutter 的所有的内容,并且充当了原生操作系统与 Flutter 之间的粘合剂。当我们打开 Flutter 应用的时候,嵌入器提供了一个入口:初始化 Flutter 引擎,获得 UI 线程和 光栅,创建 Flutter 纹理。嵌入器也负责:应用的生命周期、手势输入、窗口尺寸、线程管理和平台消息。Flutter 包含了 Android、iOS、Windows、macOS、Linux。开发者也可以自定义平台嵌入器,有两个比较不错的案例 👉 案例1 和 👉 案例2——Raspberry Pi

每一个平台尤其本身的API和约束。一些简明的平台原则如下:

  • 在 iOS 和 macOS,Flutter 是作为 UIViewController 或者 NSViewController 而被加载进嵌入器的。平台嵌入器创建了 FlutterEngine,而引擎可以当作 Dart VM 和 Flutter 运行时的宿主。FlutterViewControllerFlutterEngine 相绑定,将 UIKit 或者 Cocoa 输入事件传递给 Flutter,并且使用 Metal 或者 OpenGL 来渲染帧。

  • 在 Android 上,Flutter 默认加载到 Activity 中,视图就是 FlutterViewFlutterView 可以渲染 Flutter 的内容(ui 或者 纹理,取决于合成信息和 z 轴顺序),

  • 在 Windows 上,Flutter 被装载在传统的 Win32 应用中。Flutter 内容使用 ANGLE 渲染, 这个库可以将 OpenGL API 转为与之等价的 DirectX 11。目前正在做的事情是,提供一个使用 UWP 应用模型的 Windows 嵌入器,以及使用更加高效直接的方式将 DirectX 12 到 GPU,替换现有的 ANGLE。

集成其他代码

Flutter 提供了一些互操作的机制,访问 Kotlin 或者 Swift 编写的代码,调用 基于 C 的本地 API,在 Flutter 中嵌入原生组件,在既有应用中嵌入 Flutter。

Platform channels

对于移动端和桌面 App,通过 platform channel 机制,Flutter 可以让开发者调用自定义代码。platform channel 是 Dart 代码和 App 宿主平台代码通信的机制。通过创建一个通用的 channel (指定名字和编解码器),开发者能够在 Dart 和平台之间发送和接受消息。数据会被序列化,比如 Dart 的 Map 就是 Kotlin 中的 HashMap,Swift 的 Dictionary

image.png

下面是简单的事件处理器的代码,Android 是 Kotlin,iOS 是 Swift,Dart 调用原生的方法,并获取数据:

// Dart side
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
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)
  }
}

像这样的 channel 代码,可以在 flutter/plugins 仓库中找到,里面也有响应的 macOS 的实现。一些通用的场景大概有几千个可用插件,从广告到相机、蓝牙这样的硬件设备。

外部方法接口

对于 C基础的 API(包含 Rust、Go 生产的代码),Dart 也提供了直接的调用机制,可以使用 dart:ffi 依赖库来绑定 native 代码。Foreign function interface (FFI) 模型 没有数据数据序列化过程,所以它比上面的 channel 更快。Dart 运行时提供了在堆内存上分配内存的能力,堆上的内存是 Dart 对象内存,并且可以调用静态和动态的链接库。FFI 可用在除 web 之外的所有平台上,因为 js package 提供了相同的能力。

要使用 FFI 的话,可以创建一个 typedef 为每一个 Dart 的非管理的方法签名,并且指定 Dart VM 做了映射。下面是一个案例,调用 Win32 MessageBox() 的 API:

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 内容是被绘制在纹理上的,并且 组件树完全是内部的。像 Android view 存在在 Flutter 内部模型中,或者在 Flutter 组件交错渲染,这些情况我们咩有看到。如果不能支持的话,是有问题的,比如一些原生组件不能用的话,就很麻烦。比如 WebView。

Flutter 解决这种问题是通过平台视图 Widget 的方式(AndroidView 和 UiKitView)。这些组件可以嵌入平台原生组件。Platform Widget 可以和其他的 Flutter 内容一起集成,并且充当着与底层操作系统的中介。比如,在Android上,AndroidView 有三个主要的功能:

  • 复制原生视图的图形纹理,并把纹理作为 Flutter 合成渲染的一部分,所以在每一帧的时候都会进行这样的合成绘制。

  • 响应手势,并且把手势转为等价于 Native 的输入。

  • 创建一个可访问性树的模拟,并且在原生和 Flutter 层之间传递和响应命令

显而易见的,每帧的合成都是相当耗时的,像音视频也非常耗内存。所以,这种方法一般会在复杂交互的时候采用,比如 Google Maps 这样的,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');

AndroidView 或者 UiKitView 使用的我们前面提到的 platform channel 机制来和 原生代码交互。

目前,Platform Widget 在桌面平台上是不可用的,但是这不是架构上的限制,后面可能会添加。

宿主 App 接入 Flutter

和前面Flutter 嵌入 native 相反,这一节介绍 既有应用中嵌入 Flutter。前面我们提到了 Flutter 是被 Android 的 Activity,iOS 的 UIViewController 承载的,Flutter 的内容可以用相同的方式被嵌入。

Flutter module 模版很容易被嵌入,开发者可以使用 Gradle 或者 Xcode 进行源码依赖,也可以产物依赖。产物依赖的方式的好处就是项目组的成员不需要每个人都安装 Flutter 环境。

Flutter 引擎需要一点的时间初始化,因为需要加载 Flutter 依赖库,初始化 Dart 运行时,创建和运行 Dart 线程,绑定渲染 surface 到 UI。为了最小化上面提到的时间,减少呈现 Flutter UI 的延迟,最好的处理方式是在程序初始化的时候,初始化 Flutter 引擎,至少在第一个 第一个Flutter屏幕之前,这样用户不就会在显示 Flutter 第一个页面的时候,出现短暂的白屏或者黑屏。

关于接入的更多信息,可以在 👉Load sequence, performance and memory topic找到。

Flutter web support

一些通用的框架概念适用于 Flutter 支持的所有平台,但是呢,Flutter’s web 有一些值得讨论的独特的点。

自JavaScript语言存在以来,Dart就一直在编译JavaScript,并为开发和生产目的优化了工具链。许多重要的应用程序从Dart编译到JavaScript,并在今天的生产中运行,包括谷歌Ads的广告商工具。因为Flutter框架是用Dart编写的,所以将它编译成JavaScript相对简单。

从 Dart 语言面世以来,Dart 就一直在支持编译成 JavaScript,并且持续的为开发和生产优化工具链。许多重要的程序从 Dart 编译成 JavaScript,并在今天一直在运行,比如 👉advertiser tooling for Google Ads。因为 Flutter 框架是 Dart 开发的,把 Dart 编译成 JavaScript 相对来说是简单直接的。

However, the Flutter engine, written in C++, is designed to interface with the underlying operating system rather than a web browser. A different approach is therefore required. On the web, Flutter provides a reimplementation of the engine on top of standard browser APIs. We currently have two options for rendering Flutter content on the web: HTML and WebGL. In HTML mode, Flutter uses HTML, CSS, Canvas, and SVG. To render to WebGL, Flutter uses a version of Skia compiled to WebAssembly called CanvasKit. While HTML mode offers the best code size characteristics, CanvasKit provides the fastest path to the browser’s graphics stack, and offers somewhat higher graphical fidelity with the native mobile targets5. 然而,C++ 开发的 Flutter 引擎是操作系统底层的接口,而不是浏览器。因此,需要采取一个不同的方法。在 web 上,Flutter 在标准浏览器 API 之上 提供了重新实现。目前,在 Web 上渲染 Flutter 有两个方案:HTML 和 WebGL。HTML 模式下,Flutter 使用 HTML、 CSS、 Canvas 和 SVG。WebGL 模式下,Flutter 使用 CanvasKit 编译成 WebAssembly。 HTML 模式的包体积会很小,而 CanvasKit 的渲染会更快、渲染效果更佳高保真。

Web 版本的架构图是下面的:

Flutter web
architecture

和其他 Flutter 平台相比,最显著的区别是:不需要提供一个 Dart 的运行时。相反,Flutter 的framework 被编译成了 JavaScript。在 Dart 的众多模式中,比如 JIT、AOT、native、web,语言语义上的差别很小,开发者也不会在开发的过程中体验到差异。

在开发期间,Flutter web 使用 dartdevc,它支持增量编译,这就可以 hot restart 了。相反,如果想要创建一个线上正式版本的 web,就会使用 dart2js 编译器,这是一款高性能的 JavaScript 编译器,会将 Flutter 核心和框架与应用程序打包为可部署到任何 web 服务器的小型源文件。deferred imports 可以将代码封装为一个文件,或者分割为多个文件。

展望

如果想要更加深入了解 Flutter 内部的机制,那么可以看 Inside Flutter 文章。