Flutter 中的非同步程式設計總結
一、 Dart 中的事件迴圈模型
Dart 是一種單執行緒模型執行語言,其執行原理如下圖所示:
Dart 在單執行緒中是以訊息迴圈機制來執行的,其中包含兩個任務佇列,一個是“微任務佇列” microtask queue,另一個叫做“事件佇列” event queue。 從圖中可以發現,微任務佇列的執行優先順序高於事件佇列。
現在我們來介紹一下Dart執行緒執行過程,如上圖中所示,入口函式 main() 執行完後,訊息迴圈機制便啟動了。 首先會按照先進先出的順序逐個執行微任務佇列中的任務,當所有微任務佇列執行完後便開始執行事件佇列中的任務,事件任務執行完畢後再去執行微任務, 如此迴圈往復,生生不息。
在Dart中,所有的外部事件任務都在事件佇列中,如IO、計時器、點選、以及繪製事件等,而微任務通常來源於Dart內部,並且微任務非常少, 之所以如此,是因為微任務佇列優先順序高,如果微任務太多,執行時間總和就越久,事件佇列任務的延遲也就越久, 對於GUI應用來說最直觀的表現就是比較卡,所以必須得保證微任務佇列不會太長。
在事件迴圈中,當某個任務發生異常並沒有被捕獲時,程式並不會退出,而直接導致的結果是當前任務的後續程式碼就不會被執行了, 也就是說一個任務中的異常是不會影響其它任務執行的。
我們可以看出,將任務加入到MicroTask中可以被儘快執行,但也需要注意,當事件迴圈在處理MicroTask佇列時,Event佇列會被卡住,應用程式無法處理滑鼠單擊、I/O訊息等等事件。 同時,當事件循壞出現異常時,dart 中也可以通過 try/catch/finally 來捕獲異常。
二、任務排程
基於上述理論,看一下任務排程
2.1 新增到 MicroTask 任務佇列 兩種方法:
import 'dart:async';
void myTask(){
print("this is my task");
}
void main() {
/// 1. 使用 scheduleMicrotask 方法新增
scheduleMicrotask(myTask);
/// 2. 使用Future物件新增
new Future.microtask(myTask);
}
複製程式碼
兩個 task 都會被執行,控制檯輸出:
this is my task
this is my task
複製程式碼
2.2 新增到 Event 佇列。
import 'dart:async';
void myTask(){
print("this is my event task");
}
void main() {
new Future(myTask);
}
複製程式碼
控制檯輸出:
this is my event task
複製程式碼
示例:
import 'dart:async';
void main() {
print("main start");
new Future((){
print("this is my task");
});
new Future.microtask((){
print("this is microtask");
});
print("main stop");
}
複製程式碼
控制檯輸出:
main start
main stop
this is microtask
this is my task
複製程式碼
可以看到,程式碼的執行順序並不是按照我們的編寫順序來的,將任務新增到佇列並不等於立刻執行,它們是非同步執行的,當前main方法中的程式碼執行完之後,才會去執行佇列中的任務,且MicroTask佇列執行在Event佇列之前。
2.3、延時任務
new Future.delayed(new Duration(seconds:1),(){
print('task delayed');
});
複製程式碼
使用 Future.delayed 可以使用延時任務,但是延時任務不一定準
import 'dart:async';
import 'dart:io';
void main() {
var now = DateTime.now();
print("程式執行開始時間:" + now.toString());
new Future.delayed(new Duration(seconds:1),(){
now = DateTime.now();
print('延時任務執行時間:' + now.toString());
});
new Future((){
// 模擬耗時5秒
sleep(Duration(seconds:5));
now = DateTime.now();
print("5s 延時任務執行時間:" + now.toString());
});
now = DateTime.now();
print("程式結束執行時間:" + now.toString());
}
複製程式碼
輸出結果:
程式執行開始時間:2019-09-11 20:46:18.321738
程式結束執行時間:2019-09-11 20:46:18.329178
5s 延時任務執行時間:2019-09-11 20:46:23.330951
延時任務執行時間:2019-09-11 20:46:23.345323
複製程式碼
可以看到,5s 的延時任務,確實實在當前程式執行之後的 5s 後得到了執行,但是另一個延時任務,是在當前延時的基礎上又延時執行的, 也就是,延時任務必須等前面的耗時任務執行完,才得到執行,這將導致延時任務延時並不準。
三、Future 與 FutureBuilder
Dart類庫有非常多的返回Future或者Stream物件的函式。 這些函式被稱為非同步函式:它們只會在設定好一些耗時操作之後返回,比如像 IO操作。而不是等到這個操作完成。
Future與JavaScript中的Promise非常相似,表示一個非同步操作的最終完成(或失敗)及其結果值的表示。簡單來說,它就是用於處理非同步操作的,非同步處理成功了就執行成功的操作,非同步處理失敗了就捕獲錯誤或者停止後續操作。一個Future只會對應一個結果,要麼成功,要麼失敗。
由於本身功能較多,這裡我們只介紹其常用的API及特性。還有,請記住,Future 的所有API的返回值仍然是一個Future物件,所以可以很方便的進行鏈式呼叫。
3.1 Future 使用
建立方法: Future的幾種建立方法
Future() Future.microtask() Future.sync() Future.value() Future.delayed() Future.error()
Future 和 Future.microtask 和 Future.delayed 上面已經演示過了。 Future.sync() 表示同步方法,會被立即執行: 如:
import 'dart:async';
void main() {
print("main start");
new Future.sync((){
print("sync task");
});
new Future((){
print("async task");
});
print("main stop");
}
複製程式碼
輸出:
main start
sync task
main stop
async task
複製程式碼
3.2 Future 中的回撥
當Future中的任務完成後,我們往往需要一個回撥,這個回撥立即執行,不會被新增到事件佇列。 如:
import 'dart:async';
void main() {
print("main start");
Future fut =new Future.value(18);
// 使用then註冊回撥
fut.then((res){
print(res);
});
// 鏈式呼叫,可以跟多個then,註冊多個回撥
new Future((){
print("async task");
}).then((res){
print("async task complete");
}).then((res){
print("async task after");
});
print("main stop");
}
複製程式碼
輸出結果:
main start
main stop
18
async task
async task complete
async task after
複製程式碼
使用 catchError 捕獲異常:
import 'dart:async';
void main() {
print("main start");
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//執行成功會走到這裡
print("success");
}).catchError((e){
//執行失敗會走到這裡
print(e);
});
print("main stop");
}
複製程式碼
輸出:
main start
main stop
Assertion failed
複製程式碼
在本示例中,我們在非同步任務中丟擲了一個異常,then的回撥函式將不會被執行,取而代之的是 catchError回撥函式將被呼叫;但是,並不是只有 catchError回撥才能捕獲錯誤,then方法還有一個可選引數onError,我們也可以它來捕獲異常:
import 'dart:async';
void main() {
print("main start2");
Future.delayed(new Duration(seconds: 2), () {
//return "hi world!";
throw AssertionError("Error");
}).then((data) {
print("success");
}, onError: (e) {
print(e);
});
print("main stop2");
}
複製程式碼
結果:
main start2
main stop2
Assertion failed
複製程式碼
whenComplete 一定得到執行:
import 'dart:async';
void main() {
print("main start3");
Future.delayed(new Duration(seconds: 2),(){
//return "hi world!";
throw AssertionError("Error");
}).then((data){
//執行成功會走到這裡
print(data);
}).catchError((e){
//執行失敗會走到這裡
print(e);
}).whenComplete((){
//無論成功或失敗都會走到這裡
print("this is the end...");
});
print("main stop3");
}
複製程式碼
結果:
main start3
main stop3
Assertion failed
this is the end...
複製程式碼
Future.wait 表示多個任務都完成之後的回撥。
import 'dart:async';
void main() {
print("main start4");
Future.wait([
// 2秒後返回結果
Future.delayed(new Duration(seconds: 2), () {
return "hello";
}),
// 4秒後返回結果
Future.delayed(new Duration(seconds: 4), () {
return " world";
})
]).then((results){
print(results[0]+results[1]);
}).catchError((e){
print(e);
});
print("main stop4");
}
複製程式碼
結果:
main start4
main stop4
hello world
複製程式碼
3.3 FutureBuilder 的使用
很多時候我們會依賴一些非同步資料來動態更新UI,比如在開啟一個頁面時我們需要先從網際網路上獲取資料,在獲取資料的過程中我們顯式一個載入框,等獲取到資料時我們再渲染頁面;又比如我們想展示Stream(比如檔案流、網際網路資料接收流)的進度。當然,通過StatefulWidget我們完全可以實現上述這些功能。 但由於在實際開發中依賴非同步資料更新UI的這種場景非常常見,因此Flutter專門提供了FutureBuilder和StreamBuilder兩個元件來快速實現這種功能。
FutureBuilder會依賴一個Future,它會根據所依賴的Future的狀態來動態構建自身。我們看一下FutureBuilder建構函式:
FutureBuilder({
this.future,
this.initialData,
@required this.builder,
})
複製程式碼
-
future:FutureBuilder依賴的Future,通常是一個非同步耗時任務。
-
initialData:初始資料,使用者設定預設資料。
-
builder:Widget構建器;該構建器會在Future執行的不同階段被多次呼叫,構建器簽名如下:
Function (BuildContext context, AsyncSnapshot snapshot)
複製程式碼
snapshot會包含當前非同步任務的狀態資訊及結果資訊 ,比如我們可以通過snapshot.connectionState獲取非同步任務的狀態資訊、通過snapshot.hasError判斷非同步任務是否有錯誤等等,完整的定義讀者可以檢視AsyncSnapshot類定義。
示例:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<String> mockNetworkData() async {
return Future.delayed(Duration(seconds: 2), () => "這是網路請求的資料。。。");
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: FutureBuilder<String>(
future: mockNetworkData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
// 請求已結束
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
// 請求失敗,顯示錯誤
return Text("Error: ${snapshot.error}");
} else {
// 請求成功,顯示資料
return Text("Contents: ${snapshot.data}");
}
} else {
// 請求未結束,顯示loading
return CircularProgressIndicator();
}
},
),
),
// This trailing comma makes auto-formatting nicer for build methods.
);
}
}
複製程式碼
效果:
四、async/await
在Dart1.9中加入了async和await關鍵字,有了這兩個關鍵字,我們可以更簡潔的編寫非同步程式碼,而不需要呼叫Future相關的API 將 async 關鍵字作為方法宣告的字尾時,具有如下意義
- 被修飾的方法會將一個 Future 物件作為返回值
- 該方法會同步執行其中的方法的程式碼直到第一個 await 關鍵字,然後它暫停該方法其他部分的執行;
- 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行程式碼將立即執行。
// 匯入io庫,呼叫sleep函式
import 'dart:io';
// 模擬耗時操作,呼叫sleep函式睡眠2秒
doTask() async{
await sleep(const Duration(seconds:2));
return "Ok";
}
// 定義一個函式用於包裝
test() async {
var r = await doTask();
print(r);
}
void main(){
print("main start");
test();
print("main end");
}
複製程式碼
結果:
main start
main end
Ok
複製程式碼
五、Stream 與 StreamBuilder
Stream 也是用於接收非同步事件資料,和Future 不同的是,它可以接收多個非同步操作的結果(成功或失敗)。 也就是說,在執行非同步任務時,可以通過多次觸發成功或失敗事件來傳遞結果資料或錯誤異常。 Stream 常用於會多次讀取資料的非同步任務場景,如網路內容下載、檔案讀寫等。
5.1 Stream 的使用
void main(){
print("main start");
Stream.fromFutures([
// 1秒後返回結果
Future.delayed(new Duration(seconds: 1), () {
return "hello 1";
}),
// 丟擲一個異常
Future.delayed(new Duration(seconds: 2),(){
throw AssertionError("Error");
}),
// 3秒後返回結果
Future.delayed(new Duration(seconds: 3), () {
return "hello 3";
})
]).listen((data){
print(data);
}, onError: (e){
print(e.message);
},onDone: (){
});
print("main end");
}
複製程式碼
結果:
main start
main end
hello 1
Error
hello 3
複製程式碼
5.1 StreamBuilder 的使用
我們知道,在Dart中Stream 也是用於接收非同步事件資料,和Future 不同的是,它可以接收多個非同步操作的結果,它常用於會多次讀取資料的非同步任務場景,如網路內容下載、檔案讀寫等。StreamBuilder正是用於配合Stream來展示流上事件(資料)變化的UI元件。 下面看一下StreamBuilder的預設建構函式:
StreamBuilder({
Key key,
this.initialData,
Stream<T> stream,
@required this.builder,
})
複製程式碼
可以看到和FutureBuilder的建構函式只有一點不同:前者需要一個future,而後者需要一個stream。
示例:
Stream<int> counter() {
return Stream.periodic(Duration(seconds: 1), (i) {
return i;
});
}
複製程式碼
Widget buildStream (BuildContext context) {
return StreamBuilder<int>(
stream: counter(), //
//initialData: ,// a Stream<int> or null
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('沒有Stream');
case ConnectionState.waiting:
return Text('等待資料...');
case ConnectionState.active:
return Text('active: ${snapshot.data}');
case ConnectionState.done:
return Text('Stream已關閉');
}
return null; // unreachable
},
);
}
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body:Center(
child: buildStream(context),
)
// This trailing comma makes auto-formatting nicer for build methods.
);
複製程式碼
效果:
六、isolate
Dart是基於單執行緒模型的語言。但是在開發當中我們經常會進行耗時操作比如網路請求,這種耗時操作會堵塞我們的程式碼,所以在Dart也有併發機制,名叫isolate。 APP的啟動入口main函式就是一個類似Android主執行緒的一個主isolate。和Java的Thread不同的是,Dart中的isolate無法共享記憶體,類似於Android中的多程式。
6.1 使用 spawnUri 建立 isolate
spawnUri方法有三個必須的引數,第一個是Uri,指定一個新Isolate程式碼檔案的路徑,第二個是引數列表,型別是List,第三個是動態訊息。 需要注意,用於執行新Isolate的程式碼檔案中,必須包含一個main函式,它是新Isolate的入口方法,該main函式中的args引數列表, 正對應spawnUri中的第二個引數。如不需要向新Isolate中傳引數,該引數可傳空List 主Isolate中的程式碼:
import 'dart:isolate';
void main() {
print("main isolate start");
create_isolate();
print("main isolate stop");
}
// 建立一個新的 isolate
create_isolate() async{
ReceivePort rp = new ReceivePort();
///建立傳送端
SendPort port1 = rp.sendPort;
///建立新的 isolate ,並傳遞了傳送埠。
Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1);
SendPort port2;
rp.listen((message){
print("main isolate message: $message");
if (message[0] == 0){
port2 = message[1];
}else{
port2?.send([1,"這條資訊是 main isolate 傳送的"]);
}
});
// 可以在適當的時候,呼叫以下方法殺死建立的 isolate
// newIsolate.kill(priority: Isolate.immediate);
}
複製程式碼
在主 isolate 檔案的同級路徑下,新建一個 other_task.dart ,內容如下:
import 'dart:isolate';
import 'dart:io';
///接收到了傳送埠。
void main(args, SendPort port1) {
print("isolate_1 start");
print("isolate_1 args: $args");
ReceivePort receivePort = new ReceivePort();
SendPort port2 = receivePort.sendPort;
receivePort.listen((message){
print("isolate_1 message: $message");
});
// 將當前 isolate 中建立的SendPort傳送到主 isolate中用於通訊
port1.send([0, port2]);
// 模擬耗時5秒
sleep(Duration(seconds:5));
port1.send([1, "isolate_1 任務完成"]);
print("isolate_1 stop");
}
複製程式碼
執行主 isolate,輸出結果:
main isolate start
main isolate stop
isolate_1 start
isolate_1 args: [hello, isolate, this is args]
main isolate message: [0, SendPort]
isolate_1 stop
main isolate message: [1, isolate_1 任務完成]
isolate_1 message: [1, 這條資訊是 main isolate 傳送的]
複製程式碼
isolate 之間的通訊時通過 ReceivePort 來完成的,ReceivePort 可以理解為一根水管,訊息的傳遞方向時固定的,把這個水管的傳送端傳送給對方, 對方就能通過這個水管傳送訊息。
6.2 通過 spawn 建立 isolate
除了使用spawnUri,更常用的是使用spawn方法來建立新的Isolate,我們通常希望將新建立的Isolate程式碼和main Isolate程式碼寫在同一個檔案, 且不希望出現兩個main函式,而是將指定的耗時函式執行在新的Isolate,這樣做有利於程式碼的組織和程式碼的複用。spawn方法有兩個必須的引數, 第一個是需要執行在新Isolate的耗時函式,第二個是動態訊息,該引數通常用於傳送主Isolate的SendPort物件。
示例:
import 'dart:isolate';
import 'dart:io';
void main() {
print("主程式開始");
create_isolate();
print("主程式結束");
}
// 建立一個新的 isolate
create_isolate() async{
ReceivePort rp = new ReceivePort();
SendPort port1 = rp.sendPort;
Isolate newIsolate = await Isolate.spawn(doWork, port1);
SendPort port2;
rp.listen((message){
print("main isolate message: $message");
if (message[0] == 0){
port2 = message[1];
}else{
port2?.send([1,"這條資訊是 main isolate 傳送的"]);
}
});
}
// 處理耗時任務
void doWork(SendPort port1){
print("new isolate start");
ReceivePort rp2 = new ReceivePort();
SendPort port2 = rp2.sendPort;
rp2.listen((message){
print("doWork message: $message");
});
// 將新isolate中建立的SendPort傳送到主isolate中用於通訊
port1.send([0, port2]);
// 模擬耗時5秒
sleep(Duration(seconds:5));
port1.send([1, "doWork 任務完成"]);
print("new isolate end");
}
複製程式碼
結果:
主程式開始
主程式結束
new isolate start
main isolate message: [0, SendPort]
new isolate end
main isolate message: [1, doWork 任務完成]
doWork message: [1, 這條資訊是 main isolate 傳送的]
複製程式碼
參考:
juejin.im/post/5cdbf2… juejin.im/post/5c876e… book.flutterchina.club/chapter2/th… book.flutterchina.club/chapter7/fu… book.flutterchina.club/chapter1/da…
最後
歡迎關注「Flutter 程式設計開發」微信公眾號 。