Flutter —— 异步编程和多线程

Flutter —— 异步编程

    • 1. 异步编程
      • 1.1 Future
      • 1.2 async ,await
      • 1.3 Future.then()
      • 1.4 Future.catchError
      • 1.5 多个Future
      • 1.6 Future.wait
      • 1.7 microtask
        • Dart的事件循环(event loop)
    • 2. 多线程
      • 2.1. isolate
      • 2.2 compute
    • 3. 异步和多线程的结合
    • 4. Timer
    • 5. 多线程和异步使用时机

1. 异步编程

1.1 Future

Future 类,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则类型为 Future。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。
在网络请求中经常使用异步编程,那么来探索一下Flutter 中的异步编程。

dart是单线程的,所以底层没有锁之类的东西,但是这不代表着dart不能异步,异步不代表多线程。
下面代码运行后发现做其他事情被堵塞住了,这里async不起作用是因为需要搭配Future使用。
Flutter —— 异步编程和多线程_第1张图片
将耗时操作使用Future包装起来,这里可以看到做其他事情就不会被堵塞住了,那么现在即使方法不加async也是异步的,因为Future里面已经是异步的了。或者说async不会异步执行,Future才会异步执行。
Flutter —— 异步编程和多线程_第2张图片
如果把 print(‘结束data = $_data’); 放在Future外面那么其就会先于Future里面的代码执行。
Flutter —— 异步编程和多线程_第3张图片

1.2 async ,await

那么如何让等待Future里面的执行完在执行后面代码呢?这时候需要用到await。加了await之后,后面的代码就会等待Future里面的执行完之后在执行。这里的await需要搭配async使用,而没有await的情况下,async是没有意义的。await后面的操作也需要是异步的。那么也就是说,Future是用来异步执行的,而async和await搭配是用来让Future里面的某块代码同步执行的。
Flutter —— 异步编程和多线程_第4张图片

1.3 Future.then()

Future的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。这里可以使用then来将特定的需要等待的任务放进去,然后不堵塞后面的任务的执行。 如果then没有返回值的话,那么value就是null。
Flutter —— 异步编程和多线程_第5张图片
这里看到Future里面return的话那么value就是return的那个值。这个时候,Future里面返回的数据会被Future包装,然后给到了then里面 的value。
Flutter —— 异步编程和多线程_第6张图片

1.4 Future.catchError

异步中的错误是用catchError来进行处理的。当异步任务中出现异常之后,
如果调用下列方法就会报错,这是因为任务里面抛出了异常没有处理。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future =  Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });
  future.then((value) {
    print('value = $value');
  });

  print('做一点其他事情');
}

在.then 下面添加代码

  future.catchError((e){print("捕获到了错误:$e");}); 

运行后发现还是报错,但是捕获到了异常。正常来说如果处理了异常,就不应该报错了,那么这里是执行顺序的问题吗?
Flutter —— 异步编程和多线程_第7张图片
将catch和then交换位置后运行,发现还是报错
Flutter —— 异步编程和多线程_第8张图片
把then注释掉后发现不报错了。
Flutter —— 异步编程和多线程_第9张图片
那么这里要怎么处理呢?这里一般用链式调用。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then((value) {
    print('value = $value');
  }).catchError((e) {
    print("捕获到了错误:$e");
  });

  print('做一点其他事情');
}

或者在.then中添加onError对错误进行处理
在这里插入图片描述

 future.then((value) {
    print('value = $value');
  },onError: (error){print("捕获到了错误:$error");});

onError是在.then这一次对错误进行处理,而catchError则是多次链式调用中的错误处理
如果在.then之前catchError了,那么.then中的value就是catchError中传过来的值了。在catchError之前的.then不会执行。
Flutter —— 异步编程和多线程_第10张图片
在CatchError之后添加了whenComplete运行后发现还是报出了异常。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });

      future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value'));

  future.whenComplete(() => print("完成了"));

  print('做一点其他事情');
}

这个时候可以在whenComplete之后添加一个catchError,或者使用链式调用。

    future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value')).whenComplete(() => print("完成了"));

上面的例子可以看到链式调用能避免大部分的错误,所以在一般都是使用链式调用来进行处理的,并且把catchError放到最后调用,这样出现异常的时候,前面的.then就不会执行了。如果链式太长的话,可以创建方法来让链式更加简洁。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then(thenFunc).whenComplete(completeFunc).catchError((e) {
    print('error = $e');
    return "错误";
  });

  print('做一点其他事情');
}

FutureOr thenFunc(String value) {
}

FutureOr completeFunc() {
}

1.5 多个Future

Future是放在队列中的,所以Future的执行是有顺序的。下面的代码结果输出是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() {
   Future((){
    sleep(Duration(seconds: 2));
    print("C");
  });

  print("B");
}

