Flutter之异步编程 2025-05-12 周一

简介

从客户端编程角度来说,异步模型是重要并且比较难理解的一块。每种语言的具体实现不一样,但是应用场景却比较相似。

Future的使用

  • 这是最常见的异步操作,经常用于网络API的调用。

  • 推荐使用 async/await 语法糖;不考虑then的方式。

Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2)); // 模拟网络请求
  return '数据加载完成';
}
  • 使用起来就像写同步代码,非常方便。在需要的时候,可以放入try结构,不过大部分时候不需要。
void fetchDataExample() async {
  String data = await fetchData(); // 等待Future完成
  print('数据: $data');
}
  • Future.delayed是延时的方便方法,用的比较多。普通的写法就是顺序执行的,效果和同步代码一样,非常方便。

并行执行多个 Future

这种使用场景还是比较多的,比如访问多个接口,然后再展示页面数据。Future.wait可以很好地满足这种需求。

Future<void> asyncOperation1() async {
    await Future.delayed(
        Duration(seconds: 2), () => print('Async Operation 1'));
  }

  Future<void> asyncOperation2() async {
    await Future.delayed(
        Duration(seconds: 3), () => print('Async Operation 2'));
  }

  Future<void> asyncOperation3() async {
    await Future.delayed(
        Duration(seconds: 4), () => print('Async Operation 3'));
  }

void futureWaitApproach() async {
    print('Start: ${DateTime.now()}');
    await Future.wait([asyncOperation1(), asyncOperation2(), asyncOperation3()]);
    print('End: ${DateTime.now()}');
}

/*
Start: 2024-02-18 12:16:20.796960
Async Operation 1
Async Operation 2
Async Operation 3
End: 2024-02-18 12:16:24.804660
*/
  • Future.wait 总是返回一个 List,类型是List<Object>,可以通过下标方式访问。

  var futures = [
    Future.value(42),
    Future.value('hello'),
  ];

  var result = await Future.wait(futures);
  print(result); // [42, hello]
  print(result[0].runtimeType); // int
  print(result[1].runtimeType); // String
  print(result.runtimeType); // List<Object>

关于FutureBuilder

  • 不适合页面级的UI,比如和上拉下拉组件配合就比较麻烦。

  • 页面级的还是使用GetX比较好,大多数情况用不到FutureBuilder。

  • 适合单一的Future,对于那种多个接口的,比如Future.wait就不合适。

  • 可以用在StatelessWidget,更加简洁。

class MyWidget extends StatelessWidget {
  Future<String> _fetchData() => Future.delayed(
        Duration(seconds: 2), 
        () => '从API获取的数据'
      );

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _fetchData(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator(); // 加载中
        } else if (snapshot.hasError) {
          return Text('错误: ${snapshot.error}'); // 出错
        } else {
          return Text('数据: ${snapshot.data}'); // 加载完成
        }
      },
    );
  }
}
  • 实际做个的一个例子是距离显示。需要百度地图插件,输入经纬度,计算两点之间的距离。由于插件给出的经纬度计算需要用到future,而这个距离只是页面的一行,所以把这部分专门做了一个Widget,用到了FutureBuilder,效果还不错。在用FutureBuilder之前试过了好多方法,都不理想。
class DistanceWidget extends StatelessWidget {
  const DistanceWidget({
    super.key,
    this.latitude,
    this.longitude,
  });

  final dynamic latitude;
  final dynamic longitude;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: calculateDistance(),
      builder: (BuildContext context, AsyncSnapshot<double> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        } else if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        } else if (snapshot.hasData) {
          double? distanceKm = snapshot.data;
          return Row(
            children: [
              Image.asset(
                R.assetsImgDistanceIcon16pix,
              ),
              SizedBox(width: 2.r),
              Text(
                "${distanceKm?.toStringAsFixed(1)}km",
                style: TextStyle(
                  color: StyleUtils.textColor0.withValues(alpha: 0.85),
                  fontSize: 13.r,
                  fontWeight: FontWeight.normal,
                ),
              ),
            ],
          );
        } else {
          return const Text('No data');
        }
      },
    );
  }

  Future<double> calculateDistance() async {
    /// 计算距离;首页保存了当前的位置信息
    final MainLogic mainLogic = Get.find<MainLogic>();
    final BMFCoordinate start =
        BMFCoordinate(mainLogic.latitude, mainLogic.longitude);

    /// 终点的经纬度外部传入,或者给默认值
    final endLatitude = double.tryParse("$latitude") ?? 39.909187;
    final endLongitude = double.tryParse("$longitude") ?? 116.397451;
    final BMFCoordinate end = BMFCoordinate(endLatitude, endLongitude);

    double distance =
        await BMFCalculateUtils.getLocationDistance(start, end) ?? 0;
    LogUtil.log('直径距离;$distance米');
    double distanceKm = distance / 1000.0;
    return distanceKm;
  }
}

