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

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

文章匯總地址:

Flutter 完整實戰實戰系列文章專欄

Flutter 番外的世界系列文章專欄

一、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 的內部工作流程。

image

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 內部在執行異步操作過程執行流程:

image

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 去回調數據 。

image

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 嵌套調用組成的。

image

同時 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();
    });

image

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

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

三、rxdart

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

image

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 有全面的理解了沒?

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

資源推薦

完整開源項目推薦:
我們還會再見嗎?
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 作為系列文章的第十二篇,本篇將通過 scope_model 、 BloC 設計模式、flutter_redux 、...
    戀貓月亮閱讀 1,941評論 4 14
  • Dart 部分 其實學習過 JavaScript 或者 Java/Kotlin 的人,在學習 Dart 上幾乎是沒...
    Android高級工程師閱讀 1,661評論 0 10
  • Dart是基于事件循環機制的單線程模型 一條執行線上,同時且只能執行一個任務(事件),其他任務都必須在后面排隊等待...
    Hankkinn閱讀 951評論 0 1
  • 約定的力量 早上我打開凱叔講故事叫他起床,他動了下,又睡著了,于是我在他耳邊輕聲說:要起床了,然后他就起來了。就是...
    Eva5266閱讀 331評論 0 1
  • 悄悄藏起來偷拍你, 怕樓梯太緩 怕光影太長, 怕掏心掏肺的都是假話。 怕這一瞬是寫不完的作業, 是補不了色的彩霞。...
    Karimah蘇閱讀 162評論 0 1