Flutter 中的异步事件处理之Stream使用(四)

500 阅读1分钟

前面的章节, 我们探究了StreamStreamController的源码, 了解了它们的实现。接下来, 我们一起探讨怎样使用它们。

Stream

Stream 是一系列异步事件的序列。其类似于一个异步的 Iterable,不同的是当你向 Iterable 获取下一个事件时它会立即给你,但是 Stream 则不会立即给你而是在它准备好时告诉你。

async*

下面这段代码演示了如何使用async*进行 1 到 10 的相加。

Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  await for (final value in stream) {
    print("---迭代流,value:$value---");
    sum += value;
  }
  return sum;
}

Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    yield i;
  }
}

void main() async {
  var stream = countStream(10);
  var sum = await sumStream(stream);
  print(sum); // 55
}

执行结果:

---迭代流,value:1---
---迭代流,value:2---
---迭代流,value:3---
---迭代流,value:4---
---迭代流,value:5---
---迭代流,value:6---
---迭代流,value:7---
---迭代流,value:8---
---迭代流,value:9---
---迭代流,value:10---
55

打印结果可以发现, yield关键字标识发送一个流, await for关键字标识不停地在接收流。

那么, 实际开发过程中, Stream 完成前会出现错误, 我们该怎样处理呢?

流错误事件

Stream 可以像提供数据事件那样提供错误事件。大多数 Stream 会在第一次错误出现后停止,但其也可以提供多次错误并可以在在出现错误后继续提供数据事件。我们只讨论 Stream 最多出现并提供一次错误事件的情况。

Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  try {
    await for (final value in stream) {
      sum += value;
    }
  } catch (e) {
    return -1;
  }
  return sum;
}

Stream<int> countStream(int to) async* {
  for (int i = 1; i <= to; i++) {
    if (i == 5) {
      throw Exception('Intentional exception');
    } else {
      yield i;
    }
  }
}

void main() async {
  var stream = countStream(10);
  var sum = await sumStream(stream);
  print(sum); // -1
}

当使用 await for 读取 Stream 时,如果出现错误,则由循环语句抛出,同时循环结束。你可以使用 try-catch 语句捕获错误。下面的示例会在循环迭代到参数值等于 5 时抛出一个错误。

StreamController创建Stream

日常开发中,通常会通过StreamController创建Stream。只需要构造出StreamController对象,通过这个对象的.stream就可以得到Stream

Stream<int> countStream(int to) {
  // 先创建 StreamController
  late StreamController<int> controller;
  controller = StreamController<int>(onListen: () {
    // 当 Stream 被监听时会触发 onListen 回调
    for (var i = 0; i < to; i++) {
      controller.add(i);
    }
    controller.close();
  });

  return controller.stream;
}

Future<int> listenOn(Stream<int> stream) async {
  var completer = Completer<int>();
  var sum = 0;

  // 监听 stream
  stream.listen(
        (event) {
      sum += event;
    },
    onDone: () => completer.complete(sum),
  );

  return completer.future;
}

void main() async {
  var stream = countStream(10);
  // 当注释掉下面这行,控制台也不会打印出 "stream 被监听"
  var sum = await listenOn(stream);
  print(sum); // 55
}

在创建StreamController的时候传入了一个onListen回调,当流第一次被监听的时候,会触发这个回调,此时会往流里面依次添加多个数据,listenOn方法里拿到这些数据执行相加操作。这里使用了streamlisten的方法进行监听。

常见使用场景

前面章节, 我们一起探讨了流的原理, 下面我们看几个常见的使用场景。

同步流

void _testCStreamSync() {
  final StreamController _sc = StreamController(sync: true);

  /// 流被监听时触发
  _sc.onListen = () {
    debugPrint('---😆流被监听时触发😆😆---');
  };

  /// 监听流,订阅时,流控制器内部在微任务里面调度任务
  final StreamSubscription _subscription = _sc.stream.listen((event) {
    debugPrint('---😆😆监听流,event:$event😆😆---');
  });

  /// 添加任务
  _sc.add('1');
  _sc.add('2');
  _sc.add('3');

  debugPrint('暂停');
  _subscription.pause();

  Future.delayed(const Duration(seconds: 3), () {
    debugPrint('3秒后 -> 恢复');
    _subscription.resume();
  });
}

同步流, 订阅后, 添加了三个任务, 然后暂停, 3秒后恢复, 程序的执行顺序是怎样的呢?

截屏2023-09-14 10.10.04.png 打印日志,发现,三个任务按顺序执行完成后,再打印的暂停, 3s后, 打印3秒后 -> 恢复

异步流跟scheduleMicrotask结合

void _testCStreamASync() {
  final StreamController _sc = StreamController(sync: false);

  /// 流被监听时触发
  _sc.onListen = () {
    debugPrint('---😆流被监听时触发😆😆---');
  };

  /// 监听流,订阅时,流控制器内部在微任务里面调度任务
  final StreamSubscription _subscription = _sc.stream.listen((event) {
    debugPrint('---😆😆监听流,event:$event😆😆---');
  });

  /// 添加任务
 _sc.add('1');
 _sc.add('2');
 _sc.add('3');

  scheduleMicrotask(() {
    debugPrint('暂停');
    _subscription.pause();

    Future.delayed(const Duration(seconds: 3), () {
      debugPrint('3秒后 -> 恢复');
      _subscription.resume();
    });
  });
}