运行后看到是B->A->C;
在这里插入图片描述
那么下面的代码运行后会打印什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
  await Future((){
    sleep(Duration(seconds: 2));
    print("C");
  }).then((value) =>   print("D"));

  print("B");
}

运行后发现是A->C->D->B,这里因为B被C堵塞了,所以A会先执行,然后C执行完之后执行D,最后执行B。
在这里插入图片描述
那么下面代码的输出结果是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
   Future((){
    return "任务1";
  }).then((value) =>   print("$value结束"));

   Future((){
     return "任务2";
   }).then((value) =>   print("$value结束"));

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

   Future((){
     return "任务4";
   }).then((value) =>   print("$value结束"));

  print("任务添加完毕");
}

运行后发现是任务添加完毕->A->任务1结束->任务2结束->任务3结束->任务4结束。
Flutter —— 异步编程和多线程_第11张图片

那么这里任务一定是按顺序执行的吗?在任务二添加sleep后重新执行,发现任务顺序还是一样的。
Flutter —— 异步编程和多线程_第12张图片
这说明这里会按异步任务的添加顺序执行的。

1.6 Future.wait

当需要等待多个Future完成,并收集它们的结果,可以使用Future.wait。这个时候.then就会等wait里面所有任务完成后在执行,然后返回的值可以用数组来取。wait里面的任务同时处理,但是是按添加顺序执行的,而如果是链式执行的话,则是一个执行完在执行下一个。

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

1.7 microtask

Dart的事件循环(event loop)

在Dart中,实际上有两种队列:

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

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

在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。
Flutter —— 异步编程和多线程_第13张图片

异步任务我们用的最多的还是优先级更低的 event queue。Dart为 event queue 的任务建立提供了一层封装,就是我们在Dart中经常用到的Future。

正常情况下,一个 Future 异步任务的执行是相对简单的:

声明一个 Future 时,Dart 会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行。
当同步执行的代码执行完毕后,event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的操作。
上面说过了microtask队列的优先级比较高,那么使用microtask就可以让任务优先执行。
下面代码的先执行代码1,代码2,A以及B。

void testFuture3() {
  print('外部代码1');
  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  print('外部代码2');
}

添加了微代码后,那么微任务就会在异步任务之前执行。

void testFuture3() {
  print('外部代码1');

  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  scheduleMicrotask(() {
    print("微任务A");
  });
  print('外部代码2');
}

下面的任务执行后打印情况是什么样的呢?这里5一定是先执行的,然后执行微任务3,然后异步任务按照添加的顺序执行,那么就会先执行future1打印1和4,最后执行future2 打印2,所以打印 5 —— 3 —— 1 —— 4 —— 2;

void testFuture4() {
 Future future1 =  Future((){print('1');});
 Future future2 =  Future((){print('2');});
 scheduleMicrotask(() {
   print("微任务3");
 });
 future1.then((value) => print('4'));
 print('5');
}

运行后验证果真是的。
Flutter —— 异步编程和多线程_第14张图片
那么这里的打印顺序是什么呢?这里future3最先被添加到队列,所以依然会比 1, 2 优先执行,所以会先打印6,所以打印 5 —— 3 —— 6 —— 1 —— 4 —— 2;
Flutter —— 异步编程和多线程_第15张图片
打印结果:
Flutter —— 异步编程和多线程_第16张图片
那么如果是这样的话,打印结果会是什么呢?按照图片里的,执行future3的时候会把自身任务执行完再重新开始循环,那么也就是说,这里5,3之后会先打印6,8 ,然后再打印7,然后再打印142。

void testFuture4() {
  Future future3 = Future(() => null);
  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  }).then((value) =>  print("8"));

  Future future1 = Future(() {
    print('1');
  });

  future1.then((value) => print('4'));
  Future future2 = Future(() {
    print('2');
  });
  scheduleMicrotask(() {
    print("3");
  });

  print('5');
}

打印结果:
Flutter —— 异步编程和多线程_第17张图片
那么把.then拆出来的话是什么结果呢?其实这里相当于把.then里面的任务放到微任务里面去了,所以8依然会优先执行。这也是为什么 future1里面的then的任务会比 future2先执行。

  Future future4 =  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  });
  
  future4.then((value) => print("8"));

打印结果:
Flutter —— 异步编程和多线程_第18张图片

2. 多线程

2.1. isolate

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main。isolate。

下面的打印结果一定是123,因为这里Future会等待主线程睡眠结束。

void IsolateDemo() {
  print('1');
  Future(func);
  sleep(Duration(seconds: 2));
  print('2');
}

FutureOr func() {
  print('3');
}

那么如果将func放在Isolate里面执行,那么在打印2之前func就会先打印3。

