Flutter非同步程式設計-Stream

熊喵先生發表於2021-03-21

Stream可以說是構成Dart響應式流程式設計重要組成部分。還記得之前文章中說過的Future嗎,我們知道每個Future代表單一的值,可以非同步傳送資料或異常。而Stream的非同步工作方式和Future類似,只是Stream代表的是一系列的事件,那麼就可能傳遞任意資料值,可能是多個值也可以是異常。比如從磁碟中讀取一個檔案,那麼這裡返回的就是一個Stream。此外Stream是基於事件流訂閱的機制來運轉工作的。

1. 為什麼需要Stream

首先,在Dart單執行緒模型中,要實現非同步就需要藉助類似Stream、Future之類的API實現。所以Stream可以很好地實現Dart的非同步程式設計。 此外,在Dart中一些非同步場景中,比如磁碟檔案、資料庫讀取等類似需要讀取一系列的資料時,這種場景Future是不太合適的,所以在一些需要實現一系列非同步事件時Stream就是不錯的選擇,Stream提供一系列非同步的資料序列。換個角度理解Stream就是一系列的Future組合,Future只能有一個非同步響應,而Stream就是一系列的非同步響應

//Futures實現
void main() {
  Future.delayed(Duration(seconds: 1), () => print('future value is: 1'));
}
複製程式碼

輸出結果: image.png

//Stream實現
void main() async {
  Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (int value) {
    return value + 1;
  });
  await stream.forEach((element) => print('stream value is: $element'));
}
複製程式碼

輸出結果: (輸出結果是一直在執行的) image.png

2. 什麼是Stream

用官方的術語來說: Stream 是一系列非同步事件的序列。其類似於一個非同步的 Iterable,不同的是當你向 Iterable 獲取下一個事件時它會立即給你,但是 Stream 則不會立即給你而是在它準備好時告訴你。 Streams是非同步資料的源,Stream提供了一種接收事件序列的方式。每個事件要麼是資料事件(或稱為流的元素),要麼就是用於通知異常資訊error事件。當Stream所有的事件發出以後,一個"done"結束事件將作為最後一個事件發出。實際上類似RX響應式流的概念。

2.1 單一訂閱模型(Single-subscription)

image.png

2.2 廣播訂閱模型(Broadcast-subscription)

image.png

2.3 模型分析

  • StreamController 是建立 Stream 物件主要方式之一
  • 每個 StreamController 都會有一個槽口(Sink), 也就是Stream事件的入口,通過Sink的 add 將事件序列加入到 StreamController 中。
  • StreamController 類似一個生產者和消費者模型,它不知道什麼時候會有事件從Sink槽口加進來,而對於外部訂閱者也不知道何時有事件出來,所以對於外部訂閱者只需要新增監聽就好了。
  • 當有事件通過sink槽口加入到StreamController後,StreamController就開始工作,然後直到它輸出資料。
  • 需要注意的是從sink槽口加入的事件序列是有序的,監聽器得到序列是和加入序列一致,也就是說StreamController處理並不會打亂事件序列順序。
  • 單一訂閱者顧明思意就是隻能有一個訂閱者監聽整個事件流,而對於廣播訂閱可以有若干個訂閱者監聽整個事件流,類似於廣播通知的機制。

3. 如何使用Stream

3.1 建立Stream的方法

3.1.1 通過Stream構造器建立
  • Stream.fromFuture: 通過Future建立一個新的 single-subscription(單一訂閱)Stream , 當Future完成時觸發 then 回撥,然後就會把返回的value加入到 StreamController 中, 並且還會新增一個 Done 事件表示結束。若Future完成時觸發 onError 回撥,則會把error加入到StreamController 中, 並且還會新增一個 Done 事件表示結束。
  factory Stream.fromFuture(Future<T> future) {
    _StreamController<T> controller =
        new _SyncStreamController<T>(null, null, null, null);
    future.then((value) {//future完成時,then回撥
      controller._add(value);//將value加入到_StreamController中
      controller._closeUnchecked();//最後傳送一個done事件
    }, onError: (error, stackTrace) {//future完成時,error回撥
      controller._addError(error, stackTrace);//將error加入到_StreamController中
      controller._closeUnchecked();//最後傳送一個done事件
    });
    return controller.stream;//最後返回stream
  }

