面向 Swift 開發者的 Flutter 併發指南
在學習 Flutter 和 Dart 的過程中充分利用您的 Swift 併發知識。
Dart 和 Swift 都支援併發程式設計。本指南旨在幫助您理解 Dart 的併發工作原理及其與 Swift 的區別。透過理解這些概念,您可以建立高效能的 iOS 應用。
在蘋果生態系統中進行開發時,某些任務可能需要很長時間才能完成。這些任務包括獲取或處理大量資料。iOS 開發者通常使用 Grand Central Dispatch (GCD) 透過共享執行緒池來排程任務。使用 GCD,開發者可以將任務新增到排程佇列(dispatch queues)中,由 GCD 決定在哪個執行緒上執行它們。
然而,GCD 會啟動多個執行緒來處理剩餘的工作項。這意味著您最終可能會產生大量的執行緒,從而導致系統超載。而在 Swift 中,結構化併發模型減少了執行緒數量和上下文切換。現在,每個核心只有一個執行緒。
Dart 採用單執行緒執行模型,並支援 Isolates(隔離區)、事件迴圈和非同步程式碼。Isolate 是 Dart 對輕量級執行緒的實現。除非您顯式生成一個 Isolate,否則您的 Dart 程式碼將執行在由事件迴圈驅動的主 UI 執行緒中。Flutter 的事件迴圈等同於 iOS 的主迴圈——換句話說,就是附加在主執行緒上的 Looper。
Dart 的單執行緒模型並不意味著您必須將所有操作都設為阻塞式,從而導致 UI 卡頓。相反,請使用 Dart 語言提供的非同步功能,例如 async/await。
非同步程式設計
#非同步操作允許在它完成之前執行其他操作。Dart 和 Swift 都支援使用 async 和 await 關鍵字的非同步函式。在這兩種語言中,async 標記函式執行非同步工作,而 await 則告訴系統等待函式的返回結果。這意味著 Dart 虛擬機器(VM)在必要時可以掛起該函式。有關非同步程式設計的更多詳細資訊,請查閱 Dart 中的併發。
利用主執行緒/Isolate
#對於蘋果作業系統,主執行緒(也稱為 UI 執行緒)是應用程式開始執行的地方。使用者介面的渲染始終在主執行緒上進行。Swift 和 Dart 之間的一個區別是,Swift 可能會針對不同的任務使用不同的執行緒,且 Swift 不保證具體使用哪個執行緒。因此,在 Swift 中分發 UI 更新時,您可能需要確保工作是在主執行緒上執行的。
假設您要編寫一個函式來非同步獲取天氣並顯示結果。
在 GCD 中,若要手動將程序排程到主執行緒,您可能會執行以下操作:
首先,定義 Weather enum(列舉)。
enum Weather: String {
case rainy, sunny
}
接下來,定義檢視模型並將其標記為 @Observable,用於釋出型別為 Weather? 的 result。使用 GCD 建立一個後臺 DispatchQueue 將工作傳送到執行緒池,然後排程回主執行緒以更新 result。
@Observable class ContentViewModel {
private(set) var result: Weather?
private let queue = DispatchQueue(label: "weather_io_queue")
func load() {
// Mimic 1 second network delay.
queue.asyncAfter(deadline: .now() + 1) { [weak self] in
DispatchQueue.main.async {
self?.result = .sunny
}
}
}
}
最後,顯示結果。
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
Text(viewModel.result?.rawValue ?? "Loading...")
.onAppear {
viewModel.load()
}
}
}
最近,Swift 引入了 actors 來支援共享可變狀態的同步。為了確保在主執行緒上執行工作,請定義一個標記為 @MainActor 的檢視模型類,並在其中包含一個使用 Task 內部呼叫非同步函式的 load() 函式。
@MainActor @Observable class ContentViewModel {
private(set) var result: Weather?
func load() async {
// Mimic 1 second network delay.
try? await Task.sleep(nanoseconds: 1_000_000_000)
self.result = .sunny
}
}
接下來,使用 @State 將檢視模型定義為狀態,幷包含一個可由檢視模型呼叫的 load() 函式。
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
Text(viewModel.result?.rawValue ?? "Loading...")
.task {
await viewModel.load()
}
}
}
在 Dart 中,所有工作預設在主 Isolate 上執行。要使用 Dart 實現相同的示例,首先,建立 Weather enum。
enum Weather { rainy, windy, sunny }
然後,定義一個簡單的檢視模型(類似於 SwiftUI 中建立的模型)來獲取天氣。在 Dart 中,Future 物件表示未來將要提供的值。Future 類似於 Swift 的 @Observable。在此示例中,檢視模型中的函式返回一個 Future<Weather> 物件。
@immutable
class HomePageViewModel {
const HomePageViewModel();
Future<Weather> load() async {
await Future.delayed(const Duration(seconds: 1));
return Weather.sunny;
}
}
此示例中的 load() 函式與 Swift 程式碼有相似之處。該 Dart 函式被標記為 async,因為它使用了 await 關鍵字。
此外,標記為 async 的 Dart 函式會自動返回一個 Future。換句話說,您無需在標記為 async 的函式內手動建立 Future 例項。
最後一步是顯示天氣值。在 Flutter 中,FutureBuilder 和 StreamBuilder 元件用於在 UI 中顯示 Future 的結果。以下示例使用了 FutureBuilder。
class HomePage extends StatelessWidget {
const HomePage({super.key});
final HomePageViewModel viewModel = const HomePageViewModel();
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
// Feed a FutureBuilder to your widget tree.
child: FutureBuilder<Weather>(
// Specify the Future that you want to track.
future: viewModel.load(),
builder: (context, snapshot) {
// A snapshot is of type `AsyncSnapshot` and contains the
// state of the Future. By looking if the snapshot contains
// an error or if the data is null, you can decide what to
// show to the user.
if (snapshot.hasData) {
return Center(child: Text(snapshot.data.toString()));
} else {
return const Center(child: CupertinoActivityIndicator());
}
},
),
);
}
}
有關完整示例,請檢視 GitHub 上的 async_weather 檔案。
利用後臺執行緒/Isolate
#Flutter 應用可以在多種多核硬體上執行,包括執行 macOS 和 iOS 的裝置。為了提高這些應用的效能,有時您必須在不同的核心上併發執行任務。這對於避免長時間執行的操作阻塞 UI 渲染尤為重要。
在 Swift 中,您可以利用 GCD 在具有不同服務質量(qos)屬性的全域性佇列上執行任務。這指示了任務的優先順序。
func parse(string: String, completion: @escaping ([String:Any]) -> Void) {
// Mimic 1 sec delay.
DispatchQueue(label: "data_processing_queue", qos: .userInitiated)
.asyncAfter(deadline: .now() + 1) {
let result: [String:Any] = ["foo": 123]
completion(result)
}
}
}
在 Dart 中,您可以將計算解除安裝到工作 Isolate(通常稱為後臺工作者)。一種常見的場景是生成一個簡單的工作 Isolate,並在工作者退出時透過訊息返回結果。您可以使用 Isolate.run() 來生成一個 Isolate 並執行計算。
void main() async {
// Read some data.
final jsonData = await Isolate.run(() => jsonDecode(jsonString) as Map<String, dynamic>);`
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
在 Flutter 中,您還可以使用 compute 函式來啟動一個 Isolate 來執行回撥函式。
final jsonData = await compute(getNumberOfKeys, jsonString);
在這種情況下,回撥函式是一個頂層函式,如下所示:
Map<String, dynamic> getNumberOfKeys(String jsonString) {
return jsonDecode(jsonString);
}
您可以在 Swift 開發者學習 Dart 中找到更多關於 Dart 的資訊,並在 面向 SwiftUI 開發者的 Flutter 或 面向 UIKit 開發者的 Flutter 中找到更多關於 Flutter 的資訊。