不知道大家有没有一个疑问:Dart是单线程执行,那它是如何实现异步操作的呢?

本文将对Dart/Flutter提供的Isolate,Event Loop,Future,async/await等进行异步操作相关的知识点进行分析。

Isolate

什么是Isolate?

An isolate is what all Dart code runs in. It’s like a little space on the machine with its own, private chunk of memory and a single thread running an event loop.

  • Isolate相当于Dart语言中的线程Thread,是Dart/Flutter的执行上下文环境(容器);
  • Isolate有自己独立的内存地址和Event Loop,不存在共享内存所以不会出现死锁,但是比Thread更耗内存;

  • Isolate间不能直接访问,需凭借Port进行通信;

Main Isolate

当执行完main()入口函数后,Flutter会创建一个Main Isolate。一般情况下任务都是在这个Main Isolate中执行的。

多线程

一般情况下在Main Isolate执行任务是可以接受的,但是把一些耗时操作放在Main Isolate中执行,会造成掉帧的现象,这会对用户体验造成严重影响。此时,选择将耗时任务分发到其他的Isolate中就是一个很好的实现方式了。

所有的Dart Code都是在Isolate中执行的,代码只能使用同一个Isolate中的内容,不同的 Isolate是内存隔离的,因此只能通过 Port 机制发送消息通信,其原理是向不同的 Isolate 队列中执行写任务。

案例

我们做了个简单的Demo,屏幕中间有一个心在不停的动画(由小变大,再由大变小)。当我们点击右下角对的加号按钮,会进行一个耗时的运算。如果耗时操作在Main Isolate执行,将会造成界面的丢帧,动画将会出现卡顿的情况。

我们目前就是需要解决这个掉帧的问题。

1.compute 方法

Flutter封装了一个compute这个高级API函数可以让我们方便的实现多线程的功能。

Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
}

compute接收两个必传参数:1,需要执行的方法;2,传入的参数,这参数最多只能是1个,所以多个参数需要封装到Map中;

  • 最开始的代码
// 耗时操作的方法:`bigCompute`
Future<int> bigCompute(int initalNumber) async {int total = initalNumber;for (var i = 0; i < 1000000000; i++) {total += i;}return total;
}// 点击按钮调用的方法:`calculator`
void calculator() async {int result = await bigCompute(0);print(result);
}// FloatingActionButton的点击事件
FloatingActionButton(onPressed: calculator,tooltip: 'Increment',child: Icon(Icons.add),
)
  • 修改代码
  1. 新建一个calculatorByComputeFunction方法,用compute调用bigCompute方法:
void calculatorByComputeFunction() async {// 使用`compute`调用`bigCompute`方法,传参0int result = await compute(bigCompute, 0);print(result);
}
  1. 修改FloatingActionButton的点击事件方法为calculatorByComputeFunction
FloatingActionButton(onPressed: calculatorByComputeFunction,tooltip: 'Increment',child: Icon(Icons.add),
)

咱点击试试?

