Flutter完整開發實戰詳解(十一、全面深入理解Stream)

戀貓de小郭發表於2019-04-26

作為系列文章的第十一篇,本篇將非常全面帶你瞭解 Flutter 中最關鍵的設計之一,深入原理幫助你理解 Stream 全家桶,這也許是目前 Flutter 中最全面的 Stream 分析了。

前文:

一、Stream 由淺入深

Stream 在 Flutter 是屬於非常關鍵的概念,在 Flutter 中,狀態管理除了 InheritedWidget 之外,無論 rxdartBloc 模式,flutter_reduxfish_redux 都離不開 Stream 的封裝,而事實上 Stream 並不是 Flutter 中特有的,而是 Dart 中自帶的邏輯。

通俗來說,Stream 就是事件流或者管道,事件流相信大家並不陌生,簡單的說就是:基於事件流驅動設計程式碼,然後監聽訂閱事件,並針對事件變換處理響應

而在 Flutter 中,整個 Stream 設計外部暴露的物件主要如下圖,主要包含了 StreamControllerSinkStreamStreamSubscription 四個物件。

圖片要換

1、Stream 的簡單使用

如下程式碼所示,Stream 的使用並不複雜,一般我們只需要:

  • 建立 StreamController
  • 然後獲取 StreamSink 用做事件入口,
  • 獲取 Stream 物件用於監聽,
  • 並且通過監聽得到 StreamSubscription 管理事件訂閱,最後在不需要時關閉即可,看起來是不是很簡單?
class DataBloc {
  ///定義一個Controller
  StreamController<List<String>> _dataController = StreamController<List<String>>();
  ///獲取 StreamSink 做 add 入口
  StreamSink<List<String>> get _dataSink => _dataController.sink;
  ///獲取 Stream 用於監聽
  Stream<List<String>> get _dataStream => _dataController.stream;
  ///事件訂閱物件
  StreamSubscription _dataSubscription;

  init() {
    ///監聽事件
    _dataSubscription = _dataStream.listen((value){
      ///do change
    });
    ///改變事件
    _dataSink.add(["first", "second", "three", "more"]);

  }

  close() {
    ///關閉
    _dataSubscription.cancel();
    _dataController.close();
  }
}
複製程式碼

在設定好監聽後,之後每次有事件變化時, listen 內的方法就會被呼叫,同時你還可以通過操作符對 Stream 進行變換處理。

如下程式碼所示,是不是一股 rx 風撲面而來?

_dataStream.where(test).map(convert).transform(streamTransformer).listen(onData);
複製程式碼

而在 Flutter 中, 最後結合 StreamBuilder , 就可以完成 基於事件流的非同步狀態控制元件 了!

StreamBuilder<List<String>>(
    stream: dataStream,
    initialData: ["none"],
    ///這裡的 snapshot 是資料快照的意思
    builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
      ///獲取到資料,為所欲為的更新 UI
      var data = snapshot.data;
      return Container();
    });

複製程式碼

那麼問題來了,它們內部究竟是如果實現的呢?原理是什麼?各自的作用是什麼?都有哪些特性呢?後面我們將開始深入解析這個邏輯

2、Stream 四天王

從上面我們知道,在 Flutter 中使用 Stream 主要有四個物件,那麼這四個物件是如何“勾搭”在一起的?他們各自又擔任什麼責職呢?

首先如下圖,我們可以從進階版的流程圖上看出 整個 Stream 的內部工作流程。

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

Flutter中 StreamStreamControllerStreamSinkStreamSubscription 都是 abstract 物件,他們對外抽象出介面,而內部實現物件大部分都是 _ 開頭的如 _SyncStreamControllerControllerStream 等私有類,在這基礎上整個流程概括起來就是:

有一個事件源叫 Stream,為了方便控制 Stream ,官方提供了使用 StreamController 作為管理;同時它對外提供了 StreamSink 物件作為事件輸入口,可通過 sink 屬性訪問; 又提供 stream 屬性提供 Stream 物件的監聽和變換,最後得到的 StreamSubscription 可以管理事件的訂閱。

所以我們可以總結出:

  • StreamController :如類名描述,用於整個 Stream 過程的控制,提供各類介面用於建立各種事件流。
  • StreamSink:一般作為事件的入口,提供如 addaddStream 等。
  • Stream:事件源本身,一般可用於監聽事件或者對事件進行轉換,如 listenwhere
  • StreamSubscription:事件訂閱後的物件,表面上用於管理訂閱過等各類操作,如 cacenlpause ,同時在內部也是事件的中轉關鍵。

回到 Stream 的工作流程上,在上圖中我們知道, 通過 StreamSink.add 新增一個事件時, 事件最後會回撥到 listen 中的 onData 方法,這個過程是通過 zone.runUnaryGuarded 執行的,這裡 zone.runUnaryGuarded 是什麼作用後面再說,我們需要知道這個 onData 是怎麼來的?

image.png

