烧脑时刻: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% 进度
}
相关推荐
用户636836608552 小时前
前端使用nuxt.js的seo优化
前端
湛海不过深蓝2 小时前
【echarts】折线图颜色分段设置不同颜色
前端·javascript·echarts
昨晚我输给了一辆AE862 小时前
关于 react-hook-form 的 isValid 在有些场景下的值总是 false 问题
前端·react.js
老马95272 小时前
事务工具类
数据库·后端
xinyu_Jina2 小时前
Calculator Game:WebAssembly在计算密集型组合优化中的性能优势
前端·ui·性能优化
JustHappy2 小时前
「2025年终个人总结」🤬🤬回答我!你个菜鸟程序员这一年发生了啥?
前端
啃火龙果的兔子2 小时前
可以指定端口启动本地前端的npm包
前端·npm·node.js
汤姆yu2 小时前
基于springboot的林业资源管理系统
java·spring boot·后端
软件管理系统2 小时前
基于Spring Boot的医疗服务系统的设计与实现
java·spring boot·后端