[VERBOSE-2:ui_dart_state.cc(186)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message : (object is a closure - Function ‘bigCompute’:.)

  1. 解决Error:将bigCompute改为为static方法(改为全局函数也是可行的)
static Future<int> bigCompute(int initalNumber) async {int total = initalNumber;for (var i = 0; i < 1000000000; i++) {total += i;}return total;
}

警告:还有一个需要注意的是所有的Platform-Channel的通信必须在Main Isolate中执行,譬如在其他Isolate中调用rootBundle.loadString("assets/***")就掉坑里了。

2. 直接使用Isolate

上面我们用compute方法,基本上没有看到Isolate的身影,因为Flutter帮我们做了很多工作,包括Isolate创建,销毁,方法的执行等等。一般情况下我们使用这个方法就够了。

但是这个方法有个缺陷,我们只能执行一个任务,当我们有多个类似的耗时操作时候,如果使用这个compute方法将会出现大量的创建和销毁,是一个高消耗的过程,如果能复用Isolate那就是最好的实现方式了。

多线程Isolate间通信的原理如下:

  1. 当前Isolate接收其他Isolate消息的实现逻辑: Isolate之间是通过Port进行通信的,ReceivePort是接收器,它配套有一个SendPort发送器, 当前Isolate可以把SendPort发送器送给其他Isolate,其他Isolate通过这个SendPort发送器就可以发送消息给当前Isolate了。

  2. 当前Isolate给其他Isolate发消息的实现逻辑: 其他Isolate通过当前IsolateSendPort发送器发送一个SendPort2发送器2过来,其他的Isolate则持有SendPort 2发送器2对应的接收器ReceivePort2接收器2,当前Isolate通过SendPort 2发送消息就可以被其他Isolate收到了。

是不是很绕!我再打个比喻:市面上有一套通信工具套件,这套通信工具套件包括一个接电话的工具和一个打电话的工具。A留有接电话的,把打电话的送给B,这样B就可以随时随地给A打电话了(此时是单向通信)。 如果B也有一套工具,把打电话的送给A,这样A也能随时随地给B打电话了(此时是双向通信了)。

上代码:

class _MyHomePageState extends State<MyHomePage>with SingleTickerProviderStateMixin {// 1.1 新建的isolateIsolate isolate;// 1.2 Main Isolate的接收器ReceivePort mainIsolaiteReceivePort;    // 1.3 Other Isolate的发送器SendPort otherIsolateSendPort;// 新建(复用)Isolatevoid spawnNewIsolate() async {// 2.1 建一个接收Main Isolate的接收器if (mainIsolaiteReceivePort == null) {mainIsolaiteReceivePort = ReceivePort();}try {if (isolate == null) {// 2.2 新建的isolate, 把Main Isolate发送器传给新的isolate,calculatorByIsolate是需要执行的任务isolate = await Isolate.spawn(calculatorByIsolate, mainIsolaiteReceivePort.sendPort);// 2.3 Main Isolate 通过接收器接收新建的isolate发来的消息    mainIsolaiteReceivePort.listen((dynamic message) {if (message is SendPort) {// 2.4 如果新建的isolate发来的是一个发送器,就通过这个发送器给新建的isolate发送值过去(此时双向通讯建立成功)otherIsolateSendPort = message;otherIsolateSendPort.send(1);print("双向通讯建立成功,主isolate传递初始参数1");} else {// 2.5 如果新建的isolate发来了一个值,我们知道是耗时操作的计算结果。print("新建的isolate计算得到的结果$message");}});} else {// 2.6 复用otherIsolateSendPortif (otherIsolateSendPort != null) {otherIsolateSendPort.send(1);print("双向通讯复用,主isolate传递初始参数1");}}} catch (e) {}}// 这个是新的Isolate中执行的任务static void calculatorByIsolate(SendPort sendPort) {// 3.1 新的Isolate把发送器发给Main IsolateReceivePort receivePort = new ReceivePort();sendPort.send(receivePort.sendPort);// 3.2 如过Main Isolate发过来了初始数据,就可以进行耗时计算了receivePort.listen((val) {print("从主isolate传递过来的初始参数是$val");int total = val;for (var i = 0; i < 1000000000; i++) {total += i;}// 3.3 通过Main Isolate的发送器发给Main Isolate计算结果sendPort.send(total);});} @overridevoid dispose() {// 释放资源mainIsolaiteReceivePort.close();isolate.kill();super.dispose();}
}

代码注释的很详细了,就不再解释了。是不是代码好多的感觉,其实如果理解流程了逻辑倒不复杂。

关于Isolate的概念和使用我们就介绍到这里,接下来我们来介绍Isolate中的一个重要知识点Event Loop.

Event Loop

Loop这个概念绝大部分开发者都应该很熟悉了,iOS中有NSRunLoop,Android中有Looper, js中有Event Loop,名字上类似,其实所做的事情也是类似的。

Event Loop的官方介绍如下图:

  • 静态示意图

执行完main()函数后将会创建一个Main Isolate

  • 动态示意图

  • Event Loop会处理两个队列MicroTask queueEvent queue中的任务;
  • Event queue主要处理外部的事件任务:I/O,手势事件,定时器,isolate间的通信等;
  • MicroTask queue主要处理内部的任务:譬如处理I/O事件的中间过程中可能涉及的一些特殊处理等;
  • 两个队列都是先进先出的处理逻辑,优先处理MicroTask queue的任务,当MicroTask queue队列为空后再执行Event queue中的任务;
  • 当两个队列都为空的时候就进行GC操作,或者仅仅是在等待下个任务的到来。

为了比较好的理解 Event Loop 的异步逻辑,我们来打个比喻:就像我去长沙某网红奶茶品牌店买杯“幽兰拿铁”(由于是现做的茶,比较耗时)的过程。

  1. 我来到前台给服务员说我要买一杯你们店的“幽兰拿铁”,然后服务员递给了我一个有编号的飞盘(获取凭证);
  2. 奶茶店的备餐员工就将我的订单放在订单列表的最后面,他们按照顺序准备订单上的商品,准备好一个就让顾客去领取(Event queue 先进先出进行处理),而我就走开了,该干啥干啥去了(异步过程,不等待处理结果);
  3. 突然他们来了个超级VIP会员的订单,备餐员工就把这个超级VIP订单放在了其他订单的最前面,优先安排了这个订单的商品(MicroTask优先处理)—此场景为虚构;
  4. 当我的订单完成后,飞盘开始震动(进行结果回调),我又再次回到了前台,如果前台妹子递给我一杯奶茶(获得结果),如果前台妹子说对不起先生,到您的订单的时候没水了,订单没法完成了给我退钱(获得异常错误错误)。

我们常用的异步操作Future,async,await都是基于Event Loop,我们接下来就来介绍他们异步操作背后的原理。

Future

我们接下来用代码总体说明一下Future背后的逻辑:


final myFuture = http.get('https://my.image.url');
myFuture.then((resp) {setImage(resp);
}).catchError((err) {print('Caught $err'); // Handle the error.
});
// 继续其他任务
...
  1. http.get('https://my.image.url')返回的是一个未完成状态的Future, 可以理解为一个句柄,同时http.get('https://my.image.url')被丢进了Event queue中等待被执行,然后接着执行当前的其他任务;
  2. Event queue执行完这个get请求成功后会回调then方法,将结果返回,Future为完成状态 ,就可以进行接下来的操作了;
  3. Event queue执行完这个get请求失败后会回调catchError方法,将错误返回,Future为失败状态 ,就可以进行错误处理了。

我们接下来分别介绍下Future的一些相关函数:

构造函数
  • Future(FutureOr<T> computation())
final future1 = Future(() {return 1;
});

computation被放入了Event queue队列中

  • Future.value
final future2 = Future.value(2);

值在MicroTask queue队列中返回

  • Future.error(Object error, [StackTrace? stackTrace])
final future3 = Future.error(3);

这个error表示出现了错误,其中的值不一定需要给一个Error对象

  • Future.delay
final future4 = Future.delayed(Duration(seconds: 1), () {return 4;
});

延迟一定时间再执行

Future结果回调then
final future = Future.delayed(Duration(seconds: 1), () {print('进行计算');return 4;
});
future.then((value) => print(value));
print('继续进行接下来的任务');// flutter: 继续进行接下来的任务
// flutter: 进行计算
// flutter: 4
Future出现错误后的回调onError
final future = Future.error(3);
future.then((value) => print(value))
.onError((error, stackTrace) => print(error));print('继续进行接下来的任务');// flutter: 继续进行接下来的任务
// flutter: 3
Future完成的回调whenComplete
final future = Future.error(3);
future.then((value) => print(value))
.onError((error, stackTrace) => print(error))
.whenComplete(() => print("完成"));print('继续进行接下来的任务');// flutter: 继续进行接下来的任务
// flutter: 3
// flutter: 完成

async/await

做过前端开发的对这两个关键字应该很熟悉,Flutterasync/await本质上只是Future的语法糖,使用方法也很简单。

Future<String> createOrderMessage() async {var order = await fetchUserOrder();return 'Your order is: $order';
}
  1. await放在返回值为Future的执行任务前面,相当于做了个标记,表示执行到此为止,等有结果后再往下执行;
  2. 使用了await必须在方法后面加上async;
  3. async方法必须在返回值上封装上Future

FutureBuilder

Flutter为我们封装了一个FutureBuilder这个Widget,可以方便的构造UI, 以获取图片进行展示为例:

FutureBuilder(future: 加载图片的Futureuilder: (context, snapshot) {// 未完成if (!snapshot.hasData) {// 使用默认的占位图} else if (snapshot.hasError) {// 使用加载失败的图}  else {// 使用加载到的图}
},

总结

通过新建Isolate可以实现多线程,每个线程Isolate都有Event Loop可以执行异步操作。

有些移动开发者可能偏爱ReactiveX响应式编程,譬如RxJavaRxSwiftReactiveCocoa等。其实他们也是异步编程的一种方式,Flutter为我们提供了一个对应的类—Stream,其也有丰富的中间操作符,还提供了StreamBuilder可以构建UI,接下来我们将会一篇文章来分析它。

Flutter异步编程详解相关推荐

  1. [进阶] --- Python3 异步编程详解(史上最全篇)

    [进阶] - Python3 异步编程详解:https://blog.csdn.net/lu8000/article/details/45025987 参考:http://aosabook.org/e ...

  2. Python异步编程详解

    一.异步编程相关概念 1.I/O模型 IO操作实际过程涉及到内核和调用这个IO操作的进程.对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝 ...

  3. [转][进阶]-Python3 异步编程详解

    目录 1 什么是异步编程 1.1 阻塞 1.2 非阻塞 1.3 同步 1.4 异步 1.5 并发 1.6 并行 1.7 概念总结 1.8 异步编程 1.9 异步之难(nán) 2 苦心异步为哪般 2. ...

  4. [进阶]-Python3 异步编程详解(史上最全篇)

    目录 1 什么是异步编程 1.1 阻塞 1.2 非阻塞 1.3 同步 1.4 异步 1.5 并发 1.6 并行 1.7 概念总结 1.8 异步编程 1.9 异步之难(nán) 2 苦心异步为哪般 2. ...

  5. Python3 异步编程详解

    1 什么是异步编程 1.1 阻塞 程序未得到所需计算资源时被挂起的状态. 程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的. 常见的阻塞形式有:网络I/O阻塞.磁盘I/ ...

  6. python2异步编程_最新Python异步编程详解

    我们都知道对于I/O相关的程序来说,异步编程可以大幅度的提高系统的吞吐量,因为在某个I/O操作的读写过程中,系统可以先去处理其它的操作(通常是其它的I/O操作),那么Python中是如何实现异步编程的 ...

  7. 最新Python异步编程详解

    我们都知道对于I/O相关的程序来说,异步编程可以大幅度的提高系统的吞吐量,因为在某个I/O操作的读写过程中,系统可以先去处理其它的操作(通常是其它的I/O操作),那么Python中是如何实现异步编程的 ...

  8. 前端JavaScript 异步编程详解

    目录 菜鸟教程官网 JavaScript 异步编程 异步的概念 详图 什么时候用异步编程 回调函数 概念 例如: 最后 菜鸟教程官网 地址 JavaScript 异步编程 异步的概念 异步(Async ...

  9. promise异步编程 详解

    1.基本概念 promise是对异步编程的一种抽象.它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常.也就是说promise对象代表了一个异步操作,可以将异步对象和回调函数脱离开来 ...

最新文章

  1. Spring IOC 容器根据Bean 名称或者类型进行autowiring 自动依赖注入
  2. leetcode43. 字符串相乘 经典大数+和*
  3. python进阶-Python 进阶用法 (持续更新)
  4. Van-UI发送验证码demo -效果篇
  5. DOM——获取元素的方式
  6. apache Apache winnt_accept: Asynchronous AcceptEx failed 错误的解决
  7. Codeforces Round #243 (Div. 2) A~C
  8. 20191010:希尔排序代码详解
  9. CGCS2000 VS WGS84
  10. Python 语言程序设计(5-3) 代码复用与函数递归
  11. Windows下Python,setuptools,pip,virtualenv的安装
  12. SDOI2015 约数个数和
  13. python简明教程_06
  14. win7怎么设置悬浮桌面便签
  15. 常用的前端还款计算器(包括按月等额本息、按四月等额本息、到期还本付息、到期还本按月付息四种还款方式)
  16. 常见的服务器报错数字的意思
  17. 【老九学堂】【C语言】CodeBlocks安装文档
  18. C/c++中内存拷贝函数memcpy详解
  19. android 全景usb 全景,汽车360度全景USB高清数字信号输出系统的制作方法
  20. java课程设计-音乐播放器,基于java的音乐播放器设计.doc

热门文章

  1. C语言中信号量的使用
  2. php tinyint,Qeephp 中数据的 tinyint 类型
  3. 对 MVC、MTV 和 MVVM的理解总结
  4. 不再饥饿营销 苹果公司一改中国内地销售策略
  5. Sequoiadb分布式数据库入门使用教程
  6. matlab RBF语音识别
  7. 【8051单片机学习资料大全】
  8. 印刷工艺- 喷墨印刷
  9. 文件管理工具:Path Finder mac
  10. 1 什么是PyTorch