如上圖,通過原始碼我們知道:

  • 1、Streamlisten 的時候傳入了 onData 回撥,這個回撥會傳入到 StreamSubscription 中,之後通過 zone.registerUnaryCallback 註冊得到 _onData 物件( 不是前面的 onData 回撥哦 )。

  • 2、StreamSink 在新增事件是,會執行到 StreamSubscription 中的 _sendData 方法,然後通過 _zone.runUnaryGuarded(_onData, data); 執行 1 中得到的 _onData 物件,觸發 listen 時傳入的回撥方法。

可以看出整個流程都是和 StreamSubscription 相關的,現在我們已經知道從 事件入口到事件出口 的整個流程時怎麼運作的,那麼這個過程是**怎麼非同步執行的呢?其中頻繁出現的 zone 是什麼?

3、執行緒

首先我們需要知道,Stream 是怎麼實現非同步的?

這就需要說到 Dart 中的非同步實現邏輯了,因為 Dart 是 單執行緒應用 ,和大多數單執行緒應用一樣,Dart 是以 訊息迴圈機制 來執行的,而這裡面主要包含兩個任務佇列,一個是 microtask 內部佇列,一個是 event 外部佇列,而 microtask 的優先順序又高於 event

預設的在 Dart 中,如 點選、滑動、IO、繪製事件 等事件都屬於 event 外部佇列,microtask 內部佇列主要是由 Dart 內部產生,而 Stream 中的執行非同步的模式就是 scheduleMicrotask 了。

因為 microtask 的優先順序又高於 event ,所以如果 microtask 太多就可能會對觸控、繪製等外部事件造成阻塞卡頓哦。

如下圖,就是 Stream 內部在執行非同步操作過程執行流程:

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

4、Zone

那麼 Zone 又是什麼?它是哪裡來的?

在上一篇章中說過,因為 Dart 中 Future 之類的非同步操作是無法被當前程式碼 try/cacth 的,而在 Dart 中你可以給執行物件指定一個 Zone,類似提供一個沙箱環境 ,而在這個沙箱內,你就可以全部可以捕獲、攔截或修改一些程式碼行為,比如所有未被處理的異常。

那麼專案中預設的 Zone 是怎麼來的?在 Flutter 中,Dart 中的 Zone 啟動是在 _runMainZoned 方法 ,如下程式碼所示 _runMainZoned@pragma("vm:entry-point") 註解表示該方式是給 Engine 呼叫的,到這裡我們知道了 Zone 是怎麼來的了。

///Dart 中

@pragma('vm:entry-point')
// ignore: unused_element
void _runMainZoned(Function startMainIsolateFunction, Function userMainFunction) {
  startMainIsolateFunction((){
    runZoned<Future<void>>(····);
  }, null);
}

///C++ 中
if (tonic::LogIfError(tonic::DartInvokeField(
          Dart_LookupLibrary(tonic::ToDart("dart:ui")), "_runMainZoned",
          {start_main_isolate_function, user_entrypoint_function}))) {
    FML_LOG(ERROR) << "Could not invoke the main entrypoint.";
    return false;
}

複製程式碼

那麼 zone.runUnaryGuarded 的作用是什麼?相較於 scheduleMicrotask 的非同步操作,官方的解釋是:在此區域中使用引數執行給定操作並捕獲同步錯誤。 類似的還有 runUnaryrunBinaryGuarded 等,所以我們知道前面提到的 zone.runUnaryGuarded 就是 Flutter 在執行的這個 zone 裡執行已經註冊的 _onData,並捕獲異常

5、非同步和同步

前面我們說了 Stream 的內部執行流程,那麼同步和非同步操作時又有什麼區別?具體實現時怎麼樣的呢?

我們以預設 Stream 流程為例子, StreamController 的工廠建立可以通過 sync 指定同步還是非同步,預設是非同步模式的。 而無論非同步還是同步,他們都是繼承了 _StreamController 物件,區別還是在於 mixins 的是哪個 _EventDispatch 實現:

  • _AsyncStreamControllerDispatch

  • _SyncStreamControllerDispatch

上面這兩個 _EventDispatch 最大的不同就是在呼叫 sendData 提交事件時,是直接呼叫 StreamSubscription_add 方法,還是呼叫 _addPending(new _DelayedData<T>(data)); 方法的區別。

如下圖, 非同步執行的邏輯就是上面說過的 scheduleMicrotask, 在 _StreamImplEventsscheduleMicrotask 執行後,會呼叫 _DelayedDataperform ,最後通過 _sendData 觸發 StreamSubscription 去回撥資料 。

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

6、廣播和非廣播。

Stream 中又非為廣播和非廣播模式,如果是廣播模式中,StreamControlle 的實現是由如下所示實現的,他們的基礎關係如下圖所示:

  • _SyncBroadcastStreamController

  • _AsyncBroadcastStreamController

i

廣播和非廣播的區別在於呼叫 _createSubscription 時,內部對介面類 _StreamControllerLifecycle 的實現,同時它們的差異在於:

  • _StreamController 裡判斷了如果 Stream_isInitialState 的,也就是訂閱過的,就直接報錯 "Stream has already been listened to." ,只有未訂閱的才建立 StreamSubscription

  • _BroadcastStreamController 中,_isInitialState 的判斷被去掉了,取而代之的是 isClosed 判斷,並且在廣播中, _sendData 是一個 forEach 執行:

  _forEachListener((_BufferingStreamSubscription<T> subscription) {
      subscription._add(data);
    });
