Dart中await内部原理及Flutter异步相关实践问题咨询
问题背景
我正在开发一个需要本地存储的Flutter应用,Android中获取文档目录的getApplicationDocumentsDirectory()返回Future<Directory>,每次使用路径都要等待Future完成,示例代码如下:
class DataStructure { late Future<Directory> _dir = getApplicationDocumentsDirectory(); Future<void> read(String fileName) async { Directory dir = await _dir; // 后续操作 } }
我主要写C++,想减少函数推入事件队列的次数来提升性能(推测含await的异步函数会被编译器拆成多个推入队列的“片段”),因此做了实验验证对已完成的Future使用await是否会中断控制流。
实验1:await异步函数的情况
实验代码:
Future<void> voidFuture(String name) async { print('$name: voidFuture'); } Future<void> printSingle(String name, int index) async { print('$name: -- index = $index'); } Future<void> emit(String name, int count) async { for (int index = 0; index != count; ++index) { await printSingle(name, index); } } Future<void> task_0() async { const String name = 'task_0'; Future<void> emitFinish = emit(name, 3); await voidFuture(name); print('$name: output after await'); await emitFinish; }
运行输出(Dart SDK 2.18.5 windows_x64):
task_0: -- index = 0 task_0: voidFuture task_0: -- index = 1 task_0: output after await task_0: -- index = 2
这个结果符合预期,但修改emit()为直接使用Future构造器后:
实验2:await Future构造器的情况
修改后的emit():
Future<void> emit(String name, int count) async { for (int index = 0; index != count; ++index) { await Future(() { print('$name: -- index = $index'); }); } }
输出变为:
task_0: voidFuture task_0: output after await task_0: -- index = 0 task_0: -- index = 1 task_0: -- index = 2
无法理解为什么-- index = 0出现在output after await之后,似乎异步函数比Future构造器创建的Future优先级更高?
实验3:await已完成Future和常量的情况
核心问题:await会等待已完成的Future吗?,实验代码:
Future<String> stringFuture() async { return '.'; } Future<void> task_3() async { const String name = 'task_3'; Future<void> emitFinish = emit(name, 4); Future<String> futureString = stringFuture(); print('$name: before await of futureString'); await futureString; print('$name: 1st await of futureString over'); await 1; print('$name: 1st await of constant over'); await 2; print('$name: 2nd await of constant over'); await emitFinish; }
使用第一个版本emit()的输出:
task_3: -- index = 0 task_3: before await of futureString task_3: -- index = 1 task_3: 1st await of futureString over task_3: -- index = 2 task_3: 1st await of constant over task_3: -- index = 3 task_3: 2nd await of constant over
可见即使await常量也会把后续代码推入事件队列;用第二个版本emit()时,所有输出都在task_3()最后一个print之后,也不清楚原因。
已知可以用T? value在Future首次完成后赋值,每次使用前检查是否为null,但有四个问题需要解答:
- await关键字内部具体执行逻辑是什么?请给出足够解释上述现象的细节。
- 是否可以重写await的默认行为?比如通过重写某个方法?
- 多次使用Future值的最佳方式是什么?
- 如何在Flutter的欢迎页等待
getApplicationDocumentsDirectory()这类异步函数完成后再构建所有组件?
1. await关键字的执行逻辑
Dart的异步基于事件循环和两个优先级不同的队列:
- 微任务队列:优先级更高,处理短时间完成的异步操作(比如已完成Future的回调、
scheduleMicrotask) - 事件队列:处理耗时操作(比如IO、UI事件、
Future构造器创建的任务)
await的核心逻辑可拆解为:
- 执行到
await foo时,先检查foo是否为已完成的Future(或可转换为已完成Future的类型,比如常量):- 若为已完成的Future:Dart直接获取其值,但仍会把await之后的代码包装成微任务放入微任务队列,当前函数暂停并交出控制权。事件循环会先清空所有微任务,再处理事件队列的任务。
- 若为未完成的Future:当前函数暂停,给该Future注册回调,当Future完成时,回调(即await之后的代码)会被放入微任务队列等待执行。
解释实验现象:
- 实验1:
printSingle是async函数,调用时同步执行到结束(内部无await),返回已完成的Future。emit里的await printSingle会把下一次循环的代码放入微任务队列。task_0中emitFinish = emit(...)会立即同步打印index=0,微任务队列里有emit的下一次循环。接着await voidFuture(name):voidFuture同步打印后返回已完成Future,print('output after await')被放入微任务队列。事件循环先处理微任务:先执行emit的下一次循环(打印index=1),再执行print('output after await'),最后处理剩下的emit循环(index=2)。 - 实验2:
Future(() { ... })构造器创建的是未完成的Future,其任务被放入事件队列。emit里的await Future(...)会等待事件队列的任务,而task_0中的await voidFuture属于微任务优先级,所以voidFuture的打印、output after await的打印先完成,之后事件循环才处理Future构造器的任务,导致index的打印都在后面。 - 实验3:await常量时,Dart会把常量包装成已完成的Future,同样会把后续代码放入微任务队列,所以每次await后,
emit的微任务先执行,导致index打印穿插在task_3的print之间;用第二个版本emit时,所有Future构造器的任务都在事件队列,要等task_3的所有微任务(包括多次await后的代码)执行完,才会处理事件队列的任务,所以所有index打印在最后。
2. 能否重写await的默认行为?
不能。await是Dart语言的关键字,其行为由语言规范定义,无法通过重写方法或自定义类改变。不过你可以封装工具函数实现类似await的逻辑,但无法替换原生await的行为。
3. 多次使用Future值的最佳方式
推荐以下几种方案:
- 提前await并缓存结果:在初始化阶段(比如类的构造器、Flutter的
initState)await Future,把结果存在变量中,后续直接使用变量:class DataStructure { late Directory _dir; Future<void> init() async { _dir = await getApplicationDocumentsDirectory(); } void read(String fileName) { // 直接用_dir,无需await } } - 用
Future.value()缓存已完成的Future:如果必须保留Future类型,第一次await后把结果重新赋值给原Future变量,后续await时直接获取已完成的值:class DataStructure { late Future<Directory> _dir = getApplicationDocumentsDirectory(); Future<void> read(String fileName) async { final dir = await _dir; _dir = Future.value(dir); // 使用dir } } - 使用
AsyncMemoizer:导入package:async/async.dart,用AsyncMemoizer确保异步操作只执行一次,后续调用直接返回缓存结果:import 'package:async/async.dart'; class DataStructure { final _memoizer = AsyncMemoizer<Directory>(); Future<Directory> get _dir => _memoizer.runOnce(() => getApplicationDocumentsDirectory()); Future<void> read(String fileName) async { final dir = await _dir; // 使用dir } }
其中提前缓存结果的方式性能最优,避免了后续await的微任务调度开销。
4. Flutter欢迎页等待异步操作完成后再构建组件
有两种常用方式:
- 使用
FutureBuilder:在欢迎页的build方法中返回FutureBuilder,等待异步操作完成后构建主页面:class SplashScreen extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( future: getApplicationDocumentsDirectory(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return HomePage(docDir: snapshot.data!); } else { return Center(child: CircularProgressIndicator()); } }, ); } } - 使用
StatefulWidget的initState:在initState中执行异步操作,完成后更新状态切换到主页面:class SplashScreen extends StatefulWidget { @override _SplashScreenState createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { bool _isReady = false; Directory? _docDir; @override void initState() { super.initState(); _init(); } Future<void> _init() async { _docDir = await getApplicationDocumentsDirectory(); setState(() { _isReady = true; }); } @override Widget build(BuildContext context) { if (_isReady) { return HomePage(docDir: _docDir!); } else { return Center(child: CircularProgressIndicator()); } } }
如果需要等待多个异步操作,可用Future.wait([future1, future2])合并等待。
内容的提问来源于stack exchange,提问作者Joseph