複製程式碼
void main() {
  Stream.fromFuture(Future.delayed(Duration(seconds: 1), () => 100)).listen(
      (event) => print(event),
      onDone: () => print('is done'),
      onError: (error, stacktrace) => print('is error, errMsg: $error'),
      cancelOnError: true);//cancelOnError: true(表示出現error就取消訂閱,之後事件將無法接收;false表示出現error後,後面事件可以繼續接收)
}
複製程式碼

輸出結果: image.png

  • Stream.fromIterable: 通過從一個集合中獲取其資料來建立一個新的**single-subscription(單一訂閱)Stream**
void main() {
  Stream.fromIterable([1, 2, 3, 4, 5, 6, 7, 8])
      .map((event) => "this is $event")//還可以藉助map,fold,reduce之類操作符,可以變換事件流
      .listen((event) => print(event),
          onDone: () => print('is done'),
          onError: (error, stacktrace) => print('is error, errMsg: $error'),
          cancelOnError: true);
}
複製程式碼

輸出結果: image.png

  • Stream.fromFutures:從一系列的Future中建立一個新的 single-subscription(單一訂閱)Stream,每個future都有自己的data或者error事件,當整個Futures完成後,流將會關閉。如果Futures為空,流將會立刻關閉。
void main() {
  var future1 = Future.value(100);
  var future2 = Future.delayed(Duration(seconds: 1), () => 200);
  var future3 = Future.delayed(Duration(seconds: 2), () => 300);
  Stream.fromFutures([future1, future2, future3])
      .reduce((previous, element) => previous + element)//累加所有future中的值
      .asStream()
      .listen((event) => print(event),
          onDone: () => print('is done'),
          onError: (error, stacktrace) => print('is error, errMsg: $error'),
          cancelOnError: true);
}
複製程式碼

輸出結果: image.png

  • Stream.periodic: 可以建立一個新的重複發射事件而且可以指定間隔時間的Stream,通過再StreamController的onResume方法中建立一個Timer物件,最後呼叫Timer的 periodic方法。
 factory Stream.periodic(Duration period,
      [T computation(int computationCount)]) {
    Timer timer;
    int computationCount = 0;
    StreamController<T> controller;
    // Counts the time that the Stream was running (and not paused).
    Stopwatch watch = new Stopwatch();//建立Stopwatch用於計算Stream執行時間, 會一直執行不會停止

    void sendEvent() {
      watch.reset();
      T data;
      if (computation != null) {
        try {
          data = computation(computationCount++);
        } catch (e, s) {
          controller.addError(e, s);
          return;
        }
      }
      controller.add(data);
    }

    void startPeriodicTimer() {
      assert(timer == null);
      //建立Timer物件
      timer = new Timer.periodic(period, (Timer timer) {
        sendEvent();
      });
    }

    controller = new StreamController<T>(
        sync: true,
        onListen: () {
          watch.start();
          startPeriodicTimer();
        },
        onPause: () {
          timer.cancel();
          timer = null;
          watch.stop();
        },
        onResume: () {
          assert(timer == null);
          Duration elapsed = watch.elapsed;
          watch.start();
          timer = new Timer(period - elapsed, () {
            timer = null;
            startPeriodicTimer();
            sendEvent();
          });
        },
        onCancel: () {
          if (timer != null) timer.cancel();
          timer = null;
          return Future._nullFuture;
        });
    return controller.stream;
  }
複製程式碼
void main() {
  Stream.periodic(Duration(seconds: 1), (value) => value + 100)
        .listen((event) => print(event),
          onDone: () => print('is done'),
          onError: (error, stacktrace) => print('is error, errMsg: $error'),
          cancelOnError: true);
}
複製程式碼

