本文旨在提供 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 的不同。↩︎