本章是對Dart 基礎講解的最後一章:我們講解餘下來的非同步、泛型、異常,相對來說餘下來的這三章稍微有點難度,但是小夥伴只要用心跟著我一起學習,應該問題不,如果有問題也可以在下方留言或者私信我。好廢話不多說直接進入如主題:
Dart目錄
一、前言:
對於已經叱吒開發武林已久的開發人員來講,非同步是一個很深的知識點。而我們在接觸的每種語言裡面基本上都會提到非同步,同樣地在使用Dart開發專案過程中,也是會有非同步操作的。
我們在Java , oc中可以使用執行緒來實現非同步操作,但是Dart是單執行緒模型,想要實現非同步操作的話,我們可以使用事件佇列來處理。
重點:Dart是單執行緒模型語言,也就沒有了所謂的主執行緒/子執行緒之分。所以相對來說Dart執行緒理解起來和學習起來是相對容易的。
再這裡首我們要想理解Dart單執行緒模型,首先我們理解什麼是Even-Looper,Event-Queue
下面我們先來看一張圖
從上圖我們可以看出:Event Loop就是從EventQueue中獲取Event、處理Event一直到EventQueue為空為止,而EventQueue小夥伴們可以看作成一個管道,Events 就是管道中每一個事件。可能說事件大家還是有點模糊那我們直接說是:使用者的輸入、檔案IO、網路請求、按鈕的點選這些都可以說成Event。
下面我們來說說單執行緒模型:
重點:只要Dart函式開始執行,它將會執行到這個函式結束,也就是說Dart的函式不會被其他Dart程式碼打斷。
所以在Dart中引入一個關鍵詞名叫:isolate 那什麼叫:isolate 首先從字面上來看是”隔離“每個isolate都是隔離的,並不會共享記憶體。isolate是通過在通道上傳遞訊息來通訊,這就標識了Dart從效能上大大所有提升。不像其它語言(包括Java、Kotlin、Objective-C和Swift)都使用搶佔式來切換執行緒。每個執行緒都被分配一個時間分片來執行,如果超過了分配的時間,執行緒將被上下文切換搶佔。但是如果線上程間共享的資源(如記憶體)正在更新時發生搶佔,則會導致競態條件。 Flutter 對Dart情有獨鍾的那些事兒
import 'dart:core';
import 'dart:isolate';
int i;
void main() {
i = 10;
//建立一個訊息接收器
ReceivePort receivePort = new ReceivePort();
//建立isolate
Isolate.spawn(isolateMain, receivePort.sendPort);
//接收其他isolate發過來的訊息
receivePort.listen((message) {
//發過來sendPort,則主isolate也可以向建立的isolate傳送訊息
if (message is SendPort) {
message.send("好呀好呀!");
} else {
print("接到子isolate訊息:" + message);
}
});
}
/// 新isolate的入口函式
void isolateMain(SendPort sendPort) {
// isolate是記憶體隔離的,i的值是在主isolate定義的所以這裡獲得null
print(i);
ReceivePort receivePort = new ReceivePort();
sendPort.send(receivePort.sendPort);
// 向主isolate傳送訊息
sendPort.send("去大保健嗎?");
receivePort.listen((message) {
print("接到主isolate訊息:" + message);
});
}
複製程式碼
可以看到程式碼中,我們接收訊息使用了listene函式來監聽訊息。假設我們現在在main方法最後加入sleep休眠,會不會影響listene回撥的時機?
import 'dart:io';
import 'dart:isolate';
int i;
void main() {
i = 10;
//建立一個訊息接收器
ReceivePort receivePort = new ReceivePort();
//建立isolate
Isolate.spawn(isolateMain, receivePort.sendPort);
//接收其他isolate發過來的訊息
receivePort.listen((message) {
//發過來sendPort,則主isolate也可以向建立的isolate傳送訊息
if (message is SendPort) {
message.send("好呀好呀!");
} else {
print("接到子isolate訊息:" + message);
}
});
//增加休眠,是否會影響listen的時機?
sleep(Duration(seconds: 2));
print("休眠完成");
}
/// 新isolate的入口函式
void isolateMain(SendPort sendPort) {
// isolate是記憶體隔離的,i的值是在主isolate定義的所以這裡獲得null
print(i);
ReceivePort receivePort = new ReceivePort();
sendPort.send(receivePort.sendPort);
// 向主isolate傳送訊息
sendPort.send("去大保健嗎?");
receivePort.listen((message) {
print("接到主isolate訊息:" + message);
});
}
複製程式碼
結果是會有延遲等待,然後我們的listene才列印出其他isolate發過來的訊息。到這個地方可能開發過Android 小夥伴會覺得如同Android Handler一樣。
在Dart執行環境中也是靠事件驅動的,通過event loop不停的從佇列中獲取訊息或者事件來驅動整個應用的執行,isolate發過來的訊息就是通過loop處理。但是不同的是在Android中每個執行緒只有一個Looper所對應的MessageQueue,而Dart中有兩個佇列,一個叫做event queue(事件佇列),另一個叫做microtask queue(微任務佇列)。
這裡有個疑問:其實Dart中的Main Isolate只有一個Event Looper。但是Dart中為啥存在兩個列隊。
那microtask queue 存在的意義是啥:
其實這個裡面有巧妙的設計:microtask queue 存在是希望通過這個Queue來處理稍晚一些的事情,但是在下一個訊息到來之前需要處理完的事情。當Event Looper正在處理Microtask Queue中的Event時候,Event Queue中的Event就停止了處理了,此時App不能繪製任何圖形,不能處理任何滑鼠點選,不能處理檔案IO等等
二、什麼是Future
在 Flutter 中有兩種處理非同步操作的方式Future和Stream,Future用於處理單個非同步操作,Stream用來處理連續的非同步操作。啥意思呢?就好比:楊過練武功祕籍:練了打狗棒,並將打狗棒通過一段時間練會了這就是一個Future。
下面我們先看看單個非同步處理Future,其實在 Dart 庫中隨處可見 Future 物件:如下圖:
通常操作一個非同步函式,並對其設定返回的物件,而這個物件就是一個 Future。 當一個 future 執行完後,他裡面的值 就可以使用了,可以使用 then() 來在 future 完成的時候執行其他程式碼。Future物件其實就代表了在事件佇列中的一個事件的結果。
///讀取檔案
void readStringFromFile() {
File("/Users/Test/1.txt").readAsString().then((content) {
//任務執行完成會進入這裡,能夠獲得返回的執行結果。
print(content);
}).whenComplete(() {
//當任務停止時,最後會執行這裡。
print("楊過是武林高手");
}).catchError((e, s) {
//如果檔案地址時會發生異常,這時候可以利用catchError捕獲此異常。
print(s);
});
}
複製程式碼
void moreTaskZips() {
//可以等待多個非同步任務執行完成後,再呼叫 then()。
//只有有一個執行失敗,就會進入 catchError()。
Future.wait([
// Future.delayed() 延遲執行一個延時任務。
// 2秒後返回結果
Future.delayed(new Duration(seconds: 2), () {
return "楊過";
}),
// 4秒後返回結果
Future.delayed(new Duration(seconds: 4), () {
return "我喜歡小龍女";
})
]).then((v) {
//執行成功會走到這裡
print(v[0] + v[1]);
}).catchError((v) {
//執行失敗會走到這裡
print("我是尹志平");
}).whenComplete(() {
//無論成功或失敗都會走到這裡
print("我就要和我的過兒回古墓");
});
}
複製程式碼
三、Stream詳解
Stream是一個抽象類,用於表示一序列非同步資料的源。它是一種產生連續事件的方式,可以生成資料事件或者錯誤事件,以及流結束時的完成事件。
Stream 的好處是處理過程中記憶體佔用較小。舉個例子:在讀取file檔案資料的時候, Future 只能一次獲取非同步資料。而 Stream 能多次非同步獲得的資料。如果當檔案比較大,明顯Futrue佔用的時間更久,這樣子就會導記憶體佔用過大。
void readFile(){
// 說明:這裡的listen 其實就是一個訂閱了Stream 我們通過檢視原始碼發現會返回一個 StreamSubscription 訂閱者
File("/Users/Test/app-release.apk").openRead().listen((List<int> bytes) {
print("Stream我被執行"); //執行多次
});
//通過查詢原始碼:readAsBytes 返回的是一個Future
File("/Users/Test/app-release.apk").readAsBytes().then((_){
print("future我被執行"); //執行1次
});
}
複製程式碼
Stream 可通過listen進行資料監聽(listen其實就是訂閱當前Stream,會返回一個StreamSubscription訂閱者,訂閱者提供了取消訂閱的cancel(),去掉後我們的listen中就接不到任何資訊了。除了cancel()取消方法之外還有pause()暫停),通過error接收失敗狀態,通過done來接收結束狀態;
怎麼建立Stream和操作Stream流資料?
Dart中提供了多種建立Stream方法:
void main() {
// 第一種:建立方法Stream.fromFuture(Future<T> future)
_createStreamFromFuture();
}
_createStreamFromFuture() {
Future<String> getTimeOne() async {
await Future.delayed(Duration(seconds: 3));
return '當前時間為:${DateTime.now()}';
}
Stream.fromFuture(getTimeOne())
.listen((event) => print('測試通過Stream.fromFuture建立Stream -> $event'))
.onDone(() => print('測試通過Stream.fromFuture建立Stream -> done 結束'));
//輸出結果
//測試通過Stream.fromFuture建立Stream -> 當前時間為:2020-07-20 12:01:40.280591
//測試通過Stream.fromFuture建立Stream -> done 結束
}
複製程式碼
void main() {
// 第二種建立方法: Stream.fromFutures(Iterable<Future<T>> futures)
_createStreamFromFuture();
}
_createStreamFromFuture() {
Future<String> getTimeOne() async {
await Future.delayed(Duration(seconds: 3));
return '當前時間為:${DateTime.now()}';
}
Future<String> getTimeTwo() async {
await Future.delayed(Duration(seconds: 3));
return '當前時間為:${DateTime.now()}';
}
Stream.fromFutures([getTimeOne(),getTimeTwo()])
.listen((event) => print('測試通過Stream.fromFutures建立Stream -> $event'))
.onDone(() => print('測試通過Stream.fromFutures建立Stream -> done 結束'));
//輸出結果
//測試通過Stream.fromFuture建立Stream -> 當前時間為:2020-07-20 12:01:40.280591
//測試通過Stream.fromFuture建立Stream -> done 結束
}
複製程式碼
Stream提供的:fromFutures裡面可以塞多個Future, 通過一系列的 Future 建立新的單訂閱流,每個 Future 都會有自身的 data / error 事件,當這一系列的 Future 均完成時,Stream 以 done 事件結束;若 Futures 為空,實則是沒有意義的。則 Stream 會立刻關閉。
void main() {
// 第三種建立方法: Stream.fromIterable(Iterable<T> elements)
_createStreamFromFuture();
}
_createStreamFromFuture() {
var data = ['黃藥師', '郭靖', '楊過', false];
Stream.fromIterable(data)
.listen((event) => print('測試通過Stream.fromFuture建立Stream -> $event'))
.onDone(() => print('測試通過Stream.fromFuture建立Stream -> done 結束'));
// 測試通過Stream.fromFuture建立Stream -> 黃藥師
// 測試通過Stream.fromFuture建立Stream -> 郭靖
// 測試通過Stream.fromFuture建立Stream -> 楊過
// 測試通過Stream.fromFuture建立Stream -> false
// 測試通過Stream.fromFuture建立Stream -> done 結束
}
複製程式碼
stream 廣播模式:
Stream有兩種訂閱模式:單訂閱和多訂閱。單訂閱就是隻能有一個訂閱者,上面的使用我們都是單訂閱模式,而廣播是可以有多個訂閱者。通過 Stream.asBroadcastStream() 可以將一個單訂閱模式的 Stream 轉換成一個多訂閱模式的 Stream,isBroadcast 屬性可以判斷當前 Stream 所處的模式
import 'dart:async';
import 'dart:io';
void main() {
_createStreamBroadcast();
}
_createStreamBroadcast() {
var stream = new File("/Users/Test/app-release.apk").openRead();
stream.listen((event) => print(' $event'));
//由於是單訂閱,所以這個地方只能有一個,所以下面這種寫法是錯誤
//stream.listen((event) => print(' 我再新增一個訂閱$event'));
//一個單訂閱模式的 Stream 轉換成一個多訂閱模式的 Stream可以使用Stream.asBroadcastStream
var broadcastStream =
new File("/Users/Test/app-release.apk").openRead().asBroadcastStream();
broadcastStream.listen((_) {
print("我是黃藥師1");
});
broadcastStream.listen((_) {
print("我是黃藥師2");
});
}
複製程式碼
這裡我們要注意一下多訂閱模式如果沒有及時新增訂閱者則可能丟資料。
import 'dart:async';
import 'dart:io';
void main() {
_createStreamBroadcast();
}
_createStreamBroadcast() {
//預設是單訂閱
var stream = Stream.fromIterable(["黃藥師", "郭靖", "楊過"]);
//3s後新增訂閱者 不會丟失資料
Timer(Duration(seconds: 3), () => stream.listen(print));
//建立一個流管理器 對一個stream進行管理
var streamController = StreamController.broadcast();
///我再這個地方新增一個流資料
streamController.add("小龍女");
///先發出事件再訂閱 無法接到通知
streamController.stream.listen((i) {
print("broadcast:$i");
});
//記得關閉
streamController.close();
//這裡沒有丟失,因為stream通過asBroadcastStream轉為了多訂閱,但是本質是單訂閱流,並不改變原始 stream 的實現特性
var broadcastStream =
Stream.fromIterable(["黃藥師-1", "郭靖-2", "楊過-3"]).asBroadcastStream();
Timer(Duration(seconds: 3), () => broadcastStream.listen(print));
}
複製程式碼
四、async/await
使用async和await的程式碼是非同步的,但是看起來很像同步程式碼。有了這兩個關鍵字,我們可以更簡潔的編寫非同步程式碼,而不需要呼叫Future相關的API
import 'dart:async';
import 'dart:io';
void main() {
_readData().then((v){
print("你的名字$v");//輸出:你的名字[黃藥師, 郭靖, 楊過]
});
}
List<String> _testAsyncAndAwait() {
return ["黃藥師", "郭靖", "楊過"];
}
_readData() async {
return _testAsyncAndAwait();
}
複製程式碼
- await關鍵字必須在async函式內部使用,也就是加await不加async會報錯。
- 呼叫async函式必須使用await關鍵字,如果加async不加await會順序執行程式碼如下程式碼:
import 'dart:async';
import 'dart:io';
void main() {
_startMethod();
_methodC();
}
_startMethod() async {
_methodA();
await _methodB();
print("start結束");
}
_methodA() {
print("A開始執行這個方法~");
}
_methodB() async {
print("B開始執行這個方法~");
await print("後面執行這句話~");
print("繼續執行這句哈11111~");
}
_methodC() {
print("我是黃藥師!!!");
}
//A開始執行這個方法~
//B開始執行這個方法~
//後面執行這句話~
//我是黃藥師!!!
//繼續執行這句哈11111~
//start結束
複製程式碼
- 當使用async作為方法名字尾宣告時,說明這個方法的返回值是一個Future;
- 當執行到該方法程式碼用await關鍵字標註時,會暫停該方法其他部分執行;
- 當await關鍵字引用的Future執行完成,下一行程式碼會立即執行。
五、Dart中泛型
Dart是一種可選的型別語言,所以Dart像其他語言一樣也支援泛型,泛型的作用就是解決 類 介面 方法的複用性、以及對不特定資料型別的支援(型別校驗)。更直接的理解是傳入什麼,返回什麼,同時支援型別校驗。
語法如下:
Collection_name <data_type> identifier= new Collection_name<data_type>
複製程式碼
下面我們來聊聊,Dart中泛型方法,泛型類,泛型介面
泛型方法
import 'dart:async';
import 'dart:io';
void main() {
print(_setUser(User().name)); //輸出:黃藥師
}
_setUser<T>(T user) {
return user;
}
class User {
var name = "黃藥師";
}
複製程式碼
泛型類
import 'dart:async';
import 'dart:io';
void main() {
User<String>()
..addName("黃藥師")
..addName("楊過")
..addName("小龍女")
..addName("黃蓉")
..printInfo();
}
class User<T> {
List<T> names = List<T>();
void addName(T name) {
names.add(name);
}
void printInfo() {
names.forEach((v) {
print("我是:$v");
//輸出 我是:黃藥師
//我是:楊過
//我是:小龍女
//我是:黃蓉
});
}
}
複製程式碼
泛型介面
import 'dart:async';
import 'dart:io';
void main() {
Student<String>()
..addName("黃藥師")
..addName("小龍女")
..printInfo();
}
class Student<T> implements User<T> {
List<T> names = List<T>();
@override
void addName(T name) {
names.add(name);
}
@override
void printInfo() {
names.forEach((v) {
print("你是:$v");
//你是:黃藥師
//你是:小龍女
});
}
}
abstract class User<T> {
void addName(T name);
void printInfo();
}
複製程式碼
六、異常
和 Java 不同的是,所有的 Dart 異常是非檢查異常。 方法不一定宣告瞭他們所丟擲的異常, 並且不要求你捕獲任何異常。
Dart 提供了 Exception和Error 型別, 以及一些子型別。你還 可以定義自己的異常型別。但是, Dart 程式碼可以 丟擲任何非 null 物件為異常,不僅僅是實現了 Exception 或者Error 的物件。
throw new Exception('這是一個異常');
throw '這是一個異常';
throw 123;
複製程式碼
與Java不同之處在於捕獲異常部分,Dart中捕獲異常同樣是使用catch語句,但是Dart中的catch無法指定異常型別。需要結合on來使用,基本語法如下:
try {
throw 123;
} on int catch(e){
//使用 on 指定捕獲int型別的異常物件
} catch(e,s){
//函式 catch() 可以帶有一個或者兩個引數,
//第一個引數為丟擲的異常物件,
//第二個為堆疊資訊 ( StackTrace 物件)
rethrow; //使用 `rethrow` 關鍵字可以 把捕獲的異常給 重新丟擲
} finally{
//finally內部的語句,無論是否有異常,都會執行。
print("this is finally");
}
複製程式碼
- on可以捕獲到某一類的異常,但是獲取不到異常物件;
- catch可以捕獲到異常物件。這個兩個關鍵字可以組合使用。
- rethrow可以重新丟擲捕獲的異常。
總結: 歷經三週終於把這三篇文章完成,時間上拉距有點長,文章中針對Dar講解t相對簡單。比如:第一章裡面講解的final、const知識點,和本章非同步講解的都不夠細,後期會出兩篇單獨針對“Dart異常”和final、const知識的講解。
最後通過在寫三篇文章的同時查詢了需要關於Flutter的資料。同時也遇到了比較好的部落格如下:樑飛宇部落格
本人知乎: 如何快速掌握Dart這門語言並進階Flutter變成大神
最後感謝小夥伴認真閱讀《40分鐘快速入門Dart基礎》三篇文章,如果有喜歡可以點贊關注,也可以私信本人,後期我會持續輸出關於Flutter知識和一些開發的技巧。
下一篇:Flutter 學習計劃