众所周知,Dart是一门单线程的语言,我们可以将一些耗时的任务放到异步操作中,但是异步任务必须等线程空闲时才会去执行,这是无法满足有些场景需求的,下面就来讲下如何处理这些场景。
如何处理耗时的操作
不同语言的不同处理方式
- 多线程。比如 Java、C++,就是开启一个新的线程,将耗时操作放在新的线程里面处理,再通过线程间通信的方式,将拿到的数据传给主线程处理。
- 单线程+事件循环。比如 JavaScript、Dart 都是基于单线程加事件循环来完成耗时操作的处理。
单线程的异步操作
应用程序大部分时间是处于空闲状态的,并不是一直在和用户进行交互。而我们的操作系统存在阻塞式调用
和非阻塞式调用
。
- 阻塞式调用:调用结果返回之前,当前线程会被挂起,调用线程只有在得到调用结果之后才会继续执行。
- 非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要间隔一段时间来检查一下有没有结果返回即可。
Dart 的异步操作就是利用非阻塞式调用实现的。
什么是事件循环
和 iOS 应用很像,在 Dart 的线程中也存在事件循环和消息队列的概念,但在 Dart 中线程叫做isolate
。应用程序启动后,开始执行 main 函数并运行 main isolate
。
每个 isolate 包含一个事件循环以及两个事件队列,event loop事件循环
,以及event queue
和microtask queue事件队列
,event 和 microtask 队列有点类似 iOS 的 source0 和source1。
-
event queue:负责处理I/O事件、绘制事件、手势事件、接收其他 isolate 消息等外部事件。
-
microtask queue:可以自己向 isolate 内部添加事件,事件的优先级比 event queue高。
Dart 中的异步
Dart中的异步操作主要使用Future
以及async
、await
,async 和 await 是要一起使用的,这就是协程的一个语法糖。
- Future 延时操作的一个封装,可以将异步任务封装为
Future
对象,我们通常通过then()来处理返回的结果 - async 用于标明函数是一个异步函数,其返回值类型是Future类型
- await 用来等待耗时操作的返回结果,这个操作会阻塞到后面的任务
什么是协程
协程分为无线协程
和有线协程
,无线协程在离开当前调用位置时,会将当前变量放在堆区,当再次回到当前位置时,还会继续从堆区中获取到变量。所以,一般在执行当前函数时就会将变量直接分配到堆区,而async
、await
就属于无线协程的一种。有线协程则会将变量继续保存在栈区,在回到指针指向的离开位置时,会继续从栈中取出调用。
async、await原理
以 async、await为例,协程在执行时,执行到async
则表示进入一个协程,会同步执行async
的代码块。async
的代码块本质上也相当于一个函数,并且有自己的上下文环境。当执行到await
时,则表示有任务需要等待,CPU 则去调度执行其他 IO,也就是后面的代码或其他协程代码。过一段时间 CPU 就会轮循一次,看某个协程是否任务已经处理完成,有返回结果可以被继续执行,如果可以被继续执行的话,则会沿着上次离开时指针指向的位置继续执行,也就是await
标志的位置。
由于并没有开启新的线程,只是进行 IO 中断改变 CPU 调度,所以网络请求这样的异步操作可以使用async
、await
,但如果是执行大量耗时同步操作的话,应该使用isolate
开辟新的线程去执行。
下面举例来讲解异步
- 模拟一个同步的耗时操作,看会输出怎样的结果
输出结果,C 并没有因为有耗时操作而影响线程的任务执行
flutter: B
flutter: C
flutter: A
flutter: D
复制代码
- 那现在对这个例子改造一下,加上 async、await
输出结果是,C 等待了耗时操作完成之后才执行。使用 async 来标明 getData 这个函数是一个异步函数,await 用于等待请求返回的结果,此时会阻塞掉后面的代码,只有当请求结束后面的代码才会执行。
flutter: B
flutter: A
flutter: D
flutter: C
复制代码
- 多Future 情况下执行顺序是什么样的
执行的顺序是按着创建顺序执行
flutter: A
flutter: B
flutter: C
flutter: D
复制代码
- Future 是链式调用的,可以在后面调用
//处理返回结果
.then((value) => null)
//处理错误
.onError((error, stackTrace) => null)
//完成回调
.whenComplete(() => null)
//处理异常
.catchError(onError);
复制代码
Dart 中的事件循环
-
微任务队列:表示一个短时间内就会完成的异步任务,它的优先级比事件队列高。
-
事件队列:包含所有的外来事件,比如:I/O、手势、绘图等。
这是一张 Flutter 任务队列的执行图:
这两个队列也是有优先级的,当 isolate 开始执行后,会先处理 microtask
的事件,当microtask 队列中没有事件后,才会处理 event队列
中的事件,并按照这个顺序反复执行。但需要注意的是,当执行 microtask 事件时,会阻塞 event 队列的事件执行,这样就会导致渲染、手势响应等 event 事件响应延时。为了保证渲染和手势响应,应该尽量将耗时操作放在 event 队列中。
下面这个例子可以证明这一点:
flutter: 开始执行
flutter: 结束执行
flutter: 微任务
flutter: A
flutter: A结束
flutter: B
flutter: B结束
复制代码
假如微任务
添加在异步任务
里面,异步任务和微任务谁先执行呢?看下面这个例子:
执行结果是异步任务里面的微任务没有异步任务先执行,并且异步任务链式调用的处理也比微任务优先
flutter: 开始执行
flutter: 结束执行
flutter: 微任务
flutter: A
flutter: A结束
flutter: A里面的微任务
复制代码
多线程
在一个页面中做耗时比较大的运算时,就算用了 async、await 异步处理,UI页面的动画还是会卡顿,因为还是在这个UI线程中做运算,异步只是你可以先做其他,等我这边有结果再返回,但是,我们的计算仍旧是在这个UI线程,仍会阻塞UI的刷新,异步只是在同一个线程的并发操作。所以这个时候就需要创建新的线程来执行耗时操作解决这个问题。
什么是 Isolate
Isolate
是 Dart 平台对线程的实现方案,但和普通 Thread 不同的是,isolate 拥有独立的内存,isolate 由线程和独立内存构成。正是由于 isolate 线程之间的内存不共享,所以 isolate 线程之间并不存在资源抢夺的问题,所以也不需要锁。通过 isolate 可以很好的利用多核 CPU,来进行大量耗时任务的处理。
但是12月28号,Google发布了Dart2.15
版本。我们首先重新设计和实现了 isolate 的工作方式,引入了一个新概念: isolate 组。Isolate 组中的 isolate 共享各种内部数据结构,这些数据结构则表示正在运行的程序。这使得组中的单个 isolate 变得更加轻便。如今,因为不需要初始化程序结构,在现有 isolate 组中启动额外的 isolate 比之前快 100 多倍,并且产生的 isolate 所消耗的内存减少了 10 至 100 倍。关于Dart2.15版本更多的内容可以参考:mp.weixin.qq.com/s/g-1uCl3up… 。
先看下面这个异步的例子,一看就知道执行顺序是 A->B->C
下面举例说明 Dart 中确认存在多线程
执行结果是这样的,可以看出确实没有按着创建的顺序执行
flutter: A
flutter: 第二个
flutter: 第二个
flutter: 第一个
flutter: 第二个
flutter: 第一个
flutter: 第二个
flutter: 第一个
flutter: 第一个
flutter: B
复制代码
Isolate 通信机制
isolate 线程之间的通信主要通过 port
来进行,这个 port 消息传递的过程是异步的。通过建立通信双方的 sendPort
和 receiveport
,进行相互的消息传递。
这样就实现了通信。isolate实现方法需要用 static
修饰,不然会报下面这个错误
Unhandled Exception: Invalid argument(s): Isolate.spawn expects to be passed a static or top-level function
复制代码
什么是compute
dart 中的 Isolate 比较重量级,UI 线程和 Isolate 中的数据的传输比较复杂,因此 Flutter 为了简化用户代码,在 Foundation 库中封装了一个轻量级 compute 操作来实现。这个使用非常方便,并且可以直接返回值。
import 'package:flutter/foundation.dart';
void computeTest() async {
print('开始执行');
int b = await compute(test1, 10);
print('结束执行: b = $b');
}
static int test1(int count) {
sleep(Duration(seconds: 2));
print('执行方法');
return 100;
}
复制代码
使用场景
- 任务执行事件很短的,比如几十毫秒以内的建议用 Future
- 任务执行时间长,只有一次返回的用compute,有多次返回的用Isolate
Mac Flutter环境配置及Android Studio的使用
参考资料:www.jianshu.com/p/54da18ed1…
欢迎关注、点赞及转发。