gavin's home.

Dart之异步

Word count: 2.2kReading time: 8 min
2019/12/09 Share

在iOS开发过程中,我们经常会需要操作一些耗时操作,比如网络请求,从本地数据库读取数据等。而我们经常用到的方法就是开启一个子线程,将这些操作放到这个子线程里面操作,而不影响主线程的性能,当这些操作有返回的时候再回到主线程进行页面的刷新等。但是在Dart开发中,是没有多线程这个概念的,只有单线程,那么在处理一些耗时较长的操作的时候,Dart是如何处理的呢?

Dart中的单线程

isolate

在Dart开发中,并没有像其他语言中的多线程的概念,而是多了一个类似的isolate概念。
它有独立内存空间,也有一条时刻都在处理数据的事件处理循环(event loop)。我们可以把它看成有独立空间的线程。如下图所示,绿色的格子空间代表内存。红色箭头代表event loop。

当然了,我们也可以自己创建一个新的isolate,用Isolate.spawn() 或者 Flutter’s compute()的方式,新创建出来的isolate有自己的内存空间和event loop.
而两个isolate之间的信息传递只能通过其他特殊的方式。

event loop

Dart中的事件处理有很多,如触摸处理,网络数据处理,滑动处理等,都需要交给event loop来处理。

上图左侧的不同颜色的多边形代表的就是需要处理的不同类型的事件,右边代表的就是event loop。

Dart中的异步操作

future

说到Dart的异步操作就不能不提到一个名词:future。

那么什么是future呢?
future本质上就是一个对象,但这个对象呢?有两种状态,一种是Uncompleted状态,一种是Completed的状态,而Completed的状态又分为两种,一种是真实的value值,一直就是通用的error。举个很简单的例子:Future< String > 这个类所对应的value值就是字符串。如下图所示:

我来举个例子,假如现在我们点击一个按钮,然后去发起请求一张图片的网络请求,然后再把这张图片显示出来。这是我们平时遇到最多的需要进行异步加载的情形,代码如下:

1
2
3
4
5
6
7
8
9
RaisedButton(
onPressed: () {
final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {
setImage(resp);
});
},
child: Text('Click me!'),
)

接下里我们来拆分这个问题:

  1. 按钮点击。
  2. 发送请求。
  3. 请求返回,处理数据。

我们要完成这个操作是需要三步的,而在我们的event loop中就属于三个不同的event需要处理。我们处理异步这个关键的地方就是我们要说的future对象。在我们发出处理完第二步的时候,event loop会给我们返回一个盒子,盒子里是啥我们不知道,然后再第三步数据返回的时候,event loop会将一张图片放在这个盒子里面,如果你想处理这张图片,那么你就的注册一下then这个block。在这个block里面对返回的图片进行操作。在这个例子当中,这个盒子就是我们上文提到的future。在这个这个value就是那张图片。
如何创建future呢?
目前主要有两种方式可以得到future,一种就是系统的api,比如http网络请求库,我们上面的代码。另一种方式就是我们自己创建,代码如下:

1
2
3
4
5
void main() {
final myFuture = Future(() {
return 12;
});
}

或者

1
2
3
void main() {
final myFuture = Future.value(12);
}

我们除了可以创建有值的future的话还可以模拟创建包含错误的future:

1
2
3
void main() {
final myFuture = Future.error(ArgumentError.notNull('input'));
}

我们在上面第一段代码里面再加一句话来更加详细说明一下异步操作的过程。

1
2
3
4
5
6
7
void main() {
final myFuture = Future(() {
print('Creating the future.'); // 2.
return 12;
});
print('Done with main().'); // 1.
}

