Flutter 中异步任务及多线程介绍



异常的捕获

onError 与 catchError 的区别

getdata() async {
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
  });

  // future.then((value) => print('value=$value'),
  //     onError: (e)=>print(e.toString()));
  
  future
      .then((value) => print('value=$value'))
      .catchError((e)=>print('使用的时候捕获到了异常:' + e.toString()));
}

当我们在 Future 的闭包中抛出异常的时候可以使用 onError 或者 catchError 进行捕获异常并处理,它们的区别就是 onError 是写在 then 里面的,是针对 then 这个事件。

catchError 的使用顺序

虽然我们上面已经通过 catchError 捕获了异常,但是当我们后面再调用 whenComplete 的时候还需要再次调用 catchError,要不然程序还是会抛出异常。正确代码如下。

future
      .then((value) => print('value=$value'))
      .catchError((e)=>print('使用的时候捕获到了异常:' + e.toString()));

  future
      .whenComplete(() => print('完成了'))
      .catchError((e)=>print('完成的时候捕获到了异常:' + e.toString()));

Future 链式调用

Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
  })
      .then((value) => print('value=$value'))
      .whenComplete(() => print('完成了'))
      .catchError((e)=>print('捕获到了异常:' + e.toString()));

针对以上代码我们我们可以使用链式调用进行简写,但是需要注意一点的是 catchError 最好放到最后面。

代码抽取

getdata() async {
  Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
  })
      .then(thenFunc)
      .whenComplete(() => print('完成了'))
      .catchError((e)=>print('捕获到了异常:' + e.toString()));
}

FutureOr thenFunc(Never value) {
  print('value=$value');
}

then 为例,当我们闭包中代码量比较多的时候,我们可以在外面定义一个方法,对代码进行抽离。

多个异步任务的处理

Future 任务顺序执行

testFuture() async {
  Future(() {
    return '任务 1';
  }).then((value) => print('value=$value'));

  Future(() {
    sleep(Duration(seconds: 1));
    return '任务 2';
  }).then((value) => print('value=$value'));

  Future(() {
    return '任务 3';
  }).then((value) => print('value=$value'));
}

这里因为 Future 任务都是在同一个队列中,且 Flutter 是单线程,所以这里任务的执行顺序是按照 Future 的添加顺序执行的,任务1 -> 任务2 -> 任务3

任务前后依赖

后面的任务依赖前一个任务的结果

Future(() {
    return '任务 1';
  }).then((value) {
    print('$value结束');
    return '任务 2';
  }).then((value) {
    print('$value结束');
    return '任务 3';
  });

当我们后面的任务需要依赖前一个任务的执行结果的时候,可以在 then 的闭包中执行下一个任务,并把结果数据返回出去。

多个任务结束统一处理

Future.wait([
  Future(() {
    return '任务 1';
  }),
  Future(() {
  return '任务 2';
  }),
  Future(() {
  return '任务 3';
  })
  ]).then((value) => print(value[0] + value[1] + value[2]));

当我们碰到这样一个需求,同时请求多个接口,在这些接口都请求完成的时候统一处理,这时候我们就可以使用 Future.waitFuture.wait 里面是一个数组,可以放入多个 Future 任务,在这些任务都执行完毕的时候会调用 then 方法,这时候 value 是一个数组,装的是这几个 Future 任务的返回结果。这里 value 数组的顺序跟 Future 任务的顺序是一样的,且 Future 任务也是顺序执行的。

Dart 事件循环

通过上图案例我们可以看到 scheduleMicrotask 中的任务会比 Future 中的任务先执行,这里是因为在 Dart 中有两种队列,事件队列跟微任务队列。

  • 事件队列(event queue),包含所有的外来事件:I/Omouse eventsdrawing eventstimersisolate 之间的信息传递。
  • 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于 event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue 添加的任务主要是由 Dart 内部产生。

因为 microtask queue 的优先级高于 event queue ,所以如果 microtask queue 有太多的微任务, 那么就可能会霸占住当前的 event loop。从而对 event queue 中的触摸、绘制等外部事件造成阻塞卡顿。

