烧脑时刻:Dart 中异步生成器与流

有这么一段代码:

dart 复制代码
Stream<int> countStream(int max) async* {
    for (int i = 0; i < max; i++) {
        yield i;
    }
}

这段代码定义了一个异步生成器函数,用来产生一个整数序列的流(Stream)。我会从基础到核心,帮你把这段代码拆解清楚。

代码整体功能

这段代码定义了一个名为 countStream 的函数,它接收一个整数参数 max,返回一个 Stream<int>(整数类型的流)。这个流会异步地、逐个地生成从 0max-1 的整数序列(比如传入 max=3,会依次产生 012)。

逐行拆解解释

dart 复制代码
// 函数返回值是 Stream<int>:表示这是一个能产生整数的"数据流"
Stream<int> countStream(int max) async* {
    // 普通的 for 循环,从 0 开始,到 max-1 结束
    for (int i = 0; i < max; i++) {
        // 核心关键字:yield,作用是"产出"一个值到流中
        yield i;
    }
}

核心概念解释

Stream<int>:流

你可以把 Stream 理解成一个异步的、可迭代的 "管道"

  • 普通的 List 是一次性把所有数据(比如 [0,1,2])放在一个容器里给你;
  • 而 Stream 是把数据逐个、异步地通过管道传给你(先传 0,再传 1,再传 2),适合处理需要 "边产生边消费" 的场景(比如网络请求、文件读取、实时数据)。

async*:异步生成器函数

  • async:标记函数是异步的,但 async* 是 Dart 专门用于生成 Stream的异步生成器语法(区别于普通异步函数的 async);
  • *:表示这是一个 "生成器" 函数,作用是持续产生值,而不是只返回一个值。

yield:产出值到流中

yield 是生成器的核心关键字:

  • 当执行到yield i时,函数会把当前的i值 "发送" 到 Stream 中,供监听者接收;
  • 发送后函数不会结束,而是暂停在这里,等下一次被请求时继续执行循环(直到循环结束,Stream 才会关闭)。

实际使用示例

为了让你更直观理解,我写一个完整的使用示例,你可以直接运行:

dart 复制代码
void main() async {
  // 调用函数,得到一个流(此时流还没开始产生数据)
  Stream<int> stream = countStream(3);
  
  // 监听流,接收每一个产出的值
  await for (int value in stream) {
    print('收到值:$value');
  }
  
  print('流已结束');
}

// 你提供的原函数
Stream<int> countStream(int max) async* {
  for (int i = 0; i < max; i++) {
    yield i;
  }
}

运行结果:

dart 复制代码
收到值:0
收到值:1
收到值:2
流已结束
  • countStream 是一个异步生成器函数async*),返回一个整数类型的流(Stream<int>);
  • yield 关键字负责把循环中的每个整数逐个 "发送" 到流中,而非一次性返回所有值;
  • 流的特点是异步、逐个消费,需要通过 await forlisten 来监听和接收值。

优点

Dart 异步生成器(async* + yield)返回 Stream 的方式来生成数据序列,相比直接返回列表(List)等其他方式,具体有哪些优势。我会结合实际场景,把这些好处讲得通俗易懂。

极致的内存效率:避免一次性加载所有数据

dart 复制代码
// 危险:max=1亿时会直接卡崩/内存溢出
List<int> countList(int max) {
  return List.generate(max, (i) => i);
}

// 安全:max=1亿也能正常运行(只要消费端能处理)
Stream<int> countStream(int max) async* {
  for (int i = 0; i < max; i++) {
    yield i;
  }
}

这是最核心的好处。

  • 如果用 List<int> countList(int max),当 max 非常大(比如 100 万、1 亿)时,会一次性在内存中创建包含所有元素的列表,瞬间占用大量内存,甚至可能导致内存溢出(OOM)。
  • Stream + yield 的方式:每次只生成一个值并发送,内存中始终只保留当前的 i(单个整数),无论 max 多大,内存占用几乎可以忽略。

异步非阻塞:不卡主线程,支持耗时操作

Stream 是异步的,生成数据的过程可以包含耗时操作(比如网络请求、文件读取),且不会阻塞主线程;而普通列表是同步生成,耗时操作会卡住整个程序。

实时响应:边生产边消费,提升交互体验

Stream 是「流式处理」,生成一个值就可以立即消费一个值,无需等待所有数据生成完毕。

  • 比如做一个「实时计数展示」的功能:用 Stream 可以每生成一个数就立刻更新 UI,用户能看到计数逐步增加;
  • 用列表的话,必须等所有数生成完,才能一次性展示,用户会看到长时间的空白,体验极差。

缺点