Microtask

  • 优先级高于 Future: Microtasks 的执行优先级高于 Future,也就是说,即使有一个正在执行的 Future,如果有 Microtasks 需要执行,它们也会先于 Future 执行。

  • 同步执行: 与 Future 不同,Microtasks 是同步执行的,它们会在当前事件循环的末尾,一个接一个地执行完毕。

import 'dart:async';

void main() {
  print('开始');
  
  for (int i = 0; i < 3; i++) {
    // 每次循环添加一个microtask
    scheduleMicrotask(() {
      print('Microtask $i 执行');
    });
    
    // 每次循环添加一个Future
    Future(() {
      print('Future $i 执行');
    });
  }
  
  print('结束');
}

// 输出顺序:
// 开始
// 结束
// Microtask 0 执行
// Microtask 1 执行
// Microtask 2 执行
// Future 0 执行
// Future 1 执行
// Future 2 执行
// Microtask优先,并且顺序执行;Future后执行,并且顺序会乱。
  • 使用场景是将耗时任务分阶段进行,减少阻塞 UI 线程。
import 'dart:async';
import 'package:flutter/material.dart';

class MicrotaskExample extends StatefulWidget {
  @override
  _MicrotaskExampleState createState() => _MicrotaskExampleState();
}

class _MicrotaskExampleState extends State<MicrotaskExample> {
  List<int> data = [];
  bool isProcessing = false;

  void processData() {
    setState(() => isProcessing = true);
    
    // 模拟大量数据处理
    final largeData = List.generate(10000, (i) => i);
    
    // 使用microtask分批次处理数据,避免阻塞UI
    void processChunk(int start, int end) {
      if (start >= end) {
        setState(() => isProcessing = false);
        return;
      }
      
      // 处理一部分数据
      for (int i = start; i < start + 1000 && i < end; i++) {
        data.add(largeData[i] * 2);
      }
      
      // 更新UI
      setState(() {});
      
      // 安排下一批处理
      scheduleMicrotask(() {
        processChunk(start + 1000, end);
      });
    }
    
    processChunk(0, largeData.length);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Microtask示例')),
      body: Center(
        child: isProcessing
            ? Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 20),
                  Text('处理中: ${data.length}/10000'),
                ],
              )
            : ElevatedButton(
                onPressed: processData,
                child: Text('开始处理数据'),
              ),
      ),
    );
  }
}
  • 上面的例子就是把一个大计算(10000次)分成多个小计算(每次1000)。如果只是用Future,就得一直等着,看转转,等10000次算好了再显示一个最终结果。另外,如果像经典的Counter程序那样,没计算一次就更新一次界面,那么就要更新(10000次),也不知道页面是否会卡。

  • 在实际使用中,microtask没有用到过,这跟我们的电商业务可能有点关系,很少有计算10000次的场景。如果是科学计算,可能会碰到多一些吧。

Isolate

  • Isolate 是 Dart 语言中的并发执行单元,类似于OC中的线程thread。

  • 使用 Isolate.spawn() 可以创建新的 Isolate,并在其中执行任务。

  • Isolate 之间的通信使用 SendPort 和 ReceivePort 机制进行,这样可以避免共享内存带来的线程安全问题。

  • Isolate 适用于执行 CPU 密集型或长时间运行的任务,例如图像处理、机器学习等。

  • 除了通信和同步特殊一点,其他和OC的thread差不多。OC中是切换线程,而Isolate是监听和发送消息。

import 'dart:isolate';

void main() async {
  print('主线程开始,ID: ${Isolate.current.hashCode}');

  // 创建接收端口(用于接收子Isolate的消息)
  ReceivePort mainReceivePort = ReceivePort();

  // 启动新的Isolate,并传入入口函数和通信端口
  await Isolate.spawn(
    isolateEntryPoint,      // 子Isolate的入口函数
    mainReceivePort.sendPort,  // 用于向主线程发送消息的端口
  );

  // 监听来自子Isolate的消息
  mainReceivePort.listen((message) {
    print('主线程收到消息: $message');
    if (message == '完成') {
      mainReceivePort.close();  // 关闭端口,终止监听
    }
  });

  print('主线程继续执行...');
}

