Flutter 基础体系 · 第 3/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。
Dart 异步与并发:Future、Stream、Event Loop、Isolate 和取消
Dart 同时提供两套容易混淆、但解决层次不同的能力:
- 异步:任务不能立即完成,当前代码先让出执行权,稍后通过
Future或Stream获得结果。 - 并发:多个计算任务在时间上重叠执行。它们可以发生在同一个线程的事件循环中,也可以发生在多个 Isolate 中。
- 并行:多个计算任务同时占用不同的 CPU 执行资源。Dart 中通常通过多个 Isolate 实现,而不是在线程之间共享可变内存。
Future 和 Stream 主要描述异步结果,Event Loop 决定这些结果何时被调度,Isolate 提供相互隔离的执行环境,取消则解决“任务已经开始后,如何停止继续等待或继续计算”的问题。它们不是互相替代的概念。
一、先建立执行模型:Dart 代码到底在哪里运行
在 Dart Native 和 Flutter 移动端、桌面端中,一个 Isolate 通常拥有:
- 一块独立的堆内存;
- 一个执行线程;
- 一个事件循环;
- 一个微任务队列;
- 一个事件队列。
Flutter 应用通常首先运行在主 Isolate,也常称为 UI Isolate。Flutter 的界面构建、布局、绘制相关工作都依赖这个 Isolate 的响应能力。
一个简化的调度模型如下:
flowchart TD
A[同步代码] --> B{当前调用栈是否为空}
B -->|否| A
B -->|是| C[清空 Microtask Queue]
C --> D{仍有微任务}
D -->|是| C
D -->|否| E[取一个 Event Queue 事件]
E --> A
关键顺序是:
- 先执行当前同步调用栈;
- 调用栈清空后,连续处理微任务队列;
- 微任务队列为空后,才取一个事件队列事件;
- 事件执行完,再次清空微任务队列;
- 循环往复。
因此,async 不等于“自动创建新线程”。如果异步函数在等待 I/O,它会暂时让出当前 Isolate;如果异步函数执行的是大量同步计算,它仍然会阻塞当前 Isolate。
微任务和事件任务
下面的程序可以在 Dart 命令行项目中运行:
import 'dart:async';
void main() {
print('A');
scheduleMicrotask(() {
print('microtask-1');
});
Future(() {
print('event-future');
});
Future.microtask(() {
print('microtask-2');
});
print('B');
}
常见输出是:
A
B
microtask-1
microtask-2
event-future
原因如下:
print('A')和print('B')是同步代码;scheduleMicrotask和Future.microtask把回调放入微任务队列;Future(() {})把回调安排为稍后处理的事件;- 当前同步调用栈结束后,先清空微任务队列,再处理事件队列。
微任务适合安排“当前事件完成后、下一个事件开始前”的短回调,例如完成 Future 的后续处理。它不适合执行耗时循环:
void starveEventLoop() {
scheduleMicrotask(() {
starveEventLoop();
});
Timer.run(() {
print('这个回调可能永远得不到执行');
});
}
这个程序不断向微任务队列添加新任务。因为事件循环必须先清空微任务队列,Timer.run 对应的事件可能一直无法执行,造成事件饥饿。微任务本身不是“更快的线程”,也没有自动抢占能力。
async 和 await 的真实含义
async 函数返回一个 Future。执行函数时,函数体通常会先同步运行,直到遇到尚未完成的 await:
Future<int> loadValue() async {
print('before await');
final value = await Future<int>.delayed(
const Duration(milliseconds: 10),
() => 42,
);
print('after await');
return value;
}
void main() {
print('start');
final future = loadValue();
print('after call');
future.then((value) {
print(value);
});
}
典型顺序为:
start
before await
after call
after await
42
loadValue() 被调用后,before await 立即执行。遇到未完成的 Future 后,函数暂停并返回一个尚未完成的 Future,调用方继续执行。底层任务完成后,函数从 await 后面恢复,最终完成外层 Future。
如果 await 的对象已经完成,恢复仍然是异步语义,不应依赖“立即同步继续执行”这种实现细节。代码应只依赖 await 的值、异常和控制流语义。
二、Future:一个最终只产生一次结果的异步对象
Future<T> 表示一个将来完成的计算结果:
- 成功完成:得到一个
T; - 失败完成:得到一个异常和堆栈;
- 一个
Future只能完成一次; - 完成后结果不会再次改变。
可以把它看成状态机:
未完成
├── 成功完成(value)
└── 失败完成(error, stackTrace)
Future 不是任务本身。它是任务结果的表示。任务可能来自网络、文件、定时器、平台通道或其他异步源。
成功和失败传播
Future<String> readUserName() async {
final user = await fetchUser();
return user.name;
}
Future<User> fetchUser() async {
throw StateError('服务器返回非法用户数据');
}
如果 fetchUser() 失败:
fetchUser()返回失败的Future<User>;await fetchUser()抛出异常;readUserName()没有捕获该异常,因此返回失败的Future<String>;- 调用者可以通过
try/catch或catchError处理。
更推荐在异步函数中使用结构化的 try/catch:
Future<void> showUser() async {
try {
final name = await readUserName();
print(name);
} on FormatException catch (error, stackTrace) {
print('数据格式错误:$error');
print(stackTrace);
} catch (error, stackTrace) {
print('其他异常:$error');
print(stackTrace);
}
}
捕获异常时保留 StackTrace,否则生产诊断会丢失错误来源。
then 链和异常
以下两种写法表达相同的基本控制流:
final result = await loadData();
loadData().then((result) {
// 使用 result
});
then 回调抛出的异常会使返回的新 Future 失败:
Future<String> normalize() {
return loadData().then((data) {
if (data.isEmpty) {
throw StateError('数据为空');
}
return data.trim();
});
}
如果创建了一个失败的 Future,却没有等待、监听或显式忽略,它可能成为未处理异步异常。对于明确有意不等待的任务,可以使用 unawaited 表达意图:
import 'dart:async';
void startLogging() {
unawaited(writeLog());
}
Future<void> writeLog() async {
// 日志写入逻辑
}
unawaited 不会取消任务,也不会吞掉异常;它只是告诉读代码的人“这里故意不等待”。后台任务仍应自行处理错误。
并行等待多个 Future
如果两个异步任务相互独立,可以同时启动它们,而不是串行等待:
Future<void> loadPage() async {
final profileFuture = fetchProfile();
final settingsFuture = fetchSettings();
final profile = await profileFuture;
final settings = await settingsFuture;
print(profile);
print(settings);
}
这里两个调用在第一次 await 前已经分别启动。若写成:
final profile = await fetchProfile();
final settings = await fetchSettings();
则第二个调用要等第一个完成后才开始,整体耗时通常更长。
也可以使用 Future.wait:
final values = await Future.wait<Object>([
fetchProfile(),
fetchSettings(),
]);
Future.wait 的结果顺序与输入顺序一致,而不是与完成顺序一致。假设:
fetchSettings 完成:先完成
fetchProfile 完成:后完成
结果仍然是:
[profileResult, settingsResult]
默认情况下,任一任务失败会使等待结果失败。其他任务是否已经完成、是否仍在运行,不等于它们会被自动取消。Future.wait 没有通用的底层取消能力。
三、Stream:连续产生事件的异步序列
Stream<T> 表示随时间产生零个、一个或多个事件的异步序列。事件有三类:
- 数据事件:
T; - 错误事件:异常和堆栈;
- 完成事件:表示不会再有数据。
可以把 Stream 看作:
数据 -> 数据 -> 错误或数据 -> 数据 -> 完成
它与 Future 的区别不是“一个快、一个慢”,而是结果基数不同:
Future<T>:最终一个结果;Stream<T>:零个或多个结果。
await for 的消费过程
Stream<int> count() async* {
for (var i = 1; i <= 3; i++) {
await Future<void>.delayed(const Duration(milliseconds: 100));
yield i;
}
}
Future<void> main() async {
await for (final value in count()) {
print(value);
}
print('stream done');
}
执行过程是:
count()创建一个 Stream;await for订阅该 Stream;async*执行到第一个yield,发送1;- 消费者处理
1后,生产者继续执行; - 发送
2、3; - 生成器结束,Stream 发出完成事件;
await for退出。
yield 不只是返回一个值,它会暂停生成器,等待消费者继续推进。对于异步生成器而言,这种结构很适合分页、文件读取、数据库游标和事件转换。
单订阅 Stream 与广播 Stream
单订阅 Stream 只能有一个订阅者,适合一次性数据流,例如:
- 文件内容;
- HTTP 响应字节;
- 一次性的分页读取;
- 一个命令的处理过程。
广播 Stream 可以有多个订阅者,适合事件通知,例如:
- 网络状态变化;
- 用户登录状态变化;
- 应用生命周期事件;
- 全局消息总线。
广播 Stream 通常只把事件发送给“当前已经订阅”的监听者。订阅前产生的事件一般不会自动补发,因此它不等价于一个历史数据仓库。
final controller = StreamController<String>.broadcast();
final subscription = controller.stream.listen(
print,
onError: (Object error, StackTrace stackTrace) {
print('stream error: $error');
},
onDone: () {
print('done');
},
);
controller.add('online');
await subscription.cancel();
await controller.close();
这里存在三个不同生命周期:
StreamController的生命周期;StreamSubscription的生命周期;- 生产者底层资源的生命周期。
取消订阅不一定等于关闭控制器。关闭控制器也不一定等于取消所有外部资源,具体取决于生产者实现。
StreamSubscription 的暂停与取消
final subscription = someStream.listen((event) {
print(event);
});
subscription.pause();
await Future<void>.delayed(const Duration(seconds: 1));
subscription.resume();
await subscription.cancel();
pause() 表示暂时不向这个订阅者交付事件。事件是否缓冲、由谁缓冲,取决于 Stream 的实现和订阅类型;暂停不是免费的无限背压机制。如果生产速度长期高于消费速度,缓冲区可能增长。
cancel() 表示不再接收后续事件。它返回 Future<void>,因为底层资源释放可能本身是异步的:
await subscription.cancel();
不要只调用 cancel() 而忽略其返回值,尤其是在页面销毁、文件关闭或测试清理阶段。
Flutter 页面中的 Stream 生命周期
class UserPageState extends State<UserPage> {
StreamSubscription<User>? _subscription;
@override
void initState() {
super.initState();
_subscription = userStream.listen((user) {
if (!mounted) return;
setState(() {
// 更新页面状态
});
});
}
@override
void dispose() {
unawaited(_subscription?.cancel());
super.dispose();
}
}
mounted 只能防止已经销毁的 State 调用 setState,不能取消网络请求、定时器或 Stream。取消资源和检查 mounted 是两个不同的问题。
四、Stream 的错误、背压和资源释放
Stream 错误是事件序列的一部分。监听时可以提供错误回调:
final subscription = stream.listen(
(value) {
print('data: $value');
},
onError: (Object error, StackTrace stackTrace) {
print('error: $error');
},
onDone: () {
print('completed');
},
);
如果使用 await for,可以通过 try/catch 捕获 Stream 错误:
Future<void> consume(Stream<int> stream) async {
try {
await for (final value in stream) {
print(value);
}
} catch (error, stackTrace) {
print('消费失败:$error');
print(stackTrace);
}
}
Stream 出错后是否继续发送数据,取决于生产者是否继续发送。某些 Stream 发送一个错误后仍会继续,另一些会在错误后结束。不要假设“出现错误就一定自动完成”。
用 StreamController 管理异步资源
一个生产者可能需要在“第一个订阅者出现”时启动,在“最后一个订阅者取消”时停止。可以使用 onListen 和 onCancel:
class Ticker {
final _controller = StreamController<DateTime>.broadcast();
Timer? _timer;
Ticker() {
_controller
..onListen = () {
_timer ??= Timer.periodic(
const Duration(seconds: 1),
(_) => _controller.add(DateTime.now()),
);
}
..onCancel = () {
_timer?.cancel();
_timer = null;
};
}
Stream<DateTime> get stream => _controller.stream;
Future<void> dispose() async {
_timer?.cancel();
await _controller.close();
}
}
这里取消最后一个订阅会停止定时器,但不会关闭控制器。只有明确调用 dispose() 才会结束整个对象。若该对象长期存在且忘记 dispose(),定时器和订阅关系可能造成资源泄漏。
默认 StreamController 通常采用异步派发,生产者调用 add 后,监听回调不会在同一个 add 调用栈中直接重入。sync: true 可以改变这一点,但会引入重入和状态顺序风险,除非明确需要同步派发,否则不应随意使用。
五、取消:Dart 不会自动取消 Future
这是异步代码中最重要的边界之一:
Future本身没有通用的取消协议。
下面的代码不会取消底层任务:
final result = await future.timeout(
const Duration(seconds: 2),
);
timeout 只限制“等待这个 Future 的调用方多久”。超时后,调用方可以得到 TimeoutException,但原始 future 可能仍在执行,并且底层网络请求、文件读取或计算仍可能占用资源。
因此要区分:
- 停止等待:调用方不再等待结果;
- 停止任务:底层任务主动释放资源并停止工作;
- 丢弃结果:任务完成后不再使用其结果。
Future.timeout 主要完成第一件事。
可取消 Future 的基本结构
取消必须由任务的底层 API 支持,或者由任务自己配合检查取消状态。下面是一个协作式取消示例:
class CancelToken {
bool _isCancelled = false;
bool get isCancelled => _isCancelled;
void cancel() {
_isCancelled = true;
}
void throwIfCancelled() {
if (_isCancelled) {
throw const CancelledException();
}
}
}
class CancelledException implements Exception {
const CancelledException();
@override
String toString() => 'Operation cancelled';
}
Future<List<int>> calculateInChunks(CancelToken token) async {
final result = <int>[];
for (var chunk = 0; chunk < 100; chunk++) {
token.throwIfCancelled();
// 模拟一段异步工作,让出事件循环。
await Future<void>.delayed(const Duration(milliseconds: 10));
for (var i = 0; i < 1000; i++) {
result.add(chunk * 1000 + i);
}
}
return result;
}
Future<void> example() async {
final token = CancelToken();
final future = calculateInChunks(token);
await Future<void>.delayed(const Duration(milliseconds: 100));
token.cancel();
try {
await future;
} on CancelledException {
print('任务已协作式取消');
}
}
取消点必须真实存在。若一次循环内部执行几秒钟的纯同步计算,期间没有检查 token,也没有让出事件循环,那么调用 cancel() 的代码甚至没有机会运行。
网络请求中的取消
HTTP 库的取消能力属于具体 API,而不是 Future 的通用能力。以 dart:io 的 HttpClientRequest 为例,关闭请求可以中断请求:
import 'dart:async';
import 'dart:convert';
import 'dart:io';
Future<String> getText(
Uri uri, {
required Duration timeout,
}) async {
final client = HttpClient();
try {
final request = await client.getUrl(uri).timeout(timeout);
final response = await request.close().timeout(timeout);
return await response
.transform(utf8.decoder)
.join()
.timeout(timeout);
} finally {
client.close(force: true);
}
}
这个例子在超时时会停止等待,但不同阶段的底层取消行为仍取决于当前 API 调用和 HttpClient 的关闭时机。生产代码通常需要保留 HttpClientRequest 或 HTTP 客户端库提供的请求句柄,在页面销毁或用户主动取消时调用该库规定的取消方法。
Android、iOS、桌面端可以使用 dart:io,但 Flutter Web 不能使用 dart:io。Web 通常依赖浏览器 Fetch、XHR 或第三方 HTTP 库,其取消一般通过浏览器的 AbortController 或库自身的 cancel token 实现,不能把 Native 的 HttpClient API 直接带到 Web。
页面销毁时的竞态
class SearchPageState extends State<SearchPage> {
int _requestVersion = 0;
Future<void> search(String keyword) async {
final version = ++_requestVersion;
try {
final result = await repository.search(keyword);
if (!mounted || version != _requestVersion) {
return;
}
setState(() {
// 只应用当前请求的结果
});
} catch (error) {
if (!mounted || version != _requestVersion) {
return;
}
// 显示错误
}
}
}
这里的版本号解决的是“旧请求晚于新请求完成”的结果覆盖问题。它不是取消:旧请求仍可能继续消耗网络资源。若仓库层支持取消,应同时取消旧请求;版本号则作为结果校验的最后一道保护。
六、Event Loop 不会自动解决 CPU 密集型任务
下面的代码虽然写在 async 函数中,但仍然会阻塞 UI Isolate:
Future<int> badHash(List<int> data) async {
var result = 0;
for (final byte in data) {
result = (result * 31 + byte) & 0x7fffffff;
}
return result;
}
函数没有遇到真正未完成的 await。即使声明了 async,循环仍在当前 Isolate 同步执行。结果是:
- Flutter 无法及时处理触摸事件;
- 帧构建可能延迟;
- 动画卡顿;
- Android 或 iOS 上可能出现明显的界面冻结。
Future.delayed 也不能把计算搬到后台:
await Future<void>.delayed(Duration.zero);
heavyCalculation();
它只是在执行一次事件循环让出后,仍然回到同一个 Isolate 执行 heavyCalculation()。
七、Isolate:通过独立内存实现并发
Isolate 是 Dart 的独立执行单元。两个 Isolate 之间:
- 不共享普通可变对象;
- 不能直接访问对方的变量;
- 通过
SendPort和ReceivePort传递消息; - 消息必须满足 Dart 的可发送对象约束;
- 通信本身也有序列化、复制或传输成本。
这与共享内存线程模型不同。Isolate 的好处是降低共享可变状态导致的数据竞争;代价是消息通信和数据转换成本。
使用 Isolate.run 执行一次性计算
在支持 dart:isolate 的 Dart Native 环境中,可以这样执行一次性任务:
import 'dart:isolate';
Future<int> calculateChecksum(List<int> bytes) {
return Isolate.run(() {
var checksum = 0;
for (final byte in bytes) {
checksum = (checksum * 31 + byte) & 0x7fffffff;
}
return checksum;
});
}
Future<void> main() async {
final checksum = await calculateChecksum(List<int>.generate(1000000, (i) => i % 256));
print(checksum);
}
执行路径是:
- 主 Isolate 创建一个新的计算 Isolate;
- 将闭包和输入数据发送过去;
- 新 Isolate 执行循环;
- 将
int结果发送回主 Isolate; Isolate.run返回完成的Future<int>。
Isolate.run 适合短生命周期、一次性、结果可传输的计算。输入和结果不能依赖主 Isolate 中不可发送的对象,例如打开的文件句柄、UI 对象、某些带原生资源的实例。
Flutter 中常见的 compute 也用于把计算放到后台 Isolate,但它具有 Flutter API 自身的约束,例如回调通常应是顶层函数或静态函数,并且参数和返回值必须可发送。Flutter Web 上,compute 的具体实现可能在当前事件循环执行,而不是创建真正的后台 Isolate;因此不能把 Web 上的 compute 当作稳定的多线程保证。
长生命周期 Isolate 与消息端口
需要持续处理多个任务时,可以创建长生命周期 Isolate:
import 'dart:isolate';
class Worker {
late final Isolate _isolate;
late final SendPort _sendPort;
final _responses = <int, Completer<int>>{};
int _nextId = 0;
Future<void> start() async {
final readyPort = ReceivePort();
_isolate = await Isolate.spawn(
_workerMain,
readyPort.sendPort,
);
_sendPort = await readyPort.first as SendPort;
}
Future<int> calculate(int input) {
final id = _nextId++;
final completer = Completer<int>();
_responses[id] = completer;
_sendPort.send(<Object>[id, input]);
return completer.future;
}
void stop() {
_isolate.kill(priority: Isolate.immediate);
for (final completer in _responses.values) {
if (!completer.isCompleted) {
completer.completeError(const WorkerStoppedException());
}
}
_responses.clear();
}
}
void _workerMain(SendPort parentPort) {
final requests = ReceivePort();
parentPort.send(requests.sendPort);
requests.listen((message) {
final values = message as List<Object>;
final id = values[0] as int;
final input = values[1] as int;
final result = input * input;
// 实际代码还需要把结果发送回主 Isolate。
// 这里省略完整协议会导致 Worker 无法完成 calculate。
});
}
class WorkerStoppedException implements Exception {
const WorkerStoppedException();
}
这个示例故意展示了长生命周期协议的复杂性:启动握手、请求 ID、响应端口、错误消息、停止时完成挂起的请求,都必须设计。不能只调用 Isolate.spawn 就认为请求已经可用。
一个完整的协议通常包含:
主 Isolate -> worker:ready / request(id, payload) / cancel(id) / stop
worker -> 主 Isolate:result(id, value) / error(id, error) / stopped
如果 worker 正在执行不可中断的同步循环,主 Isolate 发送 cancel(id) 后,worker 只有在下一次检查取消标志时才能停止。若直接 isolate.kill(),可以强制终止整个 Isolate,但会丢失其中所有任务的中间状态和未发送结果。
八、Isolate 的取消和资源边界
Isolate 取消有两种语义:
协作式取消
主 Isolate 发送取消消息:
class WorkRequest {
final int id;
final int input;
const WorkRequest(this.id, this.input);
}
worker 在循环中定期检查取消集合:
void workerLoop(ReceivePort port, SendPort parent) {
final cancelled = <int>{};
port.listen((message) {
if (message is List && message.first == 'cancel') {
cancelled.add(message[1] as int);
return;
}
if (message is List && message.first == 'work') {
final id = message[1] as int;
final input = message[2] as int;
var result = 0;
for (var i = 0; i < input; i++) {
if (cancelled.contains(id)) {
parent.send(<Object>['cancelled', id]);
return;
}
result += i;
}
parent.send(<Object>['result', id, result]);
}
});
}
这种方式可以执行清理逻辑,但取消延迟取决于检查频率。检查过于频繁会增加开销,检查过少则响应慢。
强制终止
isolate.kill(priority: Isolate.immediate);
强制终止会停止整个 Isolate,而不是只停止一个请求。它适合 worker 已经失控、无法继续协作的故障路径,但可能导致:
- 未发送的结果丢失;
- 文件、数据库事务或原生资源无法按业务预期清理;
- 主 Isolate 中所有挂起请求都必须由上层显式标记为失败或取消。
因此,kill 不是普通的请求取消 API。
九、平台差异:Android、iOS、桌面和 Web
Android、iOS 和桌面端
在 Flutter 的 Native 平台上:
- UI 通常运行在主 Isolate;
- 可以使用
dart:isolate; dart:io可用于文件和网络;- Isolate 适合 CPU 密集型、可序列化的数据处理;
- 原生插件调用通常需要遵守插件自身的线程和 Isolate 约束。
不是所有插件都能在任意后台 Isolate 中直接调用。插件可能依赖主 Isolate 的二进制消息信道、Flutter 引擎绑定或平台线程。把 JSON 解析、图片处理、加密等纯 Dart 计算放到 Isolate,通常比把任意插件调用搬过去更安全。
Flutter Web
Web 平台受浏览器运行模型限制:
dart:io不可用;- Dart 代码通常运行在浏览器 JavaScript 事件循环中;
dart:isolate的 Native 能力不能直接照搬;- 浏览器 Web Worker 可以提供并行计算,但需要通过 Web 平台机制或专门方案接入;
- Flutter 的某些跨平台 API 在 Web 上可能退化为当前事件循环中的执行。
因此,跨平台代码不能仅凭 Future 或 compute 的名称推断“已经后台线程化”。如果某个计算在 Web 上也必须不阻塞主线程,应验证当前 Flutter/Dart 版本和具体 API 的 Web 实现,必要时使用 Web Worker 方案。
十、Future、Stream、Isolate 的组合方式
一个真实的数据层通常同时使用这些抽象:
页面生命周期
│
├── 取消旧请求
├── 发起 Future HTTP 请求
├── 超时或底层取消
├── JSON 解析
│ └── 数据量大时交给 Isolate
└── 将状态变化暴露为 Stream
例如,仓库层可以把一次网络请求表示为 Future<Page<T>>,而把持续的缓存状态表示为 Stream<List<T>>:
abstract interface class UserRepository {
Future<List<User>> fetchPage({
required int page,
required CancelToken token,
});
Stream<List<User>> watchCachedUsers();
}
这里的选择依据是:
- “第 1 页最终返回什么?”使用
Future; - “缓存变化时持续通知什么?”使用
Stream; - “解析一百万条记录是否阻塞 UI?”考虑
Isolate; - “页面离开后是否仍需请求?”由取消协议和业务生命周期决定;
- “结果是否可能过期?”使用请求版本、缓存版本或服务端游标校验。
不要把所有异步操作都包装成 Stream。一个只返回一次的 HTTP 请求使用 Future 更能表达其生命周期;也不要把 Stream 当作自动缓存,它是否重放历史事件取决于具体实现。
十一、常见误区与诊断方法
误区一:async 就是后台线程
错误表现是界面在 async 方法中仍然卡顿。诊断方法是查找:
- 是否存在长时间同步循环;
await等待的是否只是Duration.zero;- JSON 解码、排序、压缩或图片处理是否在主 Isolate 同步执行;
- 是否把大量工作放进了微任务队列。
解决方向不是增加 async,而是拆分计算、优化算法,或把适合的数据处理移到 Native Isolate。
误区二:Future.timeout 会取消 HTTP 请求
错误表现是页面已经显示超时,但网络请求仍在后台继续,甚至稍后触发日志、缓存写入或资源占用。
诊断时要区分:
调用方超时
≠
底层请求已关闭
≠
服务端已停止处理
要检查底层客户端是否提供请求句柄、取消 token 或关闭方法,并在超时、页面销毁、用户主动取消时调用它。
误区三:取消 Stream 就会关闭生产者
如果一个 Stream 来自定时器、Socket、文件或控制器,取消订阅只代表当前消费者离开。生产者是否停止,要看 onCancel、onPause、底层资源关闭逻辑和对象 dispose 是否完整实现。
误区四:用 Future.wait 就会取消其他任务
Future.wait 只组合结果。一个 Future 失败后,其他 Future 可能仍在运行。若业务要求“任一请求失败就取消其余请求”,必须为每个请求提供显式取消协议,并在失败路径中逐个触发取消。
误区五:把大量消息发送给 Isolate 就一定更快
Isolate 有启动成本、数据传输成本和序列化成本。若任务很小,通信成本可能超过计算本身。正确的验证方式是:
- 在目标设备上测量主 Isolate 执行时间;
- 测量 Isolate 启动和消息传输时间;
- 测量重复任务使用长生命周期 worker 的成本;
- 观察 UI 帧耗时和内存峰值;
- 再决定是否迁移。
不要根据桌面开发机上的单次测量推断低端 Android 设备表现。
十二、一个完整的异步任务生命周期
可以把生产环境中的任务抽象为以下状态:
Created
│
▼
Running ───────────────┐
│ │
├── Success │
├── Failure │
└── CancelRequested │
▼
Cancelled
但“请求取消”不一定立即进入 Cancelled:
- 调用方设置取消标志;
- 底层任务在下一个取消点发现标志;
- 底层 API 释放网络、文件或 Isolate 资源;
- 任务以取消错误或专门状态结束;
- UI 根据当前生命周期决定是否显示结果。
如果任务已经完成,之后再调用取消通常没有效果。若结果已经发送给 UI,取消也不能撤回已经执行的 setState;所以异步结果应用前仍需要检查页面状态、请求版本或业务实体版本。
结语
Future 表示一次最终结果,Stream 表示连续事件;它们都运行在某个 Isolate 的事件循环之上。事件循环先执行同步代码,再清空微任务,最后处理事件,因此异步并不自动等于后台线程。CPU 密集型同步计算必须通过 Isolate 或其他平台并行机制迁移,否则仍会阻塞 Flutter UI。
取消不是 Future 自带的魔法。StreamSubscription.cancel()、HTTP 请求关闭、协作式取消 token 和 Isolate.kill() 分别作用于不同资源和不同生命周期。只有把“停止等待”“停止底层任务”“丢弃过期结果”区分开,才能正确设计网络请求、分页、缓存、离线同步以及 Flutter 页面销毁时的异步状态管理。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 上一篇:Dart 3 语言基础:类型、空安全、模式、类、Mixin 与扩展
- 下一篇:Flutter 项目工具链:SDK、Pub、Flavor、代码生成和环境配置
- 延伸:Flutter 网络与数据层:HTTP、序列化、取消、缓存、分页和离线
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论