手动管理成本极高,极易引发内存泄漏

这是最致命的缺点,也是实际项目中最容易踩的坑。

  • 问题本质:async* 生成的 Stream 订阅后,必须手动调用 subscription.cancel() 取消订阅(比如在页面 dispose 生命周期中);如果忘记取消,async* 函数会持续执行(比如倒计时循环、分页请求),导致内存泄漏(页面销毁后仍占用资源),甚至引发空指针异常(订阅回调中操作已销毁的 Widget)。
  • 对比主流方案:Bloc/Riverpod/Provider 会自动绑定 Widget 生命周期,页面销毁时自动终止状态更新;ValueNotifier 也无需手动取消监听,GC 会自动处理。

示例:

dart 复制代码
class BadStreamPage extends StatefulWidget {
  const BadStreamPage({super.key});

  @override
  State<BadStreamPage> createState() => _BadStreamPageState();
}

class _BadStreamPageState extends State<BadStreamPage> {
  StreamSubscription<int>? _subscription;

  @override
  void initState() {
    super.initState();
    // 订阅倒计时流,但忘记在 dispose 中取消
    _subscription = countStream(60).listen((i) {
      print('倒计时:$i'); // 页面销毁后仍会打印,内存泄漏
    });
  }

  // 忘记重写 dispose 取消订阅 → 内存泄漏!
  // @override
  // void dispose() {
  //   _subscription?.cancel(); // 必须手动取消
  //   super.dispose();
  // }

  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('倒计时页面'));
  }
}

调试体验差,难以追踪数据流转

原生 Stream + async* 缺乏配套的调试工具,排查问题成本极高。

  • 问题 1:无法直观看到 yield 的调用时机、数据值,只能靠 print 日志调试;
  • 问题 2:无法追踪 Stream 的订阅 / 取消状态,排查内存泄漏时只能靠猜;
  • 对比主流方案:BlocBlocObserver 可全局监控所有状态 emit操作,Flutter DevTools 能可视化 Riverpod/Provider 的状态变化,调试效率提升 10 倍。

认知与协作成本高,团队易出分歧

async*yieldStream 属于 Dart 进阶语法,而非基础语法:

  • 新手理解成本高(比如分不清 async vs async*yield vs return),容易写出逻辑错误的代码;
  • 团队协作时,部分开发者用原生 Stream,部分用 Bloc/Riverpod,代码风格不统一,维护成本飙升;
  • 对比主流方案:setStateFutureProviderFlutter 入门必学内容,所有开发者都能快速上手。

这些缺点决定了它只适合「底层流式处理、大数据量、可中断序列」等小众高需求场景。

典型场景

实时、连续的「原生数据流」处理(不可替代)

这是最核心的高需求场景 ------ 当你需要处理持续产生、无固定终点的实时数据时,async* + Stream 是唯一简洁且高效的选择。

典型业务场景:

  • 蓝牙 / BLE / 串口通信(实时接收设备数据,比如手环心率、智能家居传感器);
  • 传感器数据(手机加速度计、陀螺仪、GPS 实时定位);
  • WebSocket/SSE 推送(实时聊天、行情刷新、消息通知);
  • 实时日志监控(APP 运行日志、服务器日志实时展示)。

这类场景的核心是「数据持续产生,需要逐次消费」,Future 只能处理单次异步操作,状态管理库(Bloc/Riverpod)是「状态封装层」,而底层的数据流生成必须依赖 Streamasync* + yield 则是生成这类流式数据最简洁的原生方式。

示例:蓝牙实时数据读取

dart 复制代码
import 'package:flutter_blue_plus/flutter_blue_plus.dart';

// 用 async* + yield 实时读取蓝牙设备的特征值数据
Stream<List<int>> readBleDataStream(BluetoothCharacteristic characteristic) async* {
  // 持续监听蓝牙数据,有新数据就 yield 出去
  await for (final value in characteristic.onValueReceived) {
    yield value; // 实时产出蓝牙设备发来的字节数据
  }
}

// 消费端使用
void listenBleData(BluetoothCharacteristic characteristic) {
  readBleDataStream(characteristic).listen((data) {
    print('实时蓝牙数据:$data'); // 每收到一次数据就处理一次
  });
}

其他方案的不足:

  • Future:无法持续监听,只能单次读取,完全不适用;
  • BlocBlocemit 本质是封装了 Stream,但底层仍需用 async* 生成数据流,只是上层封装,而非替代。

大数据量「流式处理」(内存敏感场景)

当处理超大文件 / 超大数据集时,async* + yield 是避免 OOM(内存溢出)的最优解,属于「必须用」的高需求场景。

