Flutter 架構概述
對 Flutter 架構的高階概述,包括其設計背後的核心原則和概念。
本文旨在提供 Flutter 架構的高階概述,包括其設計背後的核心原則和概念。如果您對如何構建 Flutter 應用的架構感興趣,請檢視 構建 Flutter 應用架構。
Flutter 是一個跨平臺 UI 工具包,旨在實現 iOS、Android、Web 和桌面等作業系統之間的程式碼複用,同時允許應用程式直接與底層平臺服務進行互動。其目標是使開發人員能夠交付高效能的應用程式,這些應用在不同平臺上擁有自然的原生體驗,既能兼顧各平臺的差異,又能最大限度地共享程式碼。
在開發過程中,Flutter 應用執行在提供有狀態熱過載(stateful hot reload)的虛擬機器(VM)中,無需完全重新編譯即可看到變更。釋出時,Flutter 應用會直接編譯為機器碼(Intel x64 或 ARM 指令集),若針對 Web 平臺則編譯為 JavaScript。該框架是開源的,採用寬鬆的 BSD 許可證,並擁有繁榮的第三方包生態系統,以補充核心庫的功能。
本概述分為以下幾個部分:
- 層模型:構建 Flutter 的各個部分。
- 響應式使用者介面:Flutter 使用者介面開發的核心概念。
- Widget 簡介:Flutter 使用者介面的基本構建塊。
- 渲染過程:Flutter 如何將 UI 程式碼轉換為畫素。
- 平臺嵌入層(Embedders)概述:讓移動端和桌面端作業系統執行 Flutter 應用的程式碼。
- Flutter 與其他程式碼整合:關於 Flutter 應用可用的不同技術的資訊。
- 對 Web 的支援:關於 Flutter 在瀏覽器環境中特性的總結性說明。
架構分層
#Flutter 被設計為一個可擴充套件的分層系統。它由一系列獨立的庫組成,每個庫都依賴於其下方的層。沒有任何一層擁有訪問更底層層的特權,框架的每一部分都被設計為可選和可替換的。
對於底層作業系統,Flutter 應用程式的打包方式與任何其他原生應用程式相同。平臺特定的嵌入層(Embedder)提供入口點;與底層作業系統協調以訪問渲染表面、輔助功能和輸入等服務;並管理訊息事件迴圈。嵌入層使用適合該平臺的語言編寫:目前 Android 為 Java 和 C++,iOS 和 macOS 為 Swift 和 Objective-C/Objective-C++,Windows 和 Linux 為 C++。透過使用嵌入層,Flutter 程式碼可以作為模組整合到現有應用程式中,也可以作為應用程式的全部內容。Flutter 包含了針對常見目標平臺的多個嵌入層,同時也存在其他嵌入層。
Flutter 的核心是 Flutter 引擎,它主要由 C++ 編寫,提供了支援所有 Flutter 應用程式所需的基元。引擎負責在需要繪製新幀時對組合後的場景進行光柵化。它提供了 Flutter 核心 API 的底層實現,包括圖形文字佈局、檔案和網路 I/O、Dart 執行時和編譯工具鏈。
引擎透過 dart:ui 向 Flutter 框架公開,該庫將底層的 C++ 程式碼封裝為 Dart 類。該庫公開了最低級別的基元,例如用於驅動輸入、圖形和文字渲染子系統的類。
通常,開發人員透過 Flutter 框架與 Flutter 進行互動,該框架提供了用 Dart 語言編寫的現代響應式框架。它包含了一套豐富的平臺、佈局和基礎庫,由一系列層組成。從下到上依次為:
- 基礎 基礎類(Foundation),以及諸如 動畫(animation)、繪製(painting) 和 手勢(gestures) 等構建塊服務,它們在底層基礎上提供了常用的抽象。
- 渲染層(rendering layer) 提供了處理佈局的抽象。使用此層,您可以構建一個可渲染物件的樹。您可以動態操作這些物件,樹會自動更新佈局以反映您的更改。
- Widgets 層(widgets layer) 是一種組合抽象。渲染層中的每個渲染物件在 Widgets 層都有一個對應的類。此外,Widgets 層允許您定義可重用的類組合。這是引入響應式程式設計模型的層級。
- Material 和 Cupertino 庫提供了一套全面的控制元件,它們使用 Widget 層的組合基元來實現 Material 或 iOS 設計語言。
Flutter 框架相對較小;開發人員可能使用的許多高階功能都實現為包,包括諸如 camera 和 webview 等平臺外掛,以及諸如 characters、http 和 animations 等基於 Dart 和 Flutter 核心庫構建的平臺無關功能。其中一些包來自更廣泛的生態系統,涵蓋了諸如 應用內支付、Apple 身份驗證 和 動畫 等服務。
本概述的其餘部分將大致從 UI 開發的響應式正規化開始,由下而上地深入各層。然後,我們將描述 Widget 是如何組合在一起並轉換為可作為應用程式一部分進行渲染的物件的。我們將描述 Flutter 如何在平臺層面與其他程式碼互動,最後簡要總結 Flutter 的 Web 支援與其他目標平臺的區別。
應用剖析
#下圖概述了由 flutter create 生成的常規 Flutter 應用的組成部分。它展示了 Flutter 引擎在此堆疊中的位置,標註了 API 邊界,並標識了各個元件所在的倉庫。下方的圖例說明了一些常用於描述 Flutter 應用元件的術語。
Dart 應用
- 將 Widget 組合成所需的 UI。
- 實現業務邏輯。
- 由應用開發者所有。
框架(Framework) (原始碼)
- 提供用於構建高質量應用的高階 API(例如:Widget、命中測試、手勢檢測、輔助功能、文字輸入)。
- 將應用的 Widget 樹組合成一個場景。
引擎(Engine) (原始碼)
- 負責對組合後的場景進行光柵化。
- 提供 Flutter 核心 API 的底層實現(例如:圖形、文字佈局、Dart 執行時)。
- 使用 dart:ui API 向框架公開其功能。
- 使用引擎的 Embedder API 與特定平臺整合。
嵌入層(Embedder) (原始碼)
- 與底層作業系統協調以訪問渲染表面、輔助功能和輸入等服務。
- 管理事件迴圈。
- 公開 平臺特定 API 以將嵌入層整合到應用中。
Runner
- 將嵌入層平臺特定 API 公開的部分組合成一個可在目標平臺上執行的應用包。
- 由
flutter create生成的應用模板的一部分,由應用開發者所有。
響應式使用者介面
#表面上看,Flutter 是 一種響應式、宣告式的 UI 框架,開發者在其中提供從應用程式狀態到介面狀態的對映,當應用程式狀態發生變化時,框架負責在執行時更新介面。該模型受到 Facebook React 框架相關工作 的啟發,其中包括對許多傳統設計原則的重新思考。
在大多數傳統 UI 框架中,使用者介面的初始狀態被描述一次,然後在執行時響應事件時由使用者程式碼單獨更新。這種方法的一個挑戰是,隨著應用程式複雜性的增加,開發者需要了解狀態變化如何在整個 UI 中級聯。例如,考慮以下 UI:
有很多地方可以更改狀態:顏色框、色相滑塊、單選按鈕。當用戶與 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 應用的容器,通常是 MaterialApp 或 CupertinoApp),正如這個簡單的示例所示:
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 組成,它們組合在一起產生強大的效果。
在可能的情況下,設計概念的數量保持在最低限度,同時允許總詞彙量較大。例如,在 Widgets 層中,Flutter 使用相同的核心概念(Widget)來表示螢幕繪製、佈局(定位和尺寸)、使用者互動、狀態管理、主題、動畫和導航。在動畫層中,Animation 和 Tween 這對概念涵蓋了大部分設計空間。在渲染層中,RenderObject 用於描述佈局、繪製、命中測試和輔助功能。在每種情況下,相應的詞彙量最終都很大:有數百個 Widget 和渲染物件,以及數十種動畫和 Tween 型別。
類層次結構故意設計得淺而廣,以最大化可能的組合數量,專注於每個都做好一件事的小型、可組合的 Widget。核心功能是抽象的,即使像填充(padding)和對齊(alignment)這樣的基本功能也是作為單獨的元件實現的,而不是內建到核心中。(這也與更傳統的 API 形成對比,在那些 API 中,填充等功能內置於每個佈局元件的通用核心中。)因此,例如,要居中一個 Widget,您需要將其包裝在 Center Widget 中,而不是調整概念上的 Align 屬性。
有用於填充、對齊、行、列和網格的 Widget。這些佈局 Widget 本身沒有視覺表現。相反,它們唯一的目的是控制其他 Widget 佈局的某個方面。 Flutter 還包括利用這種組合方法的實用 Widget。
例如,Container 這個常用的 Widget 由幾個負責佈局、繪製、定位和尺寸調整的 Widget 組成。具體來說,透過閱讀原始碼可以看到,Container 由 LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox 和 Transform Widget 組成。Flutter 的一個定義特徵是您可以深入任何 Widget 的原始碼進行檢查。因此,您無需子類化 Container 來生成自定義效果,而是可以以新穎的方式組合它和其他 Widget,或者僅使用 Container 作為靈感來建立新的 Widget。
構建 Widget
#如前所述,您透過重寫 build() 函式以返回新的元素樹來確定 Widget 的視覺表現。該樹以更具體的術語表示 Widget 的使用者介面部分。例如,工具欄 Widget 可能有一個 build 函式,返回一些 文字 和 各種 按鈕 的 水平佈局。根據需要,框架遞迴地要求每個 Widget 構建,直到樹完全由 具體可渲染物件 描述。然後,框架將這些可渲染物件縫合在一起,形成一個可渲染物件樹。
Widget 的 build 函式應無副作用。無論 Widget 先前返回了什麼,每當函式被要求構建時,它都應該返回一個新的 Widget 樹1。框架會完成繁重的工作,根據渲染物件樹(稍後會詳細描述)確定需要呼叫哪些 build 方法。有關此過程的更多資訊,可以在 深入 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 方法;相反,它們的使用者介面是透過其 State 物件構建的。
每當您修改 State 物件時(例如,透過遞增計數器),您必須呼叫 setState(),以通知框架透過再次呼叫 State 的 build 方法來更新使用者介面。
擁有單獨的狀態和 Widget 物件可以讓其他 Widget 以完全相同的方式處理無狀態和有狀態 Widget,而不必擔心丟失狀態。父級無需保留子級以保持其狀態,可以在任何時間建立子級的新例項,而不會丟失子級的持久狀態。框架負責在適當時查詢和重用現有狀態物件的所有工作。
狀態管理
#那麼,如果許多 Widget 可以包含狀態,狀態是如何管理和在系統中傳遞的呢?
與其他任何類一樣,您可以在 Widget 中使用建構函式來初始化其資料,因此 build() 方法可以確保任何子 Widget 都在例項化時使用它所需的資料:
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
其中 importantState 是包含對 Widget 很重要的狀態的類的佔位符。
然而,隨著 Widget 樹變得越來越深,在樹層級結構中上下傳遞狀態資訊變得非常繁瑣。因此,第三種 Widget 型別 InheritedWidget 提供了一種從共享祖先獲取資料的簡便方法。您可以使用 InheritedWidget 建立一個狀態 Widget,將通用祖先包裝在 Widget 樹中,如本例所示:
每當 ExamWidget 或 GradeWidget 物件需要來自 StudentState 的資料時,它現在可以使用如下命令訪問它:
final studentState = StudentState.of(context);
of(context) 呼叫會獲取構建上下文(指向當前 Widget 位置的控制代碼),並返回 樹中匹配 StudentState 型別的最近祖先。 InheritedWidget 還提供了一個 updateShouldNotify() 方法,Flutter 會呼叫它來確定狀態更改是否應觸發使用該資料的子 Widget 重建。
Flutter 本身在框架中廣泛使用 InheritedWidget 來處理共享狀態,例如應用程式的 視覺主題,其中包括在整個應用程式中普遍存在的 顏色和型別樣式等屬性。 MaterialApp 的 build() 方法在構建時將主題插入樹中,然後在層級結構的更深處,Widget 可以使用 .of() 方法查詢相關的主題資料。
例如
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 版本。對於 Flutter 在其他原生平臺(如 Windows 或 macOS)上的情況也是如此。
從使用者輸入到 GPU
#Flutter 應用於其渲染流水線的首要原則是:簡單即快。Flutter 有一條直接的流水線來決定資料如何流向系統,如下圖所示:
讓我們更詳細地瞭解一下其中的一些階段。
構建:從 Widget 到 Element
#考慮這個演示 Widget 層級結構的程式碼片段:
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
當 Flutter 需要渲染此片段時,它會呼叫 build() 方法,該方法返回一個基於當前應用狀態渲染 UI 的 Widget 子樹。在此過程中,build() 方法可以根據狀態按需引入新的 Widget。作為一個例子,在上述程式碼片段中,Container 具有 color 和 child 屬性。透過檢視 Container 的 原始碼,您可以看到,如果顏色不為 null,它會插入一個表示該顏色的 ColoredBox。
if (color != null)
current = ColoredBox(color: color!, child: current);
相應地,Image 和 Text Widget 可能會在構建過程中插入諸如 RawImage 和 RichText 的子 Widget。因此,最終的 Widget 層級結構可能會比程式碼表示的更深,如下所示2:
這就是為什麼當您透過除錯工具(如作為 Flutter/Dart DevTools 一部分的 Flutter inspector)檢查樹時,可能會看到比原始程式碼中深得多的結構。
在構建階段,Flutter 將程式碼中表達的 Widget 轉換為對應的 元素樹(element tree),每個 Widget 對應一個元素。每個元素代表樹層級中給定位置的 Widget 的特定例項。元素有兩種基本型別:
ComponentElement:其他元素的宿主。-
RenderObjectElement:參與佈局或繪製階段的元素。
RenderObjectElement 是其 Widget 模擬物件與底層 RenderObject(我們稍後會提到)之間的中介。
任何 Widget 的元素都可以透過其 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 在繪製其子級之前應用轉換。
大多數 Flutter Widget 由繼承自 RenderBox 子類的物件渲染,該子類表示 2D 笛卡爾空間中固定尺寸的 RenderObject。 RenderBox 提供了 盒子約束模型(box constraint model) 的基礎,為每個要渲染的 Widget 建立了最小和最大寬度及高度。
為了執行佈局,Flutter 以深度優先遍歷方式遍歷渲染樹,並將 尺寸約束(size constraints) 從父級傳遞給子級。在確定其尺寸時,子級 必須 尊重父級給出的約束。子級透過在父級建立的約束範圍內 向上傳遞尺寸 來響應。
在完成對樹的單次遍歷後,每個物件在父級的約束內都有一個定義的尺寸,並準備透過呼叫 paint() 方法進行繪製。
盒子約束模型作為一種以 O(n) 時間佈局物件的方式非常強大:
- 父級可以透過將最大和最小約束設定為相同的值來指定子物件的大小。例如,手機應用中最頂層的渲染物件會將子級限制為螢幕大小。(子級可以選擇如何使用該空間。例如,它們可能只是將想要渲染的內容居中在指定的約束內。)
- 父級可以規定子級的寬度,但在高度上給予子級靈活性(或者規定高度但在寬度上提供靈活性)。一個現實世界的例子是流式文字,它可能必須符合水平約束,但根據文字數量垂直變化。
即使子物件需要知道它有多少可用空間來決定如何渲染其內容,此模型也能工作。透過使用 LayoutBuilder Widget,子物件可以檢查傳遞下來的約束並使用它們來決定如何使用它們,例如:
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 會話的工作示例 或 這個樹莓派工作示例。
每個平臺都有自己的一套 API 和限制。一些簡短的平臺特定說明:
- 截至 Flutter 3.29,UI 和平臺執行緒在 iOS 和 Android 上已合併。具體來說,UI 執行緒被移除,Dart 程式碼在原生平臺執行緒上執行。有關更多資訊,請參閱 偉大的執行緒合併 影片。
- 在 iOS 和 macOS 上,Flutter 分別作為
UIViewController或NSViewController載入到嵌入層中。平臺嵌入層建立一個FlutterEngine,作為 Dart VM 和您的 Flutter 執行時的宿主;以及一個FlutterViewController,它連線到FlutterEngine以將 UIKit 或 Cocoa 輸入事件傳遞到 Flutter 中,並使用 Metal 或 OpenGL 顯示由FlutterEngine渲染的幀。 - 在 Android 上,預設情況下,Flutter 作為
Activity載入到嵌入層中。檢視由FlutterView控制,它根據 Flutter 內容的組合和 z-order 要求,將 Flutter 內容渲染為檢視或紋理。 - 在 Windows 上,Flutter 託管在傳統的 Win32 應用中,內容使用 ANGLE 渲染,這是一個將 OpenGL API 呼叫轉換為 DirectX 11 等效項的庫。
與其他程式碼整合
#無論您是訪問用 Kotlin 或 Swift 等語言編寫的程式碼或 API、呼叫原生 C API、在 Flutter 應用中嵌入原生控制元件,還是將 Flutter 嵌入到現有應用程式中,Flutter 都提供了多種互操作性機制。
平臺通道 (Platform channels)
#對於移動端和桌面端應用,Flutter 允許您透過 平臺通道 (platform channel) 呼叫自定義程式碼,這是一種在 Dart 程式碼和宿主應用的平臺特定程式碼之間進行通訊的機制。透過建立公共通道(封裝名稱和編解碼器),您可以在 Dart 和用 Kotlin 或 Swift 等語言編寫的平臺元件之間傳送和接收訊息。資料從 Dart 型別(如 Map)序列化為標準格式,然後反序列化為 Kotlin(如 HashMap)或 Swift(如 Dictionary)中的等效表示。
以下是一個短小的平臺通道示例,展示了從 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 到廣告再到相機和藍牙等裝置硬體。
外部函式介面 (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 的程式碼片段:
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(AndroidView 和 UiKitView)解決了這個問題,讓您可以在每個平臺上嵌入此類內容。平臺檢視可以與其他 Flutter 內容整合3。每個此類 Widget 都是底層作業系統的中介。例如,在 Android 上,AndroidView 具有三個主要功能:
- 在每次繪製幀時,製作原生檢視渲染的圖形紋理副本,並將其呈現給 Flutter,以便作為 Flutter 渲染表面的一部分進行組合。
- 響應命中測試和輸入手勢,並將其轉換為等效的原生輸入。
- 建立輔助功能樹的模擬,並在原生層和 Flutter 層之間傳遞命令和響應。
不可避免地,這種同步會帶來一定的開銷。因此,通常情況下,這種方法最適合 Google Maps 這樣複雜的控制元件,因為在 Flutter 中重新實現它們是不切實際的。
通常,Flutter 應用會基於平臺測試在 build() 方法中例項化這些 Widget。例如,來自 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 底層的原生程式碼通訊通常使用如前所述的平臺通道機制。
目前,平臺檢視在桌面平臺上不可用,但這並非架構限制;未來可能會新增支援。
在宿主應用中託管 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 隔離區(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 提供兩種渲染器:
| 渲染器 | 編譯目標 |
|---|---|
| CanvasKit | JavaScript |
| Skwasm | WebAssembly |
構建模式(Build modes) 是命令列選項,用於決定執行應用時可用的渲染器。
Flutter 提供兩種 構建 模式:
| 構建模式 | 可用渲染器 |
|---|---|
| default | CanvasKit |
| `--wasm` | Skwasm(首選),CanvasKit(回退) |
預設模式僅提供 CanvasKit 渲染器。 --wasm 選項使兩種渲染器都可用,並根據瀏覽器能力選擇引擎:如果瀏覽器有能力執行它,則首選 Skwasm,否則回退到 CanvasKit。
與 Flutter 執行的其他平臺相比,最顯著的區別可能是 Flutter 不需要提供 Dart 執行時。相反,Flutter 框架(以及您編寫的任何程式碼)被編譯為 JavaScript。還值得注意的是,Dart 在其所有模式(JIT 與 AOT,原生與 Web 編譯)下的語言語義差異極小,大多數開發者永遠不會編寫一行會遇到這種差異的程式碼。
在開發階段,Flutter Web 使用 dartdevc,這是一個支援增量編譯並因此允許熱重啟(hot restart)和 [behind a flag 的熱過載][] 的編譯器。相反,當您準備好為 Web 建立生產應用時,將使用 Dart 高度最佳化的生產 JavaScript 編譯器 dart2js,將 Flutter 核心和框架以及您的應用程式打包到一個可以部署到任何 Web 伺服器的最小化原始檔中。程式碼可以透過 延遲匯入 在單個檔案中提供或拆分為多個檔案。
有關 Flutter Web 的更多資訊,請檢視 Flutter 的 Web 支援 和 Web 渲染器。
更多資訊
#對於那些有興趣瞭解更多關於 Flutter 內部機制的人,深入 Flutter 白皮書提供了該框架設計理念的有用指南。