烧脑时刻: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% 进度
}
相关推荐
追逐时光者2 小时前
一个致力于为 C# 程序员提供更佳的编码体验和效率的 Visual Studio 扩展插件
后端·c#·visual studio
wearegogog1233 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars4 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤4 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·4 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°4 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
行百里er4 小时前
用 ThreadLocal + Deque 打造一个“线程专属的调用栈” —— Spring Insight 的上下文管理术
java·后端·架构
玄〤4 小时前
黑马点评中 VoucherOrderServiceImpl 实现类中的一人一单实现解析(单机部署)
java·数据库·redis·笔记·后端·mybatis·springboot
qq_419854055 小时前
CSS动效
前端·javascript·css
烛阴5 小时前
3D字体TextGeometry
前端·webgl·three.js