典型业务场景:

  • 读取 / 解析超大本地文件(比如 100MB+CSV/JSON 日志文件、Excel 报表);
  • 大批量数据导出(比如导出 10 万条订单数据为 CSV,逐行生成避免内存爆炸);
  • 批量数据库查询(逐批读取数据,而非一次性加载所有结果)。

这类场景的核心是「内存可控」------ 一次性加载所有数据会直接导致 App 崩溃,而 yield 能逐行/逐块产出数据,内存占用始终保持在极低水平。

实战示例:解析超大 CSV 文件

dart 复制代码
import 'dart:io';
import 'dart:convert';

// 用 async* + yield 逐行解析超大 CSV 文件,避免 OOM
Stream<Map<String, String>> parseLargeCsvStream(String filePath) async* {
  final file = File(filePath);
  if (!await file.exists()) throw Exception('文件不存在');

  final lines = file.openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter());

  // 读取表头
  final headerLine = await lines.first;
  final headers = headerLine.split(',');

  // 逐行解析数据并 yield
  await for (final line in lines) {
    if (line.isEmpty) continue;
    final values = line.split(',');
    final row = <String, String>{};
    for (int i = 0; i < headers.length; i++) {
      row[headers[i]] = values[i];
    }
    yield row; // 逐行产出解析后的行数据,内存只存当前行
  }
}

// 消费端:逐行处理,不卡内存
void processLargeCsv(String filePath) async {
  await for (final row in parseLargeCsvStream(filePath)) {
    print('处理行数据:$row'); // 处理完当前行就释放内存
  }
}

自定义「可中断的异步序列」生成

当你需要生成有固定序列、但可能中途取消的异步数据时,async* + Stream 是最灵活的选择。

典型业务场景:

  • 分页加载(带「取消加载」功能,比如用户退出页面时终止后续请求);
  • 批量任务处理(比如批量上传 100 张图片,支持中途暂停 / 取消);
  • 精准控制的倒计时 / 定时器(支持中途停止,且不残留定时器)。

这类场景的核心是「可中断」------Stream 的订阅取消能直接终止 async* 函数的执行,而其他方案(如 Future 循环 + 定时器)中断逻辑复杂,易残留资源。

实战示例:可中断的批量图片上传

dart 复制代码
// 用 async* + yield 生成批量上传进度流,支持中途取消
Stream<double> uploadImagesStream(List<String> imagePaths) async* {
  int uploadedCount = 0;
  for (final path in imagePaths) {
    // 模拟单张图片上传(耗时操作)
    await Future.delayed(const Duration(seconds: 1));
    uploadedCount++;
    // 产出上传进度(0.0 ~ 1.0)
    yield uploadedCount / imagePaths.length;
  }
}

// 消费端:支持中途取消上传
void startUpload(List<String> imagePaths) {
  late StreamSubscription<double> subscription;
  
  subscription = uploadImagesStream(imagePaths).listen((progress) {
    print('上传进度:${(progress * 100).toStringAsFixed(1)}%');
    // 模拟:进度到50%时取消上传
    if (progress >= 0.5) {
      subscription.cancel(); // 取消订阅,uploadImagesStream 会立即停止循环
      print('上传已取消');
    }
  });
}

封装「底层流式工具 / SDK」

当你需要开发通用工具类、SDK 或底层库时,async* + Stream 是对外暴露流式接口的标准方式

  • 自定义网络请求库(对外暴露下载进度流);
  • 日志工具(对外暴露实时日志流);
  • 数据同步工具(对外暴露同步进度流)。

作为底层工具,需要提供「通用、灵活、低耦合」的接口,StreamDart/Flutter 生态的标准流式接口,而 async* 是生成这类接口的最简洁方式。

实战示例:自定义下载进度工具

dart 复制代码
import 'dart:io';
import 'package:http/http.dart' as http;

// 封装下载工具,用 async* + yield 暴露下载进度流
Stream<double> downloadFileStream(String url, String savePath) async* {
  final request = http.Request('GET', Uri.parse(url));
  final response = await http.Client().send(request);
  
  final totalLength = response.contentLength ?? -1;
  int downloadedLength = 0;
  
  final file = File(savePath);
  final sink = file.openWrite();
  
  await for (final chunk in response.stream) {
    downloadedLength += chunk.length;
    sink.add(chunk);
    // 产出下载进度(-1 表示长度未知)
    if (totalLength > 0) {
      yield downloadedLength / totalLength;
    }
  }
  
  await sink.flush();
  await sink.close();
  yield 1.0; // 最后产出 100% 进度
}
相关推荐
想用offer打牌2 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
KYGALYX3 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法4 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端