异步+多线程组合使用

上一节异步和多线程我们是分开介绍的,那么异步Future + 多线程compute搭配使用会是什么样子的呢?还是用代码执行来观察吧

  1. void isolateFunc() {
  2. Future(() => compute(test, 1)).then((value) => print('任务1结束'));
  3. Future(() => compute(test, 1)).then((value) => print('任务2结束'));
  4. Future(() => compute(test, 1)).then((value) => print('任务3结束'));
  5. Future(() => compute(test, 1)).then((value) => print('任务4结束'));
  6. Future(() => compute(test, 1)).then((value) => print('任务5结束'));
  7. }

image.png
我们看到这个执行顺序并不是按照顺序的。我们换一种写法,把箭头函数写成一行执行语句

  1. void isolateFunc() {
  2. Future(() {
  3. compute(test, 1);
  4. }).then((value) => print('任务1结束'));
  5. Future(() {
  6. compute(test, 1);
  7. }).then((value) => print('任务2结束'));
  8. Future(() {
  9. compute(test, 1);
  10. }).then((value) => print('任务3结束'));
  11. Future(() {
  12. compute(test, 1);
  13. }).then((value) => print('任务4结束'));
  14. Future(() {
  15. compute(test, 1);
  16. }).then((value) => print('任务5结束'));
  17. }

image.png
这时可以发现这个执行又是按照顺序的,那么明明是一样的代码,为什么执行的结果不同呢?真的是一样的吗,仔细一看,箭头函数实际上是包含了return的执行语句。我们在compute前面添加一个return之后再次执行会发现跟首次的结果是一样的。
我们看下compute的定义,typedef ComputeImpl = Future<R> Function<Q, R>(ComputeCallback<Q, R> callback, Q message, { String? debugLabel });可以知道compute也是一个Future,所以return compute(test, 1);之后得到的是一个子线程的Future,此时执行的then也是在子线程。如果没有return此时then执行的当前主线程的异步Future.这也就解释了上面添加return与否执行结果完全不同的原因。

那么Future内的任务执行有序嘛?

  1. void isolateFunc() {
  2. Future(() {
  3. print('1');
  4. return compute(test, 1);
  5. }).then((value) => print('任务1结束'));
  6. Future(() {
  7. print('2');
  8. return compute(test, 1);
  9. }).then((value) => print('任务2结束'));
  10. Future(() {
  11. print('3');
  12. return compute(test, 1);
  13. }).then((value) => print('任务3结束'));
  14. }

image.png
从日志可以看出:异步的任务还是按照顺序执行,但是子线程的执行是没有顺序的。

去掉return使得.then在主线程内异步执行那么顺序又是怎么样的?

  1. void isolateFunc() {
  2. Future(() {
  3. print('1');
  4. compute(test, 1);
  5. }).then((value) => print('任务1结束'));
  6. Future(() {
  7. print('2');
  8. compute(test, 1);
  9. }).then((value) => print('任务2结束'));
  10. Future(() {
  11. print('3');
  12. compute(test, 1);
  13. }).then((value) => print('任务3结束'));
  14. }

image.png

Timer使用

Timer默认会开启异步任务

  1. void timer() {
  2. Timer.run(() {
  3. print('异步任务');
  4. });
  5. print('1');
  6. }

image.png
我们知道在iOS中如果没有设置Timer的模式为commonMode,那么在拖拽scrollView的时候定时器就会停止下来,那么在Flutter中是否有这样的现象呢?我们可以来测试下
image.png
来到项目中的这个页面,在下面方法中插入Timer的代码并且拖动当前的ListView

  1. @override
  2. void initState() {
  3. // TODO: implement initState
  4. super.initState();
  5. int count = 0;
  6. Timer.periodic(Duration(seconds: 1), (timer) {
  7. count++;
  8. print(count);
  9. if (count > 8) {
  10. timer.cancel();
  11. }
  12. });
  13. }

我们看下控制台的打印是否会停止,结果发现并没有。说明在Flutter内部对Timer的处理还是比较友好的。
image.png
页面切换出去再回来,状态保留同时Timer也没有紊乱。此时是建立在bool get wantKeepAlive => true;的基础上,如果把这个属性改为false,切换出去再回来会重复创建Timer,此时也好解决。我们可以在页面销毁的时候销毁这个Timer就可以了。

  1. @override
  2. void dispose() {
  3. // TODO: implement dispose
  4. if (timer != null && timer.isActive) {
  5. timer.cancel();
  6. }
  7. super.dispose();
  8. }

异步+多线程使用场景

在该页面新增一个耗时操作,此时再看当前的页面会不会卡顿。
AppBar新增一个action,在点击的时候新增一个耗时操作,点击这里的时候会发现当前的页面卡住在了这里,ListView不能拖动了。

  1. GestureDetector(
  2. child: Container(
  3. child: Icon(Icons.ac_unit_sharp)),
  4. onTap: () {
  5. Future(() {
  6. print('开始了');
  7. for (int i = 0; i < 10000000000000; i++) {}
  8. print('结束了');
  9. });
  10. })

