简介
从客户端编程角度来说,异步模型是重要并且比较难理解的一块。每种语言的具体实现不一样,但是应用场景却比较相似。
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用得比较少,本质上还是概念比较难理解,用起来比较麻烦。