// 子Isolate的入口函数(必须是顶级函数或静态方法)
void isolateEntryPoint(SendPort mainSendPort) {
  print('子Isolate开始,ID: ${Isolate.current.hashCode}');

  // 模拟耗时计算
  int result = 0;
  for (int i = 0; i < 1000000; i++) {
    result += i;
  }

  // 向主线程发送结果
  mainSendPort.send('计算结果: $result');
  mainSendPort.send('完成');

  print('子Isolate结束');
}

/*
flutter: 主线程开始,ID: 94828901
flutter: 主线程继续执行...
flutter: 子Isolate开始,ID: 180230284
flutter: 子Isolate结束
flutter: 主线程收到消息: 计算结果: 499999500000
flutter: 主线程收到消息: 完成
*/

从输出结果看,这个作用和GCD的工作者线程计算isolateEntryPoint;子线程完成后,回到主线程显示计算结果。行为是异步的不会卡UI。只是这种消息和监听的书写模式感觉比GCD的用block表示线程切换要繁琐很多。

  • 使用 Compute 简化 Isolate 创建,这个例子跟普通的Future感觉上也差不了多少。
import 'dart:isolate';
import 'package:flutter/foundation.dart';  // 提供compute函数

void main() async {
  print('主线程开始');

  // 使用compute函数在后台Isolate中执行耗时任务
  final result = await compute(heavyCalculation, 1000000);
  
  print('主线程收到计算结果: $result');
  print('主线程继续执行...');
}

// 耗时计算函数(必须是顶级函数或静态方法)
int heavyCalculation(int max) {
  print('后台Isolate开始计算');
  int sum = 0;
  for (int i = 0; i < max; i++) {
    sum += i;
  }
  print('后台Isolate计算完成');
  return sum;
}

/*
flutter: 主线程开始
flutter: 后台Isolate开始计算
flutter: 后台Isolate计算完成
flutter: 主线程收到计算结果: 499999500000
flutter: 主线程继续执行...
*/

书写上跟普通的Future差不多。从输出看结果看,效果是同步的,会卡UI,显然这种用法不是很可取。普通的Future只要等一小会儿,问题不大。但是既然用到了Isolate,等的时间一般会比较长。像例子中这样干等着,不大合适。UI被卡的像死机,还不直接同步执行来得干脆。

  • 相对来说,用消息传递与OC的线程差不多,用来处理图片等耗时工作还是可以的。可以考虑封装成一个StatefulWidget。比如下面的例子
import 'dart:isolate';
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Isolate示例')),
        body: Center(child: ImageProcessingWidget()),
      ),
    );
  }
}

class ImageProcessingWidget extends StatefulWidget {
  @override
  _ImageProcessingWidgetState createState() => _ImageProcessingWidgetState();
}

class _ImageProcessingWidgetState extends State<ImageProcessingWidget> {
  bool _isProcessing = false;
  String _status = '准备处理';

  Future<void> _processImage() async {
    setState(() {
      _isProcessing = true;
      _status = '处理中...';
    });

    // 创建通信端口
    final mainReceivePort = ReceivePort();

    // 启动Isolate处理图片
    await Isolate.spawn(
      _imageProcessingIsolate,
      mainReceivePort.sendPort,
    );

    // 监听处理结果
    mainReceivePort.listen((message) {
      if (message is String) {
        setState(() {
          _status = message;
          _isProcessing = false;
        });
        mainReceivePort.close();
      }
    });
  }

  // 子Isolate的图片处理函数
  static void _imageProcessingIsolate(SendPort mainSendPort) {
    // 模拟图片处理(如解码、压缩、滤镜等)
    try {
      // 模拟耗时操作
      for (int i = 0; i < 5; i++) {
        sleep(Duration(seconds: 1));
      }
      
      mainSendPort.send('图片处理完成!');
    } catch (e) {
      mainSendPort.send('处理失败: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        if (_isProcessing) CircularProgressIndicator(),
        SizedBox(height: 20),
        Text(_status),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _isProcessing ? null : _processImage,
          child: Text('处理图片'),
        ),
      ],
    );
  }
}
  • 实际项目中Isolate用得比较少,本质上还是概念比较难理解,用起来比较麻烦。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容