异步流, 在微任务scheduleMicrotask暂停、3s后恢复。可以看到, 执行第一个任务后,流就暂停了,恢复任务任务后, 剩下的任务继续执行。日志如下:

flutter: ---😆流被监听时触发😆😆---
flutter: ---😆😆监听流,event:1😆😆---
flutter: 暂停
flutter: 3秒后 -> 恢复
flutter: ---😆😆监听流,event:2😆😆---
flutter: ---😆😆监听流,event:3😆😆---

为啥会这样呢?因为流控制StreamController内部执行流的调度时, 通过scheduleMicrotask添加一个微任务全局微任务队列中。这从前面的源码中, 我们可以清楚地知道。现在我们在外部又添加了一个微任务全局微任务队列中, 执行暂停恢复操作。

在 Dart 中,通常只有一个微任务队列。

在Dart中,使用 scheduleMicrotask 可以将任务添加到微任务队列中。微任务是在事件循环的一个阶段内按照先 进先出(FIFO)顺序执行的任务。微任务通常用于在当前事件循环阶段结束之前执行一些任务。

执行流程分析: 代码中, 确实将三个任务('1'、'2' 和 '3')添加到了流控制器 _sc 中。但问题在于,在执行 _sc.add('1') 后,即使任务已经添加到流中,它们仍然需要等待当前的微任务完成,然后才能由流传递给监听器。

让我更详细地解释一下流的工作原理:

  • 当调用 _sc.add('1'),'1' 事件被添加到流 _sc 中,但它不会立即传递给监听器。
  • 在当前的微任务(流控制器里scheduleMicrotask微任务)中,继续添加了 '2' 和 '3' 事件到流 _sc 中。现在,所有三个事件都在流中排队等待。
  • _BufferingStreamSubscription_add_sendData_checkState可知, 系统开了个while循环执行调度_pending!.schedule(this)微任务。
  • 此时, 从_PendingEventsschedule方法可知, 第一个微任务正在调度执行。
  • 然后,我们在_testCStreamASync方法中, 又用scheduleMicrotask添加了一个新的微任务, 调用 _subscription.pause() 来暂停监听器。但是这个暂停操作不会立即生效。相反,它会在当前微任务(流控制器里的第一个微任务)完成后生效。
  • 目前,第一个微任务仍然在执行中,正在等待处理所有三个事件。只有当当前微(流控制器里的第一个微任务)任务完成后,才会进入执行下一个微任务。
  • 在当前微任务(流控制器里的第一个微任务)完成后,监听器会处理流中的第一个事件 '1'。然后,因为暂停操作已经生效,后续的 '2' 和 '3' 事件不会被立即处理。
  • 3 秒后,Future.delayed 中的任务触发 _subscription.resume(),这会恢复监听器的执行。此时,监听器会处理流中的 '2' 和 '3' 事件。

所以,这个行为是由 Dart 中事件循环和异步任务执行的机制导致的,导致在您的代码中首先处理了第一个事件 '1',然后才暂停,最后再处理 '2' 和 '3'。

异步流跟Future.delayed

使用Future.delayed延迟暂停

void _testCStreamSync() {
  final StreamController _sc = StreamController(sync: false);

  /// 流被监听时触发
  _sc.onListen = () {
    debugPrint('---😆流被监听时触发😆😆---');
  };

  /// 监听流,订阅时,流控制器内部在微任务里面调度任务
  final StreamSubscription _subscription = _sc.stream.listen((event) {
    debugPrint('---😆😆监听流,event:$event😆😆---');
  });

  /// 添加流
  _sc.add('1');
  _sc.add('2');
  _sc.add('3');
  _sc.add('4');
  _sc.add('5');
  _sc.add('6');
  _sc.add('7');

  Future.delayed(Duration.zero, () {
    print('暂停');
    _subscription.pause();

    Future.delayed(const Duration(seconds: 3), () {
      print('3秒后 -> 恢复');
      _subscription.resume();
    });
  });
}

根据Future.delayed文档, 如果持续时间(duration)为0或更短,那么操作将在下一个事件循环迭代之前不会立即完成,而是在所有微任务都运行完毕之后才会完成。

/// If the duration is 0 or less,
/// it completes no sooner than in the next event-loop iteration,
/// after all microtasks have run.

在 Dart 中,微任务是在事件循环中执行的异步任务,通常在当前事件迭代的末尾运行。如果某个操作的持续时间被设置为0或更短,它将不会立即完成,而是会等待当前事件迭代的微任务执行完毕,然后在下一个事件迭代中执行。这确保了微任务的顺序性,因为它们会按照它们排队的顺序依次执行。

所以, 我们看到如下日志:

flutter: ---😆流被监听时触发😆😆---
flutter: ---😆😆监听流,event:1😆😆---
flutter: ---😆😆监听流,event:2😆😆---
flutter: ---😆😆监听流,event:3😆😆---
flutter: ---😆😆监听流,event:4😆😆---
flutter: ---😆😆监听流,event:5😆😆---
flutter: ---😆😆监听流,event:6😆😆---
flutter: ---😆😆监听流,event:7😆😆---
flutter: 暂停
flutter: 3秒后 -> 恢复

参考资料

异步编程:使用 stream

在 Dart 里使用 Stream

Stream<​T> class

官方示例

【Flutter 异步编程 -陆】 | 探索 Dart 消息处理与微任务循环