Flutter 中的異步編程總結html
Dart 是一種單線程模型運行語言,其運行原理以下圖所示:編程
Dart 在單線程中是以消息循環機制來運行的,其中包含兩個任務隊列,一個是「微任務隊列」 microtask queue,另外一個叫作「事件隊列」 event queue。 從圖中能夠發現,微任務隊列的執行優先級高於事件隊列。bash
如今咱們來介紹一下Dart線程運行過程,如上圖中所示,入口函數 main() 執行完後,消息循環機制便啓動了。 首先會按照先進先出的順序逐個執行微任務隊列中的任務,當全部微任務隊列執行完後便開始執行事件隊列中的任務,事件任務執行完畢後再去執行微任務, 如此循環往復,生生不息。微信
在Dart中,全部的外部事件任務都在事件隊列中,如IO、計時器、點擊、以及繪製事件等,而微任務一般來源於Dart內部,而且微任務很是少, 之因此如此,是由於微任務隊列優先級高,若是微任務太多,執行時間總和就越久,事件隊列任務的延遲也就越久, 對於GUI應用來講最直觀的表現就是比較卡,因此必須得保證微任務隊列不會太長。網絡
在事件循環中,當某個任務發生異常並無被捕獲時,程序並不會退出,而直接致使的結果是當前任務的後續代碼就不會被執行了, 也就是說一個任務中的異常是不會影響其它任務執行的。併發
咱們能夠看出,將任務加入到MicroTask中能夠被儘快執行,但也須要注意,當事件循環在處理MicroTask隊列時,Event隊列會被卡住,應用程序沒法處理鼠標單擊、I/O消息等等事件。 同時,當事件循壞出現異常時,dart 中也能夠經過 try/catch/finally 來捕獲異常。app
基於上述理論,看一下任務調度less
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
複製代碼
import 'dart:async';
void myTask(){
print("this is my event task");
}
void main() {
new Future(myTask);
}
複製代碼
控制檯輸出:async
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隊列以前。
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 後獲得了執行,可是另外一個延時任務,是在當前延時的基礎上又延時執行的, 也就是,延時任務必須等前面的耗時任務執行完,才獲得執行,這將致使延時任務延時並不許。
Dart類庫有很是多的返回Future或者Stream對象的函數。 這些函數被稱爲異步函數:它們只會在設置好一些耗時操做以後返回,好比像 IO操做。而不是等到這個操做完成。
Future與JavaScript中的Promise很是類似,表示一個異步操做的最終完成(或失敗)及其結果值的表示。簡單來講,它就是用於處理異步操做的,異步處理成功了就執行成功的操做,異步處理失敗了就捕獲錯誤或者中止後續操做。一個Future只會對應一個結果,要麼成功,要麼失敗。
因爲自己功能較多,這裏咱們只介紹其經常使用的API及特性。還有,請記住,Future 的全部API的返回值仍然是一個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
複製代碼
當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
複製代碼
不少時候咱們會依賴一些異步數據來動態更新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.
);
}
}
複製代碼
效果:
在Dart1.9中加入了async和await關鍵字,有了這兩個關鍵字,咱們能夠更簡潔的編寫異步代碼,而不須要調用Future相關的API 將 async 關鍵字做爲方法聲明的後綴時,具備以下意義
// 導入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 也是用於接收異步事件數據,和Future 不一樣的是,它能夠接收多個異步操做的結果(成功或失敗)。 也就是說,在執行異步任務時,能夠經過屢次觸發成功或失敗事件來傳遞結果數據或錯誤異常。 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
複製代碼
咱們知道,在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.
);
複製代碼
效果:
Dart是基於單線程模型的語言。可是在開發當中咱們常常會進行耗時操做好比網絡請求,這種耗時操做會堵塞咱們的代碼,因此在Dart也有併發機制,名叫isolate。 APP的啓動入口main函數就是一個相似Android主線程的一個主isolate。和Java的Thread不一樣的是,Dart中的isolate沒法共享內存,相似於Android中的多進程。
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 能夠理解爲一根水管,消息的傳遞方向時固定的,把這個水管的發送端發送給對方, 對方就能經過這個水管發送消息。
除了使用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 編程開發」微信公衆號 。