void IsolateDemo() {
  print('1');

  Isolate.spawn(func,10);
  sleep(Duration(seconds: 1));
  print('2');
}


void func(int message) {
  print('3');
}

在这里插入图片描述
多添加几次任务来执行。

void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  sleep(Duration(seconds: 1));
  print('外部代码2');
}

运行后发现这里任务一不一定比任务2先执行,而且外部代码也会在任务一任务二之前执行,证明了这里Isolate确实是子线程里面执行任务的。
Flutter —— 异步编程和多线程_第19张图片
Dart中的isolate更像是一个进程,因为isolate有独立的内存空间,意味着每个isolate中的数据是独立的,所以没有资源抢夺的问题,也就不需要锁。所以,我们访问数据不能直接访问。isolate相当于轻量级的进程,他不会独立开辟堆和栈,而是给了一个局部的内存空间,所有的变量,内存对象都在这个空间里面,和原来的程序传递数据的时候就需要用到进程间的通讯。
声明一个属性没然后在func里面赋值,在IsolateDemo里面睡眠之后打印a的值看一下是否改变。

int a = 10;
void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,1000);

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}

void func(int message) {
  a = message;
  print('第一个来了a=$a');
}

运行后发现是没有改变的
在这里插入图片描述
那么如果非要改变a的值的话,那么就需要用到port,将port的sendPort作为参数传给func,然后添加listen监听数据变化来接受func里面传过来的值。

void IsolateDemo() {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
// 创建一个Isolate
  Isolate.spawn(func,port.sendPort);
  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
  });

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}


void func(SendPort send) {
  send.send(100);
  print('第一个来了a=$a');
}

运行后发现a的值改变了。
Flutter —— 异步编程和多线程_第20张图片
既然我们Isolate开辟了空间,那么我们就要手动去销毁Isolate。 这里创建临时变量iso,然后在port.listen里面关闭端口以及杀掉iso。这里的await不会堵塞后面代码的执行,因为这里是其他线程里面的。

void IsolateDemo() async {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
  // 创建一个Isolate
 Isolate iso =  await Isolate.spawn(func,port.sendPort);

  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
    port.close();
    iso.kill();
  });
  print('外部代码来了a=$a');
  print('外部代码2');
}

2.2 compute

关于Iso还有一个封装叫compute,这里使用compute之后发现这里并不会堵塞住compute的执行。

void computeTest() {
  print('外部代码1');
  compute(func1,10);
  sleep(Duration(seconds: 2));
  print('外部代码2');
}
FutureOr func1(message) {
  print('compute');
}

运行结果:
在这里插入图片描述
compute和iso不一样的是,如果加了await,那么后面的代码就需要等待。并且compute可以返回数据来修改数据的值,但是如果在func1修改a的值的话同样是没有效果的。

void computeTest() async{
  print('外部代码1');
  a = await compute(func1,10);
  print('外部代码2 a = $a');
}
int func1(message) {
  sleep(Duration(seconds: 2));

  print('compute');
  return 1000;
}

在这里插入图片描述

3. 异步和多线程的结合

下面的打印是同步还是异步呢?

