1. 异步编程
1.1 Future
Future 类,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则类型为 Future。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。 在网络请求中经常使用异步编程,那么来探索一下Flutter 中的异步编程。
dart是单线程的,所以底层没有锁之类的东西,但是这不代表着dart不能异步,异步不代表多线程。 下面代码运行后发现做其他事情被堵塞住了,这里async不起作用是因为需要搭配Future使用。 将耗时操作使用Future包装起来,这里可以看到做其他事情就不会被堵塞住了,那么现在即使方法不加async也是异步的,因为Future里面已经是异步的了。或者说async不会异步执行,Future才会异步执行。 如果把 print(‘结束data = $_data’); 放在Future外面那么其就会先于Future里面的代码执行。
1.2 async ,await
那么如何让等待Future里面的执行完在执行后面代码呢?这时候需要用到await。加了await之后,后面的代码就会等待Future里面的执行完之后在执行。这里的await需要搭配async使用,而没有await的情况下,async是没有意义的。await后面的操作也需要是异步的。那么也就是说,Future是用来异步执行的,而async和await搭配是用来让Future里面的某块代码同步执行的。
1.3 Future.then()
Future的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。 这里可以使用then来将特定的需要等待的任务放进去,然后不堵塞后面的任务的执行。 如果then没有返回值的话,那么value就是null。 这里看到Future里面return的话那么value就是return的那个值。这个时候,Future里面返回的数据会被Future包装,然后给到了then里面 的value。
1.4 Future.catchError
异步中的错误是用catchError来进行处理的。当异步任务中出现异常之后, 如果调用下列方法就会报错,这是因为任务里面抛出了异常没有处理。
getData() async {
print('开始data = $_data');
//耗时操作
Future future = Future(() {
for (int i = 0; i < 10000000; i++) {}
throw Exception('网络异常');
return "假的网络数据";
});
future.then((value) {
print('value = $value');
});
print('做一点其他事情');
}
在.then 下面添加代码
future.catchError((e){print("捕获到了错误:$e");});
运行后发现还是报错,但是捕获到了异常。正常来说如果处理了异常,就不应该报错了,那么这里是执行顺序的问题吗? 将catch和then交换位置后运行,发现还是报错 把then注释掉后发现不报错了。 那么这里要怎么处理呢?这里一般用链式调用。
getData() async {
print('开始data = $_data');
//耗时操作
Future future = Future(() {
for (int i = 0; i < 10000000; i++) {}
throw Exception('网络异常');
return "假的网络数据";
}).then((value) {
print('value = $value');
}).catchError((e) {
print("捕获到了错误:$e");
});
print('做一点其他事情');
}
或者在.then中添加onError对错误进行处理
future.then((value) {
print('value = $value');
},onError: (error){print("捕获到了错误:$error");});
onError是在.then这一次对错误进行处理,而catchError则是多次链式调用中的错误处理 如果在.then之前catchError了,那么.then中的value就是catchError中传过来的值了。在catchError之前的.then不会执行。 在CatchError之后添加了whenComplete运行后发现还是报出了异常。
getData() async {
print('开始data = $_data');
//耗时操作
Future future = Future(() {
for (int i = 0; i < 10000000; i++) {}
throw Exception('网络异常');
return "假的网络数据";
});
future.catchError((e) {
print('error = $e');
return "错误";
}).then((value) => print('$value'));
future.whenComplete(() => print("完成了"));
print('做一点其他事情');
}
这个时候可以在whenComplete之后添加一个catchError,或者使用链式调用。
future.catchError((e) {
print('error = $e');
return "错误";
}).then((value) => print('$value')).whenComplete(() => print("完成了"));
上面的例子可以看到链式调用能避免大部分的错误,所以在一般都是使用链式调用来进行处理的,并且把catchError放到最后调用,这样出现异常的时候,前面的.then就不会执行了。如果链式太长的话,可以创建方法来让链式更加简洁。
getData() async {
print('开始data = $_data');
//耗时操作
Future future = Future(() {
for (int i = 0; i < 10000000; i++) {}
throw Exception('网络异常');
return "假的网络数据";
}).then(thenFunc).whenComplete(completeFunc).catchError((e) {
print('error = $e');
return "错误";
});
print('做一点其他事情');
}
FutureOr thenFunc(String value) {
}
FutureOr<void> completeFunc() {
}
1.5 多个Future
Future是放在队列中的,所以Future的执行是有顺序的。下面的代码结果输出是什么呢?
void main() {
testFuture();
print("A");
}
void testFuture() {
Future((){
sleep(Duration(seconds: 2));
print("C");
});
print("B");
}
运行后看到是B->A->C; 那么下面的代码运行后会打印什么呢?
void main() {
testFuture();
print("A");
}
void testFuture() async {
await Future((){
sleep(Duration(seconds: 2));
print("C");
}).then((value) => print("D"));
print("B");
}
运行后发现是A->C->D->B,这里因为B被C堵塞了,所以A会先执行,然后C执行完之后执行D,最后执行B。 那么下面代码的输出结果是什么呢?
void main() {
testFuture();
print("A");
}
void testFuture() async {
Future((){
return "任务1";
}).then((value) => print("$value结束"));
Future((){
return "任务2";
}).then((value) => print("$value结束"));
Future((){
return "任务3";
}).then((value) => print("$value结束"));
Future((){
return "任务4";
}).then((value) => print("$value结束"));
print("任务添加完毕");
}
运行后发现是任务添加完毕->A->任务1结束->任务2结束->任务3结束->任务4结束。
那么这里任务一定是按顺序执行的吗?在任务二添加sleep后重新执行,发现任务顺序还是一样的。 这说明这里会按异步任务的添加顺序执行的。
1.6 Future.wait
当需要等待多个Future完成,并收集它们的结果,可以使用Future.wait。这个时候.then就会等wait里面所有任务完成后在执行,然后返回的值可以用数组来取。wait里面的任务同时处理,但是是按添加顺序执行的,而如果是链式执行的话,则是一个执行完在执行下一个。
Future.wait([
Future((){
return "任务1";
}),
Future((){
return "任务2";
}),
Future((){
return "任务3";
}),
Future((){
return "任务4";
}),
]).then((value) => print(value[0] + value[1] + value[2] ));
1.7 microtask
Dart的事件循环(event loop)
在Dart中,实际上有两种队列:
- 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
- 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。
因为microtask queue 的优先级高于 event queue ,所以如果 microtask queue有太多的微任务, 那么就可能会霸占住当前的event loop。从而对event queue中的触摸、绘制等外部事件造成阻塞卡顿。
在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。
异步任务我们用的最多的还是优先级更低的 event queue。Dart为 event queue 的任务建立提供了一层封装,就是我们在Dart中经常用到的Future。
正常情况下,一个 Future 异步任务的执行是相对简单的:
声明一个 Future 时,Dart 会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行。 当同步执行的代码执行完毕后,event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的操作。 上面说过了microtask队列的优先级比较高,那么使用microtask就可以让任务优先执行。 下面代码的先执行代码1,代码2,A以及B。
void testFuture3() {
print('外部代码1');
Future(()=>print('A')).then((value) => print('A结束'));
Future(()=>print('B')).then((value) => print('B结束'));
print('外部代码2');
}
添加了微代码后,那么微任务就会在异步任务之前执行。
void testFuture3() {
print('外部代码1');
Future(()=>print('A')).then((value) => print('A结束'));
Future(()=>print('B')).then((value) => print('B结束'));
scheduleMicrotask(() {
print("微任务A");
});
print('外部代码2');
}
下面的任务执行后打印情况是什么样的呢?这里5一定是先执行的,然后执行微任务3,然后异步任务按照添加的顺序执行,那么就会先执行future1打印1和4,最后执行future2 打印2,所以打印 5 —— 3 —— 1 —— 4 —— 2;
void testFuture4() {
Future future1 = Future((){print('1');});
Future future2 = Future((){print('2');});
scheduleMicrotask(() {
print("微任务3");
});
future1.then((value) => print('4'));
print('5');
}
运行后验证果真是的。 那么这里的打印顺序是什么呢?这里future3最先被添加到队列,所以依然会比 1, 2 优先执行,所以会先打印6,所以打印 5 —— 3 —— 6 —— 1 —— 4 —— 2; 打印结果: 那么如果是这样的话,打印结果会是什么呢?按照图片里的,执行future3的时候会把自身任务执行完再重新开始循环,那么也就是说,这里5,3之后会先打印6,8 ,然后再打印7,然后再打印142。
void testFuture4() {
Future future3 = Future(() => null);
future3.then((value) {
print('6');
scheduleMicrotask(() {
print("7");
});
}).then((value) => print("8"));
Future future1 = Future(() {
print('1');
});
future1.then((value) => print('4'));
Future future2 = Future(() {
print('2');
});
scheduleMicrotask(() {
print("3");
});
print('5');
}
打印结果: 那么把.then拆出来的话是什么结果呢?其实这里相当于把.then里面的任务放到微任务里面去了,所以8依然会优先执行。这也是为什么 future1里面的then的任务会比 future2先执行。
Future future4 = future3.then((value) {
print('6');
scheduleMicrotask(() {
print("7");
});
});
future4.then((value) => print("8"));
打印结果:
2. 多线程
Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main isolate。
下面的打印结果一定是123,因为这里Future会等待主线程睡眠结束。
void IsolateDemo() {
print('1');
Future(func);
sleep(Duration(seconds: 2));
print('2');
}
FutureOr func() {
print('3');
}
那么如果将func放在Isolate里面执行,那么在打印2之前func就会先打印3。
void IsolateDemo() {
print('1');
Isolate.spawn(func,10);
sleep(Duration(seconds: 1));
print('2');
}
void func(int message) {
print('3');
}
多添加几次任务来执行。
void IsolateDemo() {
print('外部代码1');
Isolate.spawn(func,10);
Isolate.spawn(func2,10);
Isolate.spawn(func,10);
Isolate.spawn(func2,10);
Isolate.spawn(func,10);
Isolate.spawn(func2,10);
sleep(Duration(seconds: 1));
print('外部代码2');
}
运行后发现这里任务一不一定比任务2先执行,而且外部代码也会在任务一任务二之前执行,证明了这里Isolate确实是子线程里面执行任务的。 Dart中的isolate更像是一个进程,因为isolate有独立的内存空间,意味着每个isolate中的数据是独立的,所以没有资源抢夺的问题,也就不需要锁。所以,我们访问数据不能直接访问。 声明一个属性没然后在func里面赋值,在IsolateDemo里面睡眠之后打印a的值看一下是否改变。
int a = 10;
void IsolateDemo() {
print('外部代码1');
Isolate.spawn(func,1000);
sleep(Duration(seconds: 1));
print('外部代码来了a=$a');
print('外部代码2');
}
void func(int message) {
a = message;
print('第一个来了a=$a');
}
运行后发现是没有改变的 那么如果非要改变a的值的话,那么就需要用到port,将port的sendPort作为参数传给func,然后添加listen监听数据变化来接受func里面传过来的值。
void IsolateDemo() {
print('外部代码1');
// 创建一个port
ReceivePort port = ReceivePort();
// 创建一个Isolate
Isolate.spawn(func,port.sendPort);
//监听数据变化
port.listen((message) {
a = message;
print('接受到了a=$a');
});
sleep(Duration(seconds: 1));
print('外部代码来了a=$a');
print('外部代码2');
}
void func(SendPort send) {
send.send(100);
print('第一个来了a=$a');
}
运行后发现a的值改变了。 既然我们Isolate开辟了空间,那么我们就要手动去销毁Isolate。 这里创建临时变量iso,然后在port.listen里面关闭端口以及杀掉iso。这里的await不会堵塞后面代码的执行,因为这里是其他线程里面的。
void IsolateDemo() async {
print('外部代码1');
// 创建一个port
ReceivePort port = ReceivePort();
// 创建一个Isolate
Isolate iso = await Isolate.spawn(func,port.sendPort);
//监听数据变化
port.listen((message) {
a = message;
print('接受到了a=$a');
port.close();
iso.kill();
});
print('外部代码来了a=$a');
print('外部代码2');
}
关于Iso还有一个封装叫compute,这里使用compute之后发现这里并不会堵塞住compute的执行。
void computeTest() {
print('外部代码1');
compute(func1,10);
sleep(Duration(seconds: 2));
print('外部代码2');
}
FutureOr func1(message) {
print('compute');
}
运行结果: compute和iso不一样的是,如果加了await,那么后面的代码就需要等待。并且compute可以返回数据来修改数据的值,但是如果在func1修改a的值的话同样是没有效果的。
void computeTest() async{
print('外部代码1');
a = await compute(func1,10);
print('外部代码2 a = $a');
}
int func1(message) {
sleep(Duration(seconds: 2));
print('compute');
return 1000;
}
|