輸出結果: image.png

3.1.2 通過StreamController建立
  • 建立任意型別StremController,也就是sink槽口可以加入任何型別的事件資料
import 'dart:async';

void main() {
  //1.建立一個任意型別StreamController物件
  StreamController streamController = StreamController(
      onListen: () => print('listen'),
      onCancel: () => print('cancel'),
      onPause: () => print('pause'),
      onResume: () => print('resumr'));
  //2.通過sink槽口新增任意型別事件資料
  streamController.sink.add(100);
  streamController.sink.add(100.121212);
  streamController.sink.add('THIS IS STRING');
  streamController.sink.close();//只有手動呼叫close方法傳送一個done事件,onDone才會被回撥
  //3.註冊監聽
  streamController.stream.listen((event) => print(event),
      onDone: () => print('is done'),
      onError: (error, stacktrace) => print('is error, errMsg: $error'),
      cancelOnError: true);
}
複製程式碼

輸出結果: image.png

  • 建立指定型別的StreamController, 也就是sink槽口可以加入對應指定型別的事件資料
import 'dart:async';

void main() {
  //1.建立一個int型別StreamController物件
  StreamController<int> streamController = StreamController(
      onListen: () => print('listen'),
      onCancel: () => print('cancel'),
      onPause: () => print('pause'),
      onResume: () => print('resumr'));
  //2.通過sink槽口新增int型別事件資料
  streamController.sink.add(100);
  streamController.sink.add(200);
  streamController.sink.add(300);
  streamController.sink.add(400);
  streamController.sink.add(500);
  streamController.sink.close(); //只有手動呼叫close方法傳送一個done事件,onDone才會被回撥
  //3.註冊監聽
  streamController.stream.listen((event) => print(event),
          onDone: () => print('is done'),
          onError: (error, stacktrace) => print('is error, errMsg: $error'),
          cancelOnError: true);
}
複製程式碼

輸出結果: image.png

3.1.3 通過async*建立

如果有一系列事件需要處理,也許會需要把它轉化為 stream。這時候可以使用 async*** 和 yield** 來生成一個 Stream。

void main() {
  generateStream(10).listen((event) => print(event),
      onDone: () => print('is done'),
      onError: (error, stacktrace) => print('is error, errMsg: $error'),
      cancelOnError: true);
}

Stream<int> generateStream(int dest) async* {
  for (int i = 1; i <= dest; i++) {
    yield i;
  }
}
複製程式碼

輸出結果: image.png

3.2 監聽Stream

3.2.1 listen方法監聽

監聽Stream流主要就是使用 listen 這個方法,它有 onData(必填引數) , onError(可選引數) , onDone(可選引數) , cancelOnError(可選引數) 

  • onData: 接收到資料時觸發回撥
  • onError: 接收到異常時觸發回撥
  • onDone: 資料接收完畢觸發回撥
  • cancelOnError: 表示true(出現第一個error就取消訂閱,之後事件將無法接收;false表示出現error後,後面事件可以繼續接收)
  StreamSubscription<T> listen(void onData(T data),
      {Function onError, void onDone(), bool cancelOnError}) {
    cancelOnError = identical(true, cancelOnError);
    StreamSubscription<T> subscription =
        _createSubscription(onData, onError, onDone, cancelOnError);
    _onListen(subscription);
    return subscription;
  }
複製程式碼
3.2.2 async-await配合for或forEach迴圈處理

通過async-await配合for或forEach可以實現當Stream中每個事件到來的時候處理它,由於Stream接收事件時機是不確定,所以for或forEach迴圈退出的時候一般是Stream關閉或者完成結束的時候

void main() async {
  Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), (int value) {
    return value + 1;
  });
  await stream.forEach((element) => print('stream value is: $element'));
}
複製程式碼

輸出結果: image.png

3.3 Stream流的轉換

