在 Flutter 中有两种处理异步操作的方式 Future
和 Stream
,Future
用于处理单个异步操作,Stream
用来处理连续的异步操作。比如往水杯倒水,将一个水杯倒满为一个 Future
,连续的将多个水杯倒满就是 Stream
。
Stream
就是事件流或者管道,事件流相信大家并不陌生,简单的说就是:基于事件流驱动设计代码,然后监听订阅事件,并针对事件变换处理响应。
Flutter 中,整个 Stream
设计外部暴露的对象主要如下图,主要包含了 StreamController
、Sink
、Stream
、StreamSubscription
四个对象。
Flutter中 Stream
、StreamController
、StreamSink
和 StreamSubscription
都是 abstract
对象,他们对外抽象出接口,而内部实现对象大部分都是 _
开头的如 _SyncStreamController
、ControllerStream
等私有类,在这基础上整个流程概括起来就是:
有一个事件源叫
Stream
,为了方便控制Stream
,官方提供了使用StreamController
作为管理;同时它对外提供了StreamSink
对象作为事件输入口,可通过sink
属性访问; 又提供stream
属性提供Stream
对象的监听和变换,最后得到的StreamSubscription
可以管理事件的订阅。
所以我们可以总结出:
Stream
过程的控制,提供各类接口用于创建各种事件流。add
, addStream
等。listen
、 where
。cacenl
、pause
,同时在内部也是事件的中转关键。回到 Stream
的工作流程上,在上图中我们知道, 通过 StreamSink.add
添加一个事件时, 事件最后会回调到 listen
中的 onData
方法,这个过程是通过 zone.runUnaryGuarded
执行的,这里 zone.runUnaryGuarded
是什么作用后面再说,我们需要知道这个 onData
是怎么来的?
如上图,通过源码我们知道:
1、Stream
在 listen
的时候传入了 onData
回调,这个回调会传入到 StreamSubscription
中,之后通过 zone.registerUnaryCallback
注册得到 _onData
对象( 不是前面的 onData
回调哦 )。
2、StreamSink
在添加事件是,会执行到 StreamSubscription
中的 _sendData
方法,然后通过 _zone.runUnaryGuarded(_onData, data);
执行 1 中得到的 _onData
对象,触发 listen
时传入的回调方法。
可以看出整个流程都是和 StreamSubscription
相关的,现在我们已经知道从 事件入口到事件出口 的整个流程时怎么运作的,那么这个过程是**怎么异步执行的呢?其中频繁出现的 zone
是什么?
首先我们需要知道,Stream 是怎么实现异步的?
这就需要说到 Dart 中的异步实现逻辑了,因为 Dart 是 单线程应用 ,和大多数单线程应用一样,Dart 是以 消息循环机制 来运行的,而这里面主要包含两个任务队列,一个是 microtask 内部队列,一个是 event 外部队列,而 microtask 的优先级又高于 event 。
默认的在 Dart 中,如 点击、滑动、IO、绘制事件 等事件都属于 event 外部队列,microtask 内部队列主要是由 Dart 内部产生,而 Stream
中的执行异步的模式就是 scheduleMicrotask
了。
因为 microtask 的优先级又高于 event ,所以如果 microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿哦。
如下图,就是 Stream 内部在执行异步操作过程执行流程:
那么 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>(····);
}, 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
的异步操作,官方的解释是:在此区域中使用参数执行给定操作并捕获同步错误。 类似的还有 runUnary
、 runBinaryGuarded
等,所以我们知道前面提到的 zone.runUnaryGuarded
就是 Flutter 在运行的这个 zone 里执行已经注册的 _onData
,并捕获异常。
前面我们说了 Stream
的内部执行流程,那么同步和异步操作时又有什么区别?具体实现时怎么样的呢?
我们以默认 Stream
流程为例子, StreamController
的工厂创建可以通过 sync
指定同步还是异步,默认是异步模式的。 而无论异步还是同步,他们都是继承了 _StreamController
对象,区别还是在于 mixins
的是哪个 _EventDispatch
实现:
_AsyncStreamControllerDispatch
_SyncStreamControllerDispatch
上面这两个 _EventDispatch
最大的不同就是在调用 sendData
提交事件时,是直接调用 StreamSubscription
的 _add
方法,还是调用 _addPending(new _DelayedData
方法的区别。
如下图, 异步执行的逻辑就是上面说过的 scheduleMicrotask
, 在 _StreamImplEvents
中 scheduleMicrotask
执行后,会调用 _DelayedData
的 perform
,最后通过 _sendData
触发 StreamSubscription
去回调数据 。
在 Stream
中又非为广播和非广播模式,如果是广播模式中,StreamControlle
的实现是由如下所示实现的,他们的基础关系如下图所示:
_SyncBroadcastStreamController
_AsyncBroadcastStreamController
广播和非广播的区别在于调用 _createSubscription
时,内部对接口类 _StreamControllerLifecycle
的实现,同时它们的差异在于:
在 _StreamController
里判断了如果 Stream
是 _isInitialState
的,也就是订阅过的,就直接报错 "Stream has already been listened to." ,只有未订阅的才创建 StreamSubscription
。
在 _BroadcastStreamController
中,_isInitialState
的判断被去掉了,取而代之的是 isClosed
判断,并且在广播中, _sendData
是一个 forEach
执行:
_forEachListener((_BufferingStreamSubscription subscription) {
subscription._add(data);
});
Stream
是支持变换处理的,针对 Stream
我们可以经过多次变化来得到我们需要的结果。那么这些变化是怎么实现的呢?
如下图所示,一般操作符变换的 Stream
实现类,都是继承了 _ForwardingStream
, 在它的内部的_ForwardingStreamSubscription
里,会通过上一个 Pre A Stream
的 listen
添加 _handleData
回调,之后在回调里再次调用新的 Current B Stream
的 _handleData
。
所以事件变化的本质就是,变换都是对 Stream
的 listen
嵌套调用组成的。
同时 Stream
还有转换为 Future
, 如 firstWhere
、 elementAt
、 reduce
等操作符方法,基本都是创建一个内部 _Future
实例,然后再 listen
的回调用调用 Future
方法返回。
如下代码所示, 在 Flutter 中通过 StreamBuilder
构建 Widget ,只需提供一个 Stream
实例即可,其中 AsyncSnapshot
对象为数据快照,通过 data
缓存了当前数据和状态,那 StreamBuilder
是如何与 Stream
关联起来的呢?
StreamBuilder>(
stream: dataStream,
initialData: ["none"],
///这里的 snapshot 是数据快照的意思
builder: (BuildContext context, AsyncSnapshot> snapshot) {
///获取到数据,为所欲为的更新 UI
var data = snapshot.data;
return Container();
});
如上图所示, StreamBuilder
的调用逻辑主要在 _StreamBuilderBaseState
中,_StreamBuilderBaseState
在 initState
、didUpdateWidget
中会调用 _subscribe
方法,从而调用 Stream
的 listen
,然后通过 setState
更新UI,就是这么简单有木有?
我们常用的
setState
中其实是调用了markNeedsBuild
,markNeedsBuild
内部标记element
为diry
,然后在下一帧WidgetsBinding.drawFrame
才会被绘制,这可以看出setState
并不是立即生效的哦。
其实无论从订阅或者变换都可以看出, Dart 中的 Stream
已经自带了类似 rx
的效果,但是为了让 rx
的用户们更方便的使用,ReactiveX 就封装了 rxdart
来满足用户的熟悉感,如下图所示为它们的对应关系:
在 rxdart
中, Observable
是一个 Stream
,而 Subject
继承了 Observable
也是一个 Stream
,并且 Subject
实现了 StreamController
的接口,所以它也具有 Controller 的作用。
如下代码所示是 rxdart
的简单使用,可以看出它屏蔽了外界需要对 StreamSubscription
和 StreamSink
等的认知,更符合 rx
历史用户的理解。
final subject = PublishSubject();
subject.stream.listen(observerA);
subject.add("AAAA1");
subject.add("AAAA2"));
subject.stream.listen(observeB);
subject.add("BBBB1");
subject.close();
这里我们简单分析下,以上方代码为例,
PublishSubject
内部实际创建是创建了一个广播 StreamController
当我们调用 add
或者 addStream
时,最终会调用到的还是我们创建的 StreamController.add
当我们调用 onListen
时,也是将回调设置到 StreamController
中
rxdart
在做变换时,我们获取到的 Observable
就是 this,也就是 PublishSubject
自身这个 Stream
,而 Observable
一系列的变换,也是基于创建时传入的 stream
对象,比如:
@override
Observable asyncMap(FutureOr convert(T value)) =>
Observable(_stream.asyncMap(convert));
所以我们可以看出来,rxdart
只是对 Stream
进行了概念变换,变成了我们熟悉的对象和操作符,而这也是为什么 rxdart
可以在 StreamBuilder
中直接使用的原因。
流可以分为两类:
Controller.sink.add添加事件
addError
Controller.close关闭水龙头
StreamController
,StreamSink
用做事件入口,Stream
对象用于监听,StreamSubscription
管理事件订阅,最后在不需要时关闭可以在多处监听它返回的结果
Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream
在streamController传入使用
map 包装转化
where 过滤
distinct去重
Stream.take(int count)
上面创建了一个无限每隔一秒发送一次事件的流,如果我们想指定只发送10个事件则,用take。下面就只会打印出0-9
void _stream() async{
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10); //指定发送事件个数
await for(int i in stream ){
print(i);
}
}
Stream.takeWhile
上面这种方式我们是只制定了发送事件的个数,如果我们也不知道发送多少个事件,我们可以从返回的结果上做一个返回值的限制,上面结果也可以用以下方式实现
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
// stream = stream.take(10);
stream = stream.takeWhile((data) {
return data < 10;
});
await for (int i in stream) {
print(i);
}
}
Stream.skip(int count)
skip可以指定跳过前面的几个事件,如下会跳过0和1,输出 2-9;
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
stream = stream.skip(2);
await for (int i in stream) {
print(i);
}
}
Stream.skipWhile
可以指定跳过不发送事件的指定条件,如下跳过0-4的输出,输出5-9
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
stream = stream.skipWhile((data) => data<5);
await for (int i in stream) {
print(i);
}
}
Stream.toList()
将流中所有的数据收集存放在List中,并返回 Future对象,listData里面 0-9
1.这个是一个异步方法,要结果则需要使用await关键字
2.这个是等待Stream当流结束时,一次返回结果
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
List listData = await stream.toList();
for (int i in listData) {
print(i);
}
}
Stream. listen()
这是一种特定的可以用于监听数据流的方式,和 forEach循环的效果一致,但是返回的是
StreamSubscription
对象,如下也会输出0-9,同时打印出 ”流已完成“看一下源码这种方式可以接收
StreamSubscription
listen(void onData(T event), {Function onError, void onDone(), bool cancelOnError}); 1.
onData
是接收到数据的处理,必须要实现的方法2.
onError
流发生错误时候的处理3.
onDone
流完成时候调取4.
cancelOnError
发生错误的时候是否立马终止
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
stream.listen((data) {
print(data);
}, onError: (error) {
print("流发生错误");
}, onDone: () {
print("流已完成");
}, cancelOnError: false);
}
Stream. forEach()
这中操作和
listen()
的方式基本差不多,也是一种监听流的方式,这只是监听了onData
,下面代码也会输出0-9
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
stream.forEach((data) {
print(data);
});
}
Stream .length
用于获取等待流中所有事件发射完成之后统计事件的总数量,下面代码会输出 10
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.take(10);
var allEvents = await stream.length;
print(allEvents);
}
Stream.where
在流中添加筛选条件,过滤掉一些不想要的数据,满足条件返回true,不满足条件返回false,如下我们筛选出流中大于5小于10的数据
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.where((data)=>data>5);
stream = stream.where((data)=> data<10);
await for(int i in stream){
print(i);
}
}
stream.map
对流中的数据进行一些变换,以下是我对Stream的每个数据都加1
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.map((data) => data + 1);
await for (int i in stream) {
print(i);
}
}
Stream.expand
对流中的数据进行一个扩展,如下,会输出1,1,2,2,3,3….
void _stream() async {
Duration interval = Duration(seconds: 1);
Stream stream = Stream.periodic(interval, (data) => data);
stream = stream.expand((data)=>[data,data]);
stream.listen((data)=>print(data),onError:(error)=> print("发生错误") );
}
Stream.transform
如果我们在在流流转的过程中需要进行一些转换和控制我们则需要使用到transform,接收一个
StreamTransformer
,S表示转换之前的类型,T表示转换后的输入类型,如下代码我们会接收到三组数字模拟输入了三次密码,并判断真确的密码,同时输出密码正确和密码错误:
void _stream() async {
var stream = Stream.fromIterable([123456,234567,678901]);
var st = StreamTransformer.fromHandlers(
handleData: (int data, sink) {
if (data == 678901) {
sink.add("密码输入正确,正在开锁。。。");
} else {
sink.add("密码输入错误...");
}
});
stream.transform(st).listen((String data) => print(data),
onError: (error) => print("发生错误"));
}
输入如下结果
I/flutter (18980): 密码输入错误...
I/flutter (18980): 密码输入错误...
I/flutter (18980): 密码输入正确,正在开锁。。。