上一节课课我们学习了异步编程的基础 ——Future 和 async/await,掌握了如何用同步风格编写异步代码。但在实际开发中,异步操作的异常处理、多个异步任务的协同以及性能优化同样重要。今天我们将深入探讨异步进阶内容,解决这些更复杂的问题。
一、异步异常捕获:try-catch + async 的深度实践
异步操作中的异常处理比同步代码更复杂,因为错误可能在 “未来某个时刻” 才会抛出。try-catch 结合 async/await 是处理异步异常的最佳实践,但需要注意一些细节。
1. 基本捕获方式
在 async 函数中,await 后面的代码如果抛出异常,会被 try-catch 捕获,就像同步代码一样:
Future<void> riskyOperation() async {
await Future.delayed(Duration(seconds: 1));
// 模拟异步操作中抛出异常
throw Exception("异步操作失败:网络连接超时");
}
void main() async {
try {
print("开始执行风险操作");
await riskyOperation(); // 等待异步操作,若抛出异常会被捕获
print("操作成功完成(不会执行)");
} catch (e) {
// 捕获异步异常
print("捕获到异常:${e.toString()}");
} finally {
// 无论成败都会执行
print("操作结束,清理资源");
}
}
// 输出:
// 开始执行风险操作
// 捕获到异常:Exception: 异步操作失败:网络连接超时
// 操作结束,清理资源
2. 嵌套异步调用的异常捕获
当 async 函数中包含多个异步调用时,try-catch 能捕获所有嵌套的异常:
Future<void> level3() async {
await Future.delayed(Duration(milliseconds: 500));
throw Exception("最内层错误"); // 抛出异常
}
Future<void> level2() async {
await level3(); // 调用内层异步函数
}
Future<void> level1() async {
await level2(); // 调用中层异步函数
}
void main() async {
try {
await level1(); // 调用外层异步函数
} catch (e) {
// 所有层级的异常都会被捕获
print("顶层捕获到异常:$e"); // 输出:顶层捕获到异常:Exception: 最内层错误
}
}
这体现了 async/await 的优势:异常传递方式与同步代码一致,避免了回调地狱中的异常处理嵌套。
3. 区分不同类型的异常
可以通过 on 类型 catch 捕获特定类型的异常,实现精细化处理:
// 自定义异常类型
class NetworkException implements Exception {
final String message;
NetworkException(this.message);
@override
String toString() => "NetworkException: $message";
}
class DatabaseException implements Exception {
final String message;
DatabaseException(this.message);
@override
String toString() => "DatabaseException: $message";
}
Future<void> fetchData() async {
// 模拟随机异常
await Future.delayed(Duration(seconds: 1));
if (DateTime.now().second % 2 == 0) {
throw NetworkException("服务器连接失败");
} else {
throw DatabaseException("数据查询错误");
}
}
void main() async {
try {
await fetchData();
} on NetworkException catch (e) {
// 只捕获 NetworkException
print("网络错误处理:$e");
// 可以在这里执行重试逻辑
} on DatabaseException catch (e) {
// 只捕获 DatabaseException
print("数据库错误处理:$e");
// 可以在这里执行数据修复逻辑
} catch (e) {
// 捕获其他所有异常
print("未知错误:$e");
}
}
4. 未捕获异常的危险
如果异步异常未被捕获,会导致程序崩溃(在 Flutter 中会导 致应用闪退):
Future<void> unhandledError() async {
throw Exception("未被捕获的异常");
}
void main() {
unhandledError(); // 调用异步函数但未处理异常
print("程序继续执行...");
// 输出:
// 程序继续执行...
// (稍后)Unhandled exception: Exception: 未被捕获的异常
}
最佳实践:所有 await 调用都必须放在 try-catch 中,或通过 .catchError 处理异常。
二、Future 组合:协同多个异步任务
实际开发中经常需要处理多个异步任务,Dart 提供了 Future.wait、Future.any 等工具类方法,帮助我们灵活组合多个 Future。
1. Future.wait:等待所有任务完成(且都成功)
当需要所有异步任务都完成后再继续(如并行加载多个资源),使用 Future.wait:
// 模拟三个不同的异步任务
Future<String> fetchUser() =>
Future.delayed(Duration(seconds: 1), () => "用户数据");
Future<String> fetchOrders() =>
Future.delayed(Duration(seconds: 2), () => "订单数据");
Future<String> fetchProducts() =>
Future.delayed(Duration(seconds: 1), () => "商品数据");
void main() async {
print("开始并行加载数据...");
// 等待所有 Future 完成(总耗时取决于最慢的任务,这里是 2 秒)
List<String> results = await Future.wait([
fetchUser(),
fetchOrders(),
fetchProducts(),
]);
// 所有任务成功完成后才会执行
print("所有数据加载完成:");
print("用户:${results[0]}");
print("订单:${results[1]}");
print("商品:${results[2]}");
}
// 输出:
// 开始并行加载数据...
// (等待 2 秒)
// 所有数据加载完成:
// 用户:用户数据
// 订单:订单数据
// 商品:商品数据
注意:Future.wait 中任何一个任务抛出异常,整个等待会立即失败,并抛出该异常:
Future<String> fetchSuccess() => Future.value("成功数据");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("某个任务失败");
});
void main() async {
try {
await Future.wait([fetchSuccess(), fetchFailure()]);
} catch (e) {
print("捕获到异常:$e"); // 输出:捕获到异常:Exception: 某个任务失败
}
}
如果需要即使部分任务失败也要继续等待其他任务,可以为每个 Future 单独添加错误处理:
Future<String> fetchSuccess() => Future.value("成功数据");
Future<String> fetchFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("某个任务失败");
});
void main() async {
// 为每个 Future 添加 catchError,将错误转换为正常值
List<Future<String>> futures = [
fetchSuccess(),
fetchFailure().catchError((e) => "错误数据:${e.toString()}"),
];
List<String> results = await Future.wait(futures);
print(results); // 输出:[成功数据, 错误数据:Exception: 某个任务失败]
}
2. Future.any:等待第一个完成的任务
当需要多个任务中只要有一个完成就继续(如从多个镜像服务器下载同一资源,选最快的),使用 Future.any:
// 模拟不同速度的异步任务
Future<String> fastTask() =>
Future.delayed(Duration(seconds: 1), () => "快速任务结果");
Future<String> mediumTask() =>
Future.delayed(Duration(seconds: 2), () => "中等任务结果");
Future<String> slowTask() =>
Future.delayed(Duration(seconds: 3), () => "慢速任务结果");
void main() async {
print("等待第一个完成的任务...");
// 只等待第一个完成的任务(这里是 1 秒后完成的 fastTask)
String firstResult = await Future.any([fastTask(), mediumTask(), slowTask()]);
print("第一个完成的任务结果:$firstResult"); // 输出:第一个完成的任务结果:快速任务结果
}
注意:Future.any 中第一个失败的任务会导致整体失败,除非该任务已被单独处理错误:
Future<String> fastFailure() => Future.delayed(Duration(seconds: 1), () {
throw Exception("快速失败");
});
Future<String> slowSuccess() =>
Future.delayed(Duration(seconds: 2), () => "慢速成功");
void main() async {
try {
// 第一个完成的是 fastFailure,会导致整体失败
await Future.any([fastFailure(), slowSuccess()]);
} catch (e) {
print("捕获到异常:$e"); // 输出:捕获到异常:Exception: 快速失败
}
// 正确处理:为可能先失败的任务添加错误处理
String result = await Future.any([
fastFailure().catchError((e) => "错误"), // 错误时返回 null
slowSuccess(),
]);
print("最终结果:$result"); // 输出:最终结果:错误
}
3. Future.forEach:按顺序处理可迭代对象
Future.forEach 会按顺序执行异步操作(完成一个再执行下一个),适合需要串行处理的场景:
void main() async {
List<int> ids = [1, 2, 3];
// 按顺序处理每个 id,每个操作完成后再处理下一个
await Future.forEach(ids, (int id) async {
await Future.delayed(Duration(seconds: 1)); // 模拟耗时操作
print("处理完 id: $id");
});
print("所有 id 处理完成");
}
// 输出(每隔 1 秒输出一行):
// 处理完 id: 1
// 处理完 id: 2
// 处理完 id: 3
// 所有 id 处理完成
三、异步循环与性能陷阱:避免循环中创建 Future
在循环中处理异步操作时,很容易陷入性能陷阱。错误的写法会导致资源浪费或执行效率低下。
1. 错误写法:循环中创建大量并行 Future
// 模拟单个异步任务
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("处理完第 $i 项");
}
void main() async {
final stopwatch = Stopwatch()..start();
// 错误:一次性创建 1000 个并行的 Future
List<Future> futures = [];
for (int i = 0; i < 1000; i++) {
futures.add(processItem(i));
}
await Future.wait(futures);
stopwatch.stop();
print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 100-200 毫秒(并行优势)
}
表面看并行处理很快,但当循环次数很大(如 10 万次)时:
- 会瞬间创建大量
Future对象,消耗大量内存 - 可能触发系统对并发连接的限制(如网络请求)
- 导致 CPU 或 I/O 资源竞争,反而降低效率
2. 改进写法:控制并发数量
通过分批处理或限制并发数,避免资源耗尽:
// 模拟单个异步任务
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("处理完第 $i 项");
}
// 分批处理:每次处理 10 个,处理完一批再处理下一批
Future<void> processInBatches(List<int> items, int batchSize) async {
for (int i = 0; i < items.length; i += batchSize) {
int end = i + batchSize;
List<int> batch = items.sublist(i, end > items.length ? items.length : end);
// 并行处理当前批次
await Future.wait(batch.map((item) => processItem(item)));
print("完成第 ${i ~/ batchSize + 1} 批处理");
}
}
void main() async {
final stopwatch = Stopwatch()..start();
// 创建 1000 个待处理项
List<int> items = List.generate(1000, (index) => index);
// 每批处理 10 个
await processInBatches(items, 10);
stopwatch.stop();
print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 1000 毫秒(10 批 × 100 毫秒)
}
虽然总耗时增加,但避免了资源竞争和系统限制,适合大规模处理场景。
3. 正确的串行循环:用 await 控制顺序
如果任务必须串行执行(后一个依赖前一个的结果),应在循环内部使用 await:
// 模拟单个异步任务
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("处理完第 $i 项");
}
void main() async {
final stopwatch = Stopwatch()..start();
// 正确:串行执行,完成一个再开始下一个
for (int i = 0; i < 5; i++) {
await processItem(i); // 每次循环等待当前任务完成
}
stopwatch.stop();
print("总耗时:${stopwatch.elapsedMilliseconds} 毫秒"); // 约 500 毫秒(5 × 100 毫秒)
}
注意:不要在循环外收集 Future 再 wait,这会变成并行执行:
// 模拟单个异步任务
Future<void> processItem(int i) async {
await Future.delayed(Duration(milliseconds: 100));
// print("处理完第 $i 项");
}
// 错误:看似串行,实际并行
void main() async {
List<Future> futures = [];
for (int i = 0; i < 5; i++) {
futures.add(processItem(i)); // 立即创建所有 Future
}
await Future.wait(futures); // 并行执行,总耗时 ~100 毫秒
}
四、异步代码的调试技巧
异步代码的调试比同步代码更复杂,因为执行顺序不直观。以下是实用技巧:
- 使用
print或日志工具:在关键节点打印时间戳和变量状态,跟踪执行顺序。 - 利用
async函数的栈跟踪:Dart 会记录异步操作的调用栈,异常时能显示完整的调用路径:
Future<void> a() async => await b();
Future<void> b() async => await c();
Future<void> c() async => throw Exception("错误发生");
void main() async {
try {
await a();
} catch (e, stackTrace) {
print("异常:$e");
print("栈跟踪:$stackTrace"); // 会显示从 c → b → a → main 的完整路径
}
}
- 在 Flutter DevTools 中调试:Flutter 提供的 DevTools 有专门的异步任务跟踪面板,可直观查看所有活跃的
Future。