以上代码后一句会先打印,而前一句会后打印,为什么呢?就像我们之前说的那样,Dart本身是单线程的,所以需要进行的操作都是一个一个接着放入event loop中进行。Future用构造函数创建的时候,一开始返回的其实就是一个uncompleted future,就好比如说是:“你好,这里有一个封闭的箱子,你先拿着,待会我会放点东西进去,然后你再打开”,就是这么简单。这是第一个需要处理的event,然后接下来进行的是print('Done with main().');这句代码。再然后呢?我们就放了一个为12的数据进去了。有时候网络请求返回的可能是个错误,也就是说我们手里拿着的这个盒子里面的东西不是我们真正想要的值,那我们该怎么办呢?我们可以跟处理正常的value一样,注册一个block来处理,代码如下:

1
2
3
4
5
6
7
8
9
10
11
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!', // 放个error进盒子里.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // 处理这个错误.
});
print('Waiting for a value...');
}

还有一种block,我们可以注册,那就是当future盒子里面有东西了,无论这个东西是个值还是个错误。
如:

1
2
3
4
5
6
7
8
9
10
11
12
void main() {
Future.delayed(
Duration(seconds: 3),
() => throw 'Error!', // 放个error进盒子里.
).then((value) {
print(value);
}).catchError((err) {
print('Caught $err'); // 处理这个错误.
}).whenComplete(){
print('完成了!');
};
}

说到了这里,我们大致弄清楚了Dart异步操作的一些基本原理和简单操作,那么为什么我们还需要awaitasync呢?

await和async

我们通过一个比较简单的例子来阐述下为什么我们需要用到await和async,以及用这两个关键词的好处。
假设我们现在有三个操作,分别是从本地数据库取用户id,然后再用这个用户id去远程服务器上获取对应的用户数据,最后我们再对这个获取到的用户数据进行处理显示在界面上:

1
2
3
getIDFromDisk()//从本地获取用户id
FetchDataFromNetWorkWithId(String id)//根据id去远程服务器取用户数据
processData() 处理数据

由于前两个操作都是耗时的操作,所以我们会用到future这个对象。伪完整的代码如下:

1
2
3
4
5
6
7
Future<ProcessData> createData(){
return getIDFromDisk().then((id){
return FetchDataFromNetWorkWithId(id)
}).then((data){
return ProcessData(data);
});
}

上述代码看上去是不是很复杂,这样还挺好的,没有对返回的错误进行注册处理。而且这还只是两个耗时的操作,就得注册两个then() 的回调。如果是多个呢?那么代码的可读性就会很差。这个时候我们为了代码看起来可读性更好的时候,并且让异步的代码看起来像同步一样,await和async就发挥出作用了
在运用这两个关键字之前,我们先简单的说明几个概念:

  • 同步操作:一个同步操作会阻塞其他操作,直到它完成。
  • 同步方法:一个同步方法里只能执行同步操作。
  • 异步操作:一旦调用成功,一个异步操作可以允许其他操作在其没完成的时候执行。
  • 异步方法:一个异步方法里面不仅可以执行同步操作,也可以执行异步操作。

根据官网的解释,await和async的基本用法如下:

  • async是为了将方法标注为异步方法。
  • 而await只能用在异步方法中(async标注的方法)
1
void main() async { ··· }

如果有返回值的话

1
Future<void> main() async { ··· }

而await主要就是放在调用的异步方法前,如

1
await createOrderMessage()

我们现在将之前的代码用await和async重新写一遍:

1
2
3
4
5
6
Future<ProcessData> createData() async{
String id = await getIDFromDisk();
Data data = await FetchDataFromNetWorkWithId(id)
return ProcessData(data);

}

这样看上去是要简洁了许多,并且看上去像是在同步调用一样。

总结

  • Dart中没有多线程的概念,而是isolate。
  • Dart中处理事件的方式是event loop。
  • 为了可以做到异步操作,Dart引入了future对象,也就是盒子概念。
  • await和async的引入是为了让异步操作看上去像同步操作,且提高代码的可读性。
CATALOG
  1. 1. Dart中的单线程
    1. 1.1. isolate
    2. 1.2. event loop
  2. 2. Dart中的异步操作
    1. 2.1. future
    2. 2.2. await和async
  3. 3. 总结