Flutter 中的异步编程总结

3,767 阅读11分钟

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 编程开发」微信公众号 。