40分钟快速入门Dart基础(下)
本章是对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)都使用抢占式来切换线程。每个线程都被分配一个时间分片来执行,如果超过了分配的时间,线程将被上下文切换抢占。但是如果在线程间共享的资源(如内存)正在更新时发生抢占,则会导致竞态条件。
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的资料。同时也遇到了比较好的博客如下:梁飞宇博客
最后感谢小伙伴认真阅读《40分钟快速入门Dart基础》三篇文章,如果有喜欢可以点赞关注,也可以私信本人,后期我会持续输出关于Flutter知识和一些开发的技巧。