複製程式碼

7、Stream 變換

Stream 是支援變換處理的,針對 Stream 我們可以經過多次變化來得到我們需要的結果。那麼這些變化是怎麼實現的呢?

如下圖所示,一般操作符變換的 Stream 實現類,都是繼承了 _ForwardingStream , 在它的內部的_ForwardingStreamSubscription 裡,會通過上一個 Pre A Streamlisten 新增 _handleData 回撥,之後在回撥裡再次呼叫新的 Current B Stream_handleData

所以事件變化的本質就是,變換都是對 Streamlisten 巢狀呼叫組成的。

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

同時 Stream 還有轉換為 Future , 如 firstWhereelementAtreduce 等操作符方法,基本都是建立一個內部 _Future 例項,然後再 listen 的回撥用呼叫 Future 方法返回。

二、StreamBuilder

如下程式碼所示, 在 Flutter 中通過 StreamBuilder 構建 Widget ,只需提供一個 Stream 例項即可,其中 AsyncSnapshot 物件為資料快照,通過 data 快取了當前資料和狀態,那 StreamBuilder 是如何與 Stream 關聯起來的呢?

StreamBuilder<List<String>>(
    stream: dataStream,
    initialData: ["none"],
    ///這裡的 snapshot 是資料快照的意思
    builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
      ///獲取到資料,為所欲為的更新 UI
      var data = snapshot.data;
      return Container();
    });

複製程式碼

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

如上圖所示, StreamBuilder 的呼叫邏輯主要在 _StreamBuilderBaseState 中,_StreamBuilderBaseStateinitStatedidUpdateWidget 中會呼叫 _subscribe 方法,從而呼叫 Streamlisten,然後通過 setState 更新UI,就是這麼簡單有木有?

我們常用的 setState 中其實是呼叫了 markNeedsBuildmarkNeedsBuild 內部標記 elementdiry ,然後在下一幀 WidgetsBinding.drawFrame 才會被繪製,這可以看出 setState 並不是立即生效的哦。

三、rxdart

其實無論從訂閱或者變換都可以看出, Dart 中的 Stream 已經自帶了類似 rx 的效果,但是為了讓 rx 的使用者們更方便的使用,ReactiveX 就封裝了 rxdart 來滿足使用者的熟悉感,如下圖所示為它們的對應關係:

Flutter完整開發實戰詳解(十一、全面深入理解Stream)

rxdart 中, Observable 是一個 Stream,而 Subject 繼承了 Observable 也是一個 Stream,並且 Subject 實現了 StreamController 的介面,所以它也具有 Controller 的作用。

如下程式碼所示是 rxdart 的簡單使用,可以看出它遮蔽了外界需要對 StreamSubscriptionStreamSink 等的認知,更符合 rx 歷史使用者的理解。

final subject = PublishSubject<String>();

subject.stream.listen(observerA);
subject.add("AAAA1");
subject.add("AAAA2"));

subject.stream.listen(observeB);
subject.add("BBBB1");
subject.close();
複製程式碼

這裡我們簡單分析下,以上方程式碼為例,

  • PublishSubject 內部實際建立是建立了一個廣播 StreamController<T>.broadcast

  • 當我們呼叫 add 或者 addStream 時,最終會呼叫到的還是我們建立的 StreamController.add

  • 當我們呼叫 onListen 時,也是將回撥設定到 StreamController 中。

  • rxdart 在做變換時,我們獲取到的 Observable 就是 this,也就是 PublishSubject 自身這個 Stream ,而 Observable 一系列的變換,也是基於建立時傳入的 stream 物件,比如:

  @override
  Observable<S> asyncMap<S>(FutureOr<S> convert(T value)) =>
      Observable<S>(_stream.asyncMap(convert));
複製程式碼

所以我們可以看出來,rxdart 只是對 Stream 進行了概念變換,變成了我們熟悉的物件和操作符,而這也是為什麼 rxdart 可以在 StreamBuilder 中直接使用的原因。

所以,到這裡你對 Flutter 中 Stream 有全面的理解了沒?

自此,第十一篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:
文章

《Flutter完整開發實戰詳解(一、Dart語言和Flutter基礎)》

《Flutter完整開發實戰詳解(二、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(三、 打包與填坑篇)》

《Flutter完整開發實戰詳解(四、Redux、主題、國際化)》

《Flutter完整開發實戰詳解(五、 深入探索)》

《Flutter完整開發實戰詳解(六、 深入Widget原理)》

《Flutter完整開發實戰詳解(七、 深入佈局原理)》

《Flutter完整開發實戰詳解(八、 實用技巧與填坑)》

《Flutter完整開發實戰詳解(九、 深入繪製原理)》

《Flutter完整開發實戰詳解(十、 深入圖片載入流程)》

《Flutter完整開發實戰詳解(十一、全面深入理解Stream)》

《跨平臺專案開源專案推薦》

《移動端跨平臺開發的深度解析》

《React Native 的未來與React Hooks》

我們還會再見嗎?

相關文章