void isoLoadDemo() {
  Future( () => compute(testfunc,123)).then((value) => {print('1结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('2结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('3结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('4结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('5结束')});

}

运行后看到这里是异步的。
Flutter —— 异步编程和多线程_第21张图片
那么如果是这样的呢?

void isoLoadDemo() {
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现这里是同步的了。
Flutter —— 异步编程和多线程_第22张图片
这是为什么呢?因为箭头函数其实是return compute(testfunc, 123) 的,如果在下面的代码添加return之后,那么就是异步的了。如果返回的是子线程的Future,那么.then处理的是子线程的Future的结果,否则就是当前Future的结果。
那么 compute(testfunc, 123) 的执行是有序的还是无序的呢?在Future里面添加打印

void isoLoadDemo() {
  Future(() {
    print('1开始');
    return compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    print('2开始');
    return compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    print('3开始');
    return  compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    print('4开始');
    return compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    print('5开始');
    return compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现开始是按顺序打印的,那么就说明compute(testfunc, 123)是按顺序执行的,但是返回的时机是无序的。
Flutter —— 异步编程和多线程_第23张图片
那么同理可知,如果去掉return, 那么就会按顺序打印1开始,1结束 … ,运行后证明是正确的。
Flutter —— 异步编程和多线程_第24张图片
其实.then任务和Future可以当作是一个整体。下面代码中,按道理应该是Future里面的微任务1先被添加到队列中,然而实际上却是微任务先执行,所以说这里可以把.then和Future当作是一个整体。

 Future x = Future((){
    print('异步任务1');
    scheduleMicrotask(() {
      print('微任务1');
    });
  });

  x.then((value) {
    print('微任务2');
  });

在这里插入图片描述
这里whenComplete也是一样的,并且如果whenComplete在then前面,那么就先于.then执行,否则就后于.then执行。
Flutter —— 异步编程和多线程_第25张图片
Flutter —— 异步编程和多线程_第26张图片

4. Timer

Timer会默认开启一个异步任务。

void main() {

  Timer.run(() {
    print('异步任务');
  });

  print('来了');
  }

运行后
在这里插入图片描述
回到之前的聊天界面,试一下timer是否会卡住ui。
在initState里面添加一个timer

  int _count = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

然后在timer启动后拖动ListView,发现timer是没有像ios里面一样是暂停的。但是这里有一个小问题,之前聊天界面是保存状态的,如果没有保存状态的话,那么重新进入聊天页面,就会重新走一个init,重新创建一个timer,而前面的timer没有被取消,那么就会有多个tiemr存在的情况。
Flutter —— 异步编程和多线程_第27张图片
这个时候就需要用到dispose,当State 被永久的从视图树中移除时,Flutter 会调用dispose 函数。
添加一个变量timer,然后给timer赋值。

  late Timer _timer;
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

在dispose中判断如果_timer有值且正在运行,那么就将其取消掉。

 @override
  void dispose() {
    // TODO: implement dispose

    if (_timer != null && _timer.isActive) {
      _timer.cancel()
    }

    super.dispose();
  }

5. 多线程和异步使用时机

什么时候该使用多线程,什么时候该使用异步呢?
在聊天界面的AppBar的actions添加一个GestureDetector,里面是一个添加按钮,然后在onTap里面添加一个异步的耗时操作。

 GestureDetector(
            child: Container(
              child: Icon(Icons.add),
            ),
            onTap: () {
              Future(() {
                print('开始');
                for (int i = 0; i < 1000000000; i++) {
                }
               print("结束了");
              });
            },
          ),

点击后发现主线程完全被卡死了,页面滑动不了,并且timer也不动了。这个时候就需要将耗时操作放在子线程执行,这样主线程就不会被卡住了。

onTap: () {
              Future(() {
               return compute(func,123);
              });
            },
 FutureOr func(int message) {
    print('开始');
    for (int i = 0; i < 1000000000; i++) {
    }
    print("结束了");
  }

总结:

  • Dart中的异步
    • Future对象来完成异步操作。
      • 通过工厂构造方法创建Future对象。
      • 参数为Dart的函数
        • 函数的执行代码将被放入事件队列异步执行。
    • async 和 await 。如果Future内部代码希望同步执行,则使用await修饰。被async修饰的函数为异步执行。
    • Future结果处理
      • Future.then 用来注册一个Future完成时要调用的回调
      • Future.catchError注册一个回调,来捕捉Future的error
        • Future.catchError回调只处理原始Future抛出的错误,不能处理回调函数抛出的错误
        • onError只能处理当前Future的错误
      • Future.whenComplete 在Future完成之后总是会调用,不管是错误导致的完成还是正常执行完毕。
  • Dart的事件循环
    • 在Dart中,实际上有两种队列:
      • 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
      • 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。
  • Dart中的多线程
    • Dart是单线程语言,但并不代表它不能并行执行代码。因为它拥有Isolate。
    • Isolate
      • Isolate可以看成是一个小的进程。
        • 它拥有独立的内存空间,不同Isolate之间通过消息进行通信
        • 它拥有自己的事件循环及队列(MicroTask 和 Event)
      • Isolate使用
        • 1、创建子线程任务:Isolate.spawn(arg1,arg2);
          • arg1: 需要在子线程执行的函数
          • arg2:传递给函数的参数
          • 这样就在另一个线程中执行arg1的代码了。
        • 2、端口通讯
          • ReceivePort port = ReceivePort()//构造一个端口。
          • port.listen(arg)//注册一个回调监听
            • arg为回调函数。参数为消息内容
          • 在子线程中.通过port.send() 发送消息
        • 3、关闭端口,销毁Isolate
          • 注意端口使用完毕需要调用port.close()函数关闭
          • Isolate使用完毕,需要调用Isolate.kill()函数销毁
    • compute
      • 由于dart中的Isolate比较复杂,数据传输比较麻烦,因此flutter在foundation库中封装了一个轻量级compute操作
      • 使用:compute(func,count)
        • func:子线程函数!func如果有返回值会直接被compute函数返回出去!
        • count: 给函数的参数
  • 异步多线程结合
    • Dart中的异步是可以和多线程结合使用的。
    • 如果Future中返回子线程的返回值。那么Future的处理是异步的
    • 如果Future中没有返回子线程的返回值。那么Future的处理是同步的
    • Future的结果处理会在Future执行完毕立即执行。可以看做是一个任务。

你可能感兴趣的:(Flutter,flutter)