Stream流的轉換實際上是通過類似 map 、 take 、 where 、 reduce 、 expand 之類的操作符函式實現流的變換。實際上他們作用和集合中變化操作符意思是類似,所以這裡由於篇幅問題就不一一展開,有了前面集合操作符函式基礎,這裡也是類似,只不過這邊返回的是 Stream<T> 而已。不過這裡需要特別說下 transform 操作符函式.

transform 操作符函式它能實現更多自定義的流變化規則, 它通過傳入一個 StreamTransformer<T, S> 引數,最後返回一個 Stream<T> . 也就是輸入的流型別是 T . 輸出的是 S ,通過StreamTransformer 輸出一個新的Stream流。

  Stream<S> transform<S>(StreamTransformer<T, S> streamTransformer) {
    return streamTransformer.bind(this);
  }

複製程式碼
import 'dart:async';

void main() {
  //1.建立一個int型別StreamController物件
  StreamController<int> streamController = StreamController();
  //2.通過sink槽口新增int型別事件資料
  streamController.sink.add(100);
  streamController.sink.add(200);
  streamController.sink.add(300);
  streamController.sink.add(400);
  streamController.sink.add(500);
  streamController.sink.close(); //只有手動呼叫close方法傳送一個done事件,onDone才會被回撥

  //自定義StreamTransformer
  final transformer = StreamTransformer<int, String>.fromHandlers(handleData: (value, sink) {
    sink.add("this number is: $value");
  });
  //3.註冊監聽
  streamController.stream
      .transform(transformer)
      .listen((event) => print("second listener: $event"), onDone: () => print('second listener: is done'));
}
複製程式碼

輸出結果: image.png

3.4 Stream流的種類

其實這裡Stream流的種類劃分是基於Stream流的訂閱模型來劃分,所以那麼這裡Stream流的種類只有兩種: Single-subscription(單一訂閱)Stream、Broadcast-subscription(廣播訂閱)Stream

3.4.1 Single-subscription(單一訂閱)Stream

image.png 單一訂閱Stream在整個流的生命週期中只會有一個訂閱者監聽,也就是listen方法只能呼叫一次,而且第一次listen取消(cancel)後,不能重複監聽,否則會丟擲異常

import 'dart:async';

void main() {
  //1.建立一個int型別StreamController物件
  StreamController<int> streamController = StreamController(
      onListen: () => print('listen'),
      onCancel: () => print('cancel'),
      onPause: () => print('pause'),
      onResume: () => print('resumr'));
  //2.通過sink槽口新增int型別事件資料
  streamController.sink.add(100);
  streamController.sink.add(200);
  streamController.sink.add(300);
  streamController.sink.add(400);
  streamController.sink.add(500);
  streamController.sink.close(); //只有手動呼叫close方法傳送一個done事件,onDone才會被回撥
  //3.註冊監聽
  streamController.stream.listen((event) => print(event), onDone: () => print('is done'));
  streamController.stream.listen((event) => print(event), onDone: () => print('is done')); //不允許兩次監聽
}
複製程式碼

輸出結果: image.png

3.4.2 Broadcast-subscription(廣播訂閱)Stream

image.png Broadcast廣播訂閱模型Stream, 可以同時存在任意多個訂閱者監聽,無論是否有訂閱者,它都會產生事件。所以中途進來的收聽者將不會收到之前的訊息。 如果多個收聽者要監聽單一訂閱Stream,需要使用 asBroadcastStream 轉化成Broadcast廣播訂閱Stream. 或者建立BroadcastStream流可以通過繼承Stream然後重寫isBroadcast為true即可。

import 'dart:async';

void main() {
  //1.建立一個int型別StreamController物件
  StreamController<int> streamController = StreamController(
      onListen: () => print('listen'),
      onCancel: () => print('cancel'),
      onPause: () => print('pause'),
      onResume: () => print('resumr'));
  //2.通過sink槽口新增int型別事件資料
  streamController.sink.add(100);
  streamController.sink.add(200);
  streamController.sink.add(300);
  streamController.sink.add(400);
  streamController.sink.add(500);
  streamController.sink.close(); //只有手動呼叫close方法傳送一個done事件,onDone才會被回撥
  //3.註冊監聽
  Stream stream = streamController.stream.asBroadcastStream();//轉換成BroadcastStream
  stream.listen((event) => print("first listener: $event"), onDone: () => print('first listener: is done'));
  stream.listen((event) => print("second listener: $event"), onDone: () => print('second listener: is done'));
}
複製程式碼

