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.cn/post/684490… juejin.cn/post/684490… book.flutterchina.club/chapter2/th… book.flutterchina.club/chapter7/fu… book.flutterchina.club/chapter1/da…
最后
欢迎关注「Flutter 编程开发」微信公众号 。