由此得出结论,如果是耗时操作的话,我们应该放到子线程中去处理,仅仅是放入到异步并不行。
所以我们需要改造一下:

  1. GestureDetector(
  2. child: Container(
  3. child: Icon(Icons.ac_unit_sharp)),
  4. onTap: () {
  5. Future(() {
  6. return compute(test, 1);
  7. });
  8. })
  9. test(int message) {
  10. print('开始了');
  11. for (int i = 0; i < 100000000000; i++) {}
  12. print('结束了');
  13. }

这样把耗时操作放入到子线程中去处理,此时拖动ListView就不会卡住,同时定时器也在运行。这里直接调用compute不使用Future包装效果是一样的直接开启子线程。
image.png

网络dio

pub上搜索dio
image.png
复制dio: ^4.0.4pubspec.yaml文件,运行flutter pub get安装
image.png
那么怎么使用呢?我们以下载腾讯课堂mac端为例,首先在官网上拿到dmg的地址:腾讯课堂Mac端下载地址

  1. void dioTest() {
  2. final dio = Dio();
  3. String downloadUrl =
  4. 'https://edu-files-1251502357.cos.ap-shanghai.myqcloud.com/CourseTeacher_2.8.1.13_DailyBuild.dmg';
  5. dio.download(downloadUrl, '/Users/liukun/Desktop/腾讯课堂.dmg',
  6. onReceiveProgress: onReceiveProgress);
  7. }
  8. void onReceiveProgress(int count, int total) {
  9. var progress = count.toDouble() / total.toDouble();
  10. print('count = $count, total = $total progress = $progress');
  11. }

image.png
调用download方法有一个属性是onReceiveProgresscommand+enter查看对此的定义typedef ProgressCallback = void Function(int count, int total);counttotal马上就能联想到当前已经下载的数据大小和总的数据大小

通过控制台的打印输出也验证了确实是这样:
image.png
以上是存储在电脑桌面上,如果想要存储在项目中的话,可以修改savePath,通过Directory.systemTemp.path就能拿到文件的沙盒systemTemp

  1. void dioTest() {
  2. final dio = Dio();
  3. String downloadUrl =
  4. 'https://edu-files-1251502357.cos.ap-shanghai.myqcloud.com/CourseTeacher_2.8.1.13_DailyBuild.dmg';
  5. String savePath = Directory.systemTemp.path + '/腾讯课堂.dmg';
  6. dio.download(downloadUrl, savePath, onReceiveProgress: onReceiveProgress);
  7. }

dio的功能是比较强大的,它的监测手段比较丰富,可以随时观测,所以在实战的项目中更偏向于使用dio
回到项目,我们把http替换成dio,首先创建一个http_manager的网络工具类,在这里封装网络请求

  1. import 'package:dio/dio.dart';
  2. class HttpManager {
  3. static Future<Response> get(String path) {
  4. return Dio().get(path);
  5. }
  6. }

回到首页在getData()方法中把http替换下来
改动一:首先换掉了Uri使用Url
改动二:httpresponse.body需要手动转换成字典map,而使用dio则自动帮我们转换了
改动三:http的数据存在response.body中,而dio的数据存在data

  1. Future<List<ChatModel>> getData() async {
  2. String path = 'http://rap2api.taobao.org/app/mock/293759/home/chat/list';
  3. var response = await HttpManager.get(path);
  4. if (response.statusCode == 200) {
  5. // json转换成字典
  6. // final responseBody = json.decode(response.body);
  7. return response.data['data']
  8. .map<ChatModel>((item) => ChatModel.fromMap(item))
  9. .toList();
  10. //转模型
  11. } else {
  12. throw Exception('statusCode=${response.statusCode}');
  13. }
  14. }

其实HttpManager我们可以更进一步的封装

  1. static Future _sendRequest(
  2. String path,
  3. HttpMethod method, {
  4. data,
  5. Map<String, dynamic>? queryParameters,
  6. CancelToken? cancelToken,
  7. Options? options,
  8. ProgressCallback? onSendProgress,
  9. ProgressCallback? onReceiveProgress,
  10. }) async {
  11. try {
  12. switch (method) {
  13. case HttpMethod.GET:
  14. return await _dioInstance.get(path,
  15. queryParameters: queryParameters,
  16. options: options,
  17. cancelToken: cancelToken,
  18. onReceiveProgress: onReceiveProgress);
  19. case HttpMethod.POST:
  20. return await _dioInstance.post(path,
  21. data: data,
  22. queryParameters: queryParameters,
  23. cancelToken: cancelToken,
  24. options: options,
  25. onSendProgress: onSendProgress,
  26. onReceiveProgress: onReceiveProgress);
  27. default:
  28. throw Exception('请求方式错误');
  29. }
  30. } on DioError catch (e) {
  31. print(e.message);
  32. } on Exception catch (e) {
  33. print(e.toString());
  34. }
  35. }
  36. enum HttpMethod {
  37. GET,
  38. POST,
  39. }