輸出結果: image.png

4. Stream使用的場景

Stream的使用場景有很多比如資料庫的讀寫、檔案IO的讀寫、基於多個網路請求轉化處理都可以使用流來處理。下面會給出一個具體的檔案複製例子實現IO檔案讀寫使用Stream的場景。

import 'dart:async';
import 'dart:io';

void main() {
  copyFile(File('/Users/mikyou/Desktop/gitchat/test.zip'),
      File('/Users/mikyou/Desktop/gitchat/copy_dir/test.copy.zip'));
}

void copyFile(File sourceFile, File targetFile) async {
  assert(await sourceFile.exists() == true);
  print('source file path: ${sourceFile.path}');

  print('target file path: ${targetFile.path}');
  //以WRITE方式開啟檔案,建立快取IOSink
  IOSink sink = targetFile.openWrite();

  //檔案大小
  int fileLength = await sourceFile.length();
  //已讀取檔案大小
  int count = 0;
  //模擬進度條
  String progress = "-";

  //以只讀方式開啟原始檔資料流
  Stream<List<int>> inputStream = sourceFile.openRead();
  inputStream.listen((List<int> data) {
    count += data.length;
    //進度百分比
    double num = (count * 100) / fileLength;
    print("${progress * (num ~/ 2)}[${num.toStringAsFixed(2)}%]");
    //將資料新增到快取sink中
    sink.add(data);
  }, onDone: () {
    //資料流傳輸結束時,觸發onDone事件
    print("複製檔案結束!");
    //關閉快取釋放系統資源
    sink.close();
  });
}
複製程式碼

輸出結果: image.png

5. Stream與Future的區別

實際上有了上面對Stream的介紹,相信很多人基本上都能分析出Stream和Future的區別了。 先用官方專業術語做下對比: Future 表示一個不會立即完成的計算過程。與普通函式直接返回結果不同的是非同步函式返回一個將會包含結果的 Future。該 Future 會在結果準備好時通知呼叫者 image.png Stream 是一系列非同步事件的序列。其類似於一個非同步的 Iterable,不同的是當你向 Iterable 獲取下一個事件時它會立即給你,但是 Stream 則不會立即給你而是在它準備好時告訴你 image.png 可以使用一個餐廳吃飯場景來理解Future和Stream的區別:

Future就好比你去一家餐廳吃飯,在前臺點好你想吃的菜後,付完錢後服務員會給你一個等待的號碼牌(相當於先拿到一個Future),後廚就開始根據你下的訂單開始做菜,等到你的菜好了後,就可以通過號碼牌拿到指定的菜了(返回的資料或異常資訊)。 Stream就好比去一家餐廳吃飯,在前臺點好A,B,C,D4種你想吃的菜後(訂閱資料流過程),然後你就去桌子等著,至於菜什麼時候好,你也不知道所以就一直等著(類似於一直監聽listen著),後廚就開始根據你下的訂單開始做菜, 等著你的第一盤A種菜好了後,服務員就會主動傳送A到你的桌子上(基於一種類似訂閱-推送機制),沒有特殊意外,服務員推送菜的順序應該也是A,B,C,D。

6. 熊喵先生的小總結

到這裡有關Dart非同步程式設計中Stream就介紹完畢了,Stream在資料庫讀寫,檔案IO讀寫方面是非常實用。此外它支援響應式流式程式設計,可以利用它一些轉換操作符實現對流的變換,可以實現一些類似Rx中的操作。

感謝關注,熊喵先生願和你在技術路上一起成長!

相關文章