Flutter异步原理
特性Isolate执行线程在主线程(Main Isolate)中轮询在新的后台线程中运行适用场景网络请求、文件 I/O、简单的异步等待复杂计算、解析超大 JSON、视频解码内存共享共享主线程内存内存隔离,通过 Port 传值异步函数的同步性async函数在遇到第一个await关键字之前的代码完全是同步执行的,不会有任何延迟。链式触发之后的代码(例如)必须等待doSth彻底完成后,作为一个微任务(M
在 Flutter(以及底层的 Dart)中,异步编程的核心并不是“多线程”,而是事件循环(Event Loop)。
理解这个机制就像理解一个单排队的咖啡店:店员(线程)只有一个,但通过巧妙的任务安排,他可以一边等咖啡机出水,一边帮下一位客人点单。
1. 核心架构:Event Loop 机制
Dart 是单线程执行模式。这意味着它在同一时间只能执行一行代码。为了处理耗时操作(如网络请求或定时器)而不卡住 UI,它维护了两个队列:
-
Microtask Queue(微任务队列): 优先级极高。通常用于非常短的、需要在返回任务调度之前立即执行的操作。
-
Event Queue(事件队列): 存放外部事件,如 I/O、手势点击、定时器、绘图事件以及
Future。
运行流程
-
首先执行
main()函数中的同步代码。 -
检查 Microtask Queue。只要里面有任务,就一直执行,直到清空。
-
从 Event Queue 中取出一个事件并执行。
-
每执行完一个事件任务,都会回到 Microtask Queue 检查一遍。
-
循环往复。
2. Future 与 async/await 的真相
很多初学者认为 Future 是开启了新线程,其实不然。
-
Future: 当你创建一个Future时,你实际上是把一个任务丢进了 Event Queue。 -
async/await: 这只是语法糖。await会暂时“挂起”当前的函数,将剩余的代码包装成一个匿名函数并放入队列,然后释放执行权,让 Event Loop 去处理其他事情。
示例对比
Dart
void test() async {
print("A"); // 同步执行
await Future(() => print("B")); // 进入 Event Queue
print("C"); // 只有等 B 执行完,这行才会回到微任务/事件循环中执行
}
// 输出顺序:A -> (其他同步代码) -> B -> C
3. 为什么 UI 会卡顿?
由于是单线程,如果你在同步代码里写了一个巨大的循环:
Dart
// 坏代码:这会导致 UI 停止刷新
for (int i = 0; i < 1000000000; i++) { ... }
此时 Event Loop 被这个循环阻塞,无法去处理 Event Queue 里的“绘图事件”(Flutter 每 1/60 秒会产生一个绘图事件)。这就是掉帧的本质。
4. 真正并行:Isolates
如果你确实有极高密度的计算任务(如图片滤镜处理、大文件加密),Future 救不了你,因为它依然在主线程跑。
这时你需要 Isolate。
-
每个 Isolate 都有自己独立的内存堆和 Event Loop。
-
它们之间不共享内存,只能通过“消息传递”来沟通。
-
这才是真正的多核并行。
总结
| 特性 | Future / async / await | Isolate |
| 执行线程 | 在主线程(Main Isolate)中轮询 | 在新的后台线程中运行 |
| 适用场景 | 网络请求、文件 I/O、简单的异步等待 | 复杂计算、解析超大 JSON、视频解码 |
| 内存共享 | 共享主线程内存 | 内存隔离,通过 Port 传值 |
追问1:下面代码执行逻辑
void main() {
print("main1");
get1(2);
get2(2);
get3(2).then((s) => print("get3 then"));
get4(2);
print("main2");
}
Future<void> get1(int s) async {
print("get1:$s");
await doSth(s);
print("get1 e:$s");
}
Future<void> get3(int s) async {
print("get3:$s");
await doSth(s);
print("get3 e:$s");
}
Future<void> get4(int s) async {
print("get4:$s");
await doSth(s);
print("get4 e:$s");
}
Future<void> get2(int s) async {
print("get2:$s");
await doSth(s);
print("get2 e:$s");
}
Future<void> doSth(int i) async {
print("doSth:$i");
await Future.delayed(Duration(seconds: i));
print("doSth e:$i");
}
main1
get1:2
doSth:2
get2:2
doSth:2
get3:2
doSth:2
get4:2
doSth:2
main2
get1 e:2
doSth e:2
get2 e:2
doSth e:2
get3 e:2
get3 then
doSth e:2
get4 e:2
doSth e:2
逻辑拆解:为什么顺序是这样?
理解这个输出的关键在于:当代码运行到 await 时,函数会立即“暂停”并返回一个 Future 给调用者,剩下的代码被送入队列。
第一步:同步“扫荡” (Main Isolate 启动)
-
main1: 最先打印。
-
get1: 执行同步部分 -> 打印
get1:2-> 进入doSth-> 打印doSth:2-> 遇到await Future.delayed。暂停get1。 -
get2: 执行同步部分 -> 打印
get2:2-> 进入doSth-> 打印doSth:2-> 遇到await Future.delayed。暂停get2。 -
get3: 执行同步部分 -> 打印
get3:2-> 进入doSth-> 打印doSth:2-> 遇到await Future.delayed。暂停get3。 -
get4: 执行同步部分 -> 打印
get4:2-> 进入doSth-> 打印doSth:2-> 遇到await Future.delayed。暂停get4。 -
main2: 主线程同步代码全部跑完,最后打印。
第二步:2秒后的异步“收网” (Event Queue)
2秒钟后,四个 Future.delayed 计时器同时到期,它们的回调被依次放入事件队列。
-
doSth(1) 恢复: 执行
await之后的代码 -> 打印doSth e:2。 -
get1 恢复:
doSth执行完了,get1继续往下走 -> 打印get1 e:2。 -
doSth(2) 恢复: 打印
doSth e:2。 -
get2 恢复: 打印
get2 e:2。 -
...以此类推,直到 get3 打印
get3 e:2后,触发了它的回调.then,打印get3 then。
核心知识点总结
-
异步函数的同步性:
async函数在遇到第一个await关键字之前的代码完全是同步执行的,不会有任何延迟。 -
链式触发:
await doSth(s)之后的代码(例如print("get1 e:2"))必须等待doSth彻底完成后,作为一个微任务(Microtask)被排进队列执行。 -
.then的时机:get3(...).then()的内容会在get3函数体内所有代码执行完毕并返回结果后立即触发。
追问2:修改get1逻辑后的执行循序
Future<void> get1(int s) async {
print("get1:$s");
await doSth(s);
doSth(s);
print("get1 e:$s");
}
main1
get1:2
doSth:2
get2:2
doSth:2
get3:2
doSth:2
get4:2
doSth:2
main2
doSth e:2
get1: doSth:2 <-- 第二次调用 doSth,它是同步开始的
get1 e:2 <-- 因为没加 await,它直接打印了 e
doSth e:2
get2 e:2
doSth e:2
get3 e:2
get3 then
doSth e:2
get4 e:2
doSth e:2 <-- 这个是 get1 里面第二次调用的 doSth 的结尾
深度解析:发生了什么?
这里的核心在于 get1 函数体内的这两行:
await doSth(s); // 1. 挂起,等待 2 秒
doSth(s); // 2. 启动,但不等待
print("get1 e:$s"); // 3. 紧接着第 2 步执行
1. 2秒钟后的“连锁反应”
当第一个 await doSth(s) 到期时:
-
Event Loop 执行完
doSth剩余的代码(打印doSth e:2)。 -
回到
get1继续执行。
2. “火上浇油”的同步调用
-
get1执行第二行doSth(s)。 -
记住原则:
async函数在遇到第一个await之前是同步的。 -
所以它立刻跳进
doSth打印了doSth:2。 -
然后
doSth内部遇到了await Future.delayed,于是这个新的doSth任务被丢进 Event Queue。 -
关键点:由于
get1这一行没写await,它不等了,直接跑下一行打印get1 e:2。
3. 最后的长尾
-
其他的
get2,get3,get4依次恢复执行。 -
大约在整个程序运行的 第 4 秒(2s + 2s),最后那个没被等待的
doSth才会打印出它的doSth e:2。
总结:await 与不 await 的区别
-
await doSth(): “请去煮咖啡,我在这等着,咖啡好了我再干活。” -
doSth(): “去煮咖啡吧,我接着看我的报纸了(print),咖啡什么时候好随它便。”
这就是为什么 get1 e:2 会出现在其他 get 函数执行完之前的原因——它“插队”完成了。
追问3:有一个异步方法,调用它的地方是某一个方法的末尾,此时await和不await有什么区别
这是一个非常经典的问题。在方法的末尾,代码看似已经“结束”了,但 await 和不 await 会在 异常捕获、生命周期、以及函数返回时机 上产生巨大的区别。
我们可以把不带 await 的调用称为 "Fire and Forget"(发射后不管)。
1. 核心区别对比
| 特性 | 使用 await | 不使用 await |
| 执行顺序 | 当前方法会“暂停”,等待异步任务彻底完成后才结束。 | 当前方法立即结束,异步任务在后台继续跑。 |
| 异常捕获 | 可以通过 try-catch 捕获到异步任务内部的错误。 |
捕获不到。错误会抛向全局,可能导致应用崩溃。 |
| 生命周期 | 保证了任务在方法返回前完成。 | 任务可能在方法所属的对象销毁后还在运行(风险点)。 |
| 返回类型 | 调用者会等待这个方法的 Future 完成。 |
调用者会立即收到结果,不管那个异步任务。 |
2. 场景演示:异常捕获(最危险的区别)
如果在方法末尾调用一个可能报错的任务:
情况 A:使用 await
Future<void> saveAndLog() async {
try {
await uploadData();
await logAction(); // 在末尾 await
} catch (e) {
print("捕获到了错误: $e"); // 如果 logAction 出错,这里能抓到
}
}
情况 B:不使用 await
Future<void> saveAndLog() async {
try {
await uploadData();
logAction(); // 发射后不管
} catch (e) {
print("捕获不到 logAction 的错误");
}
}
// 如果 logAction 报错,它会变成一个 "Uncaught Error",
// try-catch 无法拦截,因为 saveAndLog 此时已经跑完了。
3. Flutter 特有风险:内存泄露与 Context
在 Flutter UI 开发中,如果你在方法末尾不 await 且该任务涉及 context 或 setState,会非常危险。
void _onButtonPressed(BuildContext context) async {
await doMainTask();
// 如果不 await 且任务很慢
updateDatabase();
// 假设用户在 updateDatabase 完成前关闭了页面
// updateDatabase 内部如果有引用 context,可能会报错或造成内存泄露
}
4. 什么时候可以不 await?
虽然大部分时候建议显式等待,但以下情况可以选择不 await:
-
完全解耦的任务:比如“打点监控”、“统计上传”,主流程不关心它们是否成功。
-
性能优化:如果你确定后续操作不需要该结果,且任务是安全的。
-
避免死锁:某些极端的链式调用场景。
最佳实践提示: > 如果你决定不
await,建议至少链上一个.catchError(),防止异步错误导致 App 闪退:
logAction().catchError((e) => print(e));
总结:你的意图是什么?
-
如果你希望 确保任务一定完成 或者 需要处理潜在的报错,请务必
await。 -
如果你希望 UI 立即响应,且该异步任务是一个“后台副作用”,可以不
await,但要做好错误处理。
更多推荐



所有评论(0)