Flutter 基础体系 · 第 3/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Dart 异步与并发:Future、Stream、Event Loop、Isolate 和取消

Dart 同时提供两套容易混淆、但解决层次不同的能力:

  • 异步:任务不能立即完成,当前代码先让出执行权,稍后通过 FutureStream 获得结果。
  • 并发:多个计算任务在时间上重叠执行。它们可以发生在同一个线程的事件循环中,也可以发生在多个 Isolate 中。
  • 并行:多个计算任务同时占用不同的 CPU 执行资源。Dart 中通常通过多个 Isolate 实现,而不是在线程之间共享可变内存。

FutureStream 主要描述异步结果,Event Loop 决定这些结果何时被调度,Isolate 提供相互隔离的执行环境,取消则解决“任务已经开始后,如何停止继续等待或继续计算”的问题。它们不是互相替代的概念。


一、先建立执行模型:Dart 代码到底在哪里运行

在 Dart Native 和 Flutter 移动端、桌面端中,一个 Isolate 通常拥有:

  1. 一块独立的堆内存;
  2. 一个执行线程;
  3. 一个事件循环;
  4. 一个微任务队列;
  5. 一个事件队列。

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

关键顺序是:

  1. 先执行当前同步调用栈;
  2. 调用栈清空后,连续处理微任务队列;
  3. 微任务队列为空后,才取一个事件队列事件;
  4. 事件执行完,再次清空微任务队列;
  5. 循环往复。

因此,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') 是同步代码;
  • scheduleMicrotaskFuture.microtask 把回调放入微任务队列;
  • Future(() {}) 把回调安排为稍后处理的事件;
  • 当前同步调用栈结束后,先清空微任务队列,再处理事件队列。

微任务适合安排“当前事件完成后、下一个事件开始前”的短回调,例如完成 Future 的后续处理。它不适合执行耗时循环:

void starveEventLoop() {
  scheduleMicrotask(() {
    starveEventLoop();
  });

  Timer.run(() {
    print('这个回调可能永远得不到执行');
  });
}

这个程序不断向微任务队列添加新任务。因为事件循环必须先清空微任务队列,Timer.run 对应的事件可能一直无法执行,造成事件饥饿。微任务本身不是“更快的线程”,也没有自动抢占能力。

asyncawait 的真实含义

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() 失败:

  1. fetchUser() 返回失败的 Future<User>
  2. await fetchUser() 抛出异常;
  3. readUserName() 没有捕获该异常,因此返回失败的 Future<String>
  4. 调用者可以通过 try/catchcatchError 处理。

更推荐在异步函数中使用结构化的 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> 表示随时间产生零个、一个或多个事件的异步序列。事件有三类:

  1. 数据事件:T
  2. 错误事件:异常和堆栈;
  3. 完成事件:表示不会再有数据。

可以把 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');
}

执行过程是:

  1. count() 创建一个 Stream;
  2. await for 订阅该 Stream;
  3. async* 执行到第一个 yield,发送 1
  4. 消费者处理 1 后,生产者继续执行;
  5. 发送 23
  6. 生成器结束,Stream 发出完成事件;
  7. 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();

这里存在三个不同生命周期:

  1. StreamController 的生命周期;
  2. StreamSubscription 的生命周期;
  3. 生产者底层资源的生命周期。

取消订阅不一定等于关闭控制器。关闭控制器也不一定等于取消所有外部资源,具体取决于生产者实现。

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 管理异步资源

一个生产者可能需要在“第一个订阅者出现”时启动,在“最后一个订阅者取消”时停止。可以使用 onListenonCancel

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:ioHttpClientRequest 为例,关闭请求可以中断请求:

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 之间:

  • 不共享普通可变对象;
  • 不能直接访问对方的变量;
  • 通过 SendPortReceivePort 传递消息;
  • 消息必须满足 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);
}

执行路径是:

  1. 主 Isolate 创建一个新的计算 Isolate;
  2. 将闭包和输入数据发送过去;
  3. 新 Isolate 执行循环;
  4. int 结果发送回主 Isolate;
  5. 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 上可能退化为当前事件循环中的执行。

因此,跨平台代码不能仅凭 Futurecompute 的名称推断“已经后台线程化”。如果某个计算在 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、文件或控制器,取消订阅只代表当前消费者离开。生产者是否停止,要看 onCancelonPause、底层资源关闭逻辑和对象 dispose 是否完整实现。

误区四:用 Future.wait 就会取消其他任务

Future.wait 只组合结果。一个 Future 失败后,其他 Future 可能仍在运行。若业务要求“任一请求失败就取消其余请求”,必须为每个请求提供显式取消协议,并在失败路径中逐个触发取消。

误区五:把大量消息发送给 Isolate 就一定更快

Isolate 有启动成本、数据传输成本和序列化成本。若任务很小,通信成本可能超过计算本身。正确的验证方式是:

  1. 在目标设备上测量主 Isolate 执行时间;
  2. 测量 Isolate 启动和消息传输时间;
  3. 测量重复任务使用长生命周期 worker 的成本;
  4. 观察 UI 帧耗时和内存峰值;
  5. 再决定是否迁移。

不要根据桌面开发机上的单次测量推断低端 Android 设备表现。


十二、一个完整的异步任务生命周期

可以把生产环境中的任务抽象为以下状态:

Created
  │
  ▼
Running ───────────────┐
  │                    │
  ├── Success          │
  ├── Failure          │
  └── CancelRequested  │
                         ▼
                    Cancelled

但“请求取消”不一定立即进入 Cancelled

  1. 调用方设置取消标志;
  2. 底层任务在下一个取消点发现标志;
  3. 底层 API 释放网络、文件或 Isolate 资源;
  4. 任务以取消错误或专门状态结束;
  5. UI 根据当前生命周期决定是否显示结果。

如果任务已经完成,之后再调用取消通常没有效果。若结果已经发送给 UI,取消也不能撤回已经执行的 setState;所以异步结果应用前仍需要检查页面状态、请求版本或业务实体版本。


结语

Future 表示一次最终结果,Stream 表示连续事件;它们都运行在某个 Isolate 的事件循环之上。事件循环先执行同步代码,再清空微任务,最后处理事件,因此异步并不自动等于后台线程。CPU 密集型同步计算必须通过 Isolate 或其他平台并行机制迁移,否则仍会阻塞 Flutter UI。

取消不是 Future 自带的魔法。StreamSubscription.cancel()、HTTP 请求关闭、协作式取消 token 和 Isolate.kill() 分别作用于不同资源和不同生命周期。只有把“停止等待”“停止底层任务”“丢弃过期结果”区分开,才能正确设计网络请求、分页、缓存、离线同步以及 Flutter 页面销毁时的异步状态管理。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。