給 Swift 開發者的 Flutter 並行處理指南
Dart 與 Swift 都支援並行(concurrent)程式設計。
本指南將協助你了解 Dart 中的並行處理運作方式,以及它與 Swift 的比較。
透過這些知識,你可以打造高效能的 iOS 應用程式。
在 Apple 生態系統開發時,
有些任務可能需要較長時間才能完成。
這些任務包括擷取或處理大量資料。
iOS 開發者通常會使用 Grand Central Dispatch(GCD)
來透過共享執行緒池排程任務。
使用 GCD 時,開發者將任務加入 dispatch queue,
GCD 會決定要在哪一條執行緒上執行這些任務。
然而,GCD 會啟動多條執行緒來
處理剩餘的工作項目。
這代表你最終可能會有大量執行緒,
導致系統資源過度分配。
自從 Swift 採用結構化並行(structured concurrency)模型後,
減少了執行緒數量與上下文切換。
現在,每個核心僅有一條執行緒。
Dart 採用單執行緒(single-threaded)執行模型,
並支援 Isolates、事件迴圈(event loop)與非同步(asynchronous)程式碼。
Isolate 是 Dart 實作的輕量級執行緒。
除非你主動建立 Isolate,否則 Dart 程式碼會在
由事件迴圈驅動的主 UI 執行緒上執行。
Flutter 的事件迴圈
等同於 iOS 的主迴圈(main loop)——換句話說,
就是附加在主執行緒上的 Looper。
Dart 的單執行緒模型並不代表
你必須將所有操作都設為阻塞式,導致 UI 停滯。
相反地,你可以善用 Dart 語言提供的非同步
功能,例如 async/await。
非同步程式設計
#非同步操作允許其他操作
在其完成之前先行執行。
Dart 與 Swift 都支援使用 async 與 await 關鍵字來撰寫非同步函式。
在這兩種語言中,async 標記某個函式
會執行非同步工作,
await 則告訴系統要等待該函式的結果。這代表 Dart VM 可能
會在必要時暫停該函式。
想深入了解非同步程式設計,請參閱
Concurrency in Dart。
善用主執行緒/isolate
#在 Apple 作業系統中,主要(也稱為主)
執行緒是應用程式啟動時運行的地方。
使用者介面的繪製永遠在主執行緒上進行。
Swift 與 Dart 之間的一個差異是
Swift 可能會針對不同任務使用不同的執行緒,
而且 Swift 並不保證會使用哪一條執行緒。
因此,在 Swift 中進行 UI 更新時,
你可能需要確保該操作發生在主執行緒上。
假設你想撰寫一個非同步擷取天氣
並顯示結果的函式。
在 GCD 中,若要手動將處理派送到主執行緒,
你可能會這樣做:
首先,定義 Weather enum:
enum Weather: String {
case rainy, sunny
}接下來,定義 view model,並將其標記為 @Observable,
該 view model 會發佈型別為 Weather? 的 result。
使用 GCD(Grand Central Dispatch)建立背景 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(行為者)來支援對共享可變狀態的同步處理。
為了確保工作在主執行緒(main thread)上執行,請定義一個標記為 @MainActor 的 view model 類別,
並在其中建立一個 load() 函式,該函式會在內部透過 Task 呼叫非同步函式。
@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 將 view model 定義為一個 state,
並提供一個可由 view model 呼叫的 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 }接下來,定義一個簡單的 view model(類似於在 SwiftUI 中建立的), 用來取得天氣資料。在 Dart 中,Future 物件代表一個未來會提供的值。 Future 與 Swift 的 @Observable 類似。 在這個範例中,view model 內的一個函式 會回傳一個 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
元件 (Widgets) 用於在 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 的裝置。 為了提升這些應用程式的效能, 有時你必須讓任務在不同核心上同時(concurrently)執行。 這點尤其重要,能避免長時間運算導致 UI 畫面渲染被阻塞。
在 Swift 中,你可以利用 GCD(Grand Central Dispatch)在全域佇列(global queues)上執行任務, 並指定不同的服務品質等級(quality of service class, 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 中,你可以將運算工作卸載到 worker isolate(通常稱為背景工作者)。
一個常見的情境是啟動一個簡單的 worker isolate,並在該 worker 結束時,透過訊息傳回結果。
從 Dart 2.19 開始,你可以使用 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,執行回呼函式(callback function):
final jsonData = await compute(getNumberOfKeys, jsonString);在這個例子中,回呼函式(callback function)是一個頂層函式,如下所示:
Map<String, dynamic> getNumberOfKeys(String jsonString) {
return jsonDecode(jsonString);
}你可以在Learning Dart as a Swift developer找到更多關於 Dart 的資訊,
而關於 Flutter 的更多資訊,請參考Flutter for SwiftUI developers或Flutter for UIKit developers。