在每一次事件循环中,Dart 总是先去 microtask queue 中查询是否有可执行的任务,如果没有,才会处理后续的 event queue 中的任务。

相关案例

  • 案例 1
Future f = Future(() => print('1'));
  Future(() => print('2'));
  scheduleMicrotask(() => print('3'));
  f.then((value) => print('4'));
  print('5');

这里打印的顺序是 5、3、1、4、2。

  • 案例 2
Future f = Future(() => print('1'));
  Future(() => print('2'));
  scheduleMicrotask(() => print('3'));
  f.then((value) {
    print('4');
    scheduleMicrotask(() => print('5'));
  }).then((value) => print('6'));
  print('7');

这里打印的顺序是 7、3、1、4、6、5、2,这里 6 相当于 f.then 里面的任务, then 中的任务相当于被添加到了微任务队列,所以 4、6 在 5 之前执行。

Dart 中的多线程 Isolate

  Isolate.spawn(func1, 10);
  Isolate.spawn(func2, 20);
  Isolate.spawn(func3, 30);

如上代码中 func1func2func3 会在子线程中执行,且是无序的。DartIsolate 更像是一个进程,它有独立的内存空间,也就意味着每个进程中的空间是独立的,所以不存在资源抢夺的问题,所以不需要锁,这样的话用起来就非常便捷。但是也有一些需要注意的问题,数据不能直接访问,下面我们来看一下。

这里我们在 func 中对 a 的值进行了修改,但是第二次打印的时候可以看到 a 的值还是 10。因为 func 中的 a 被独立起来了,与外部相互之间不能共享。如果想让 funca 的值的修改能在外部起作用的话就需要用到端口。

int a = 10;

IsolateDemo() async {
  //创建 port
  ReceivePort port = ReceivePort();
  //创建 Isolate
  Isolate iso = await Isolate.spawn(func, port.sendPort);
  //通过 port 监听数据变化
  port.listen((message) {
    a = message;
    print('接收到了:a = $a');
    
    //关闭端口
    port.close();
    iso.kill();
  });
}

func(SendPort send) {
  send.send(100);
  print('第一次打印:a = $a');
}

这里我们可以定义一个 port,在 func 中传入 port.sendPort,这时候在 func 方法中发送数据,在 listen 闭包中就可以监听到。端口使用完毕后要调用 close 关闭端口,调用 kill 销毁 Isolate

computeDemo() async {
  int a = await compute(func1, 10);
}

int func1(int count) {
  return 100;
}

Dart 中使用多线程除了 Isolate,还有 computecompute 是基于 Isolate 的封装,用法与 Isolate 类似,有区别的就是 compute 可以接收函数中的返回值。

扩展

pubspec.yaml 文件介绍

  • name:项目名称,必填字段。
  • description:项目描述,非必填字段。
  • publish_to:代表要发布的平台,none 的话代表不发布。
  • version:工程的版本号。
  • dependencies:可以设置 flutter 的版本,默认是获取最新版本。
  • environment:可以指定 dart 版本的兼容范围。
  • dev_dependencies:代表开发环境下的指定版本,打包的时候不会被打包。
  • flutter:字体以及图片都是在 flutter 下面设置。
  • dio: ^4.0.1: 以 dio 三方库为例,^4.0.1 代表大版本区间不变的写法,相当于 >= 4.0.1, < 5.0.0dio: 4.0.1 代表指定版本,dio: any 代表任意版本,dio:>3.0.1`` 代表版本号大于 3.0.1。

import 介绍

import 'package:http/http.dart' as http;

http 为例,当我们导入 import 的时候这里可以看到 asas 的作用就是给库起别名,防止类名或者方法名冲突。导入库的时候默认是整个文件都导入,关键字 show 代表只要导入的内容,hide 代表不需要导入的内容,我们可以根据需要进行指定。

你可能感兴趣的:(Flutter 中异步任务及多线程介绍)