在协程中,如果需要通过异步的方式来返回数据,可以通过async的方式,但是这种方式存在的局限性在于,只能返回一个值,如果通过异步的方式1次返回多个值,可以使用Flow
Flow是通过异步的方式,1次返回多个值,和异步队列相对比,异步队列是阻塞线程的,但是Flow是挂起而不是阻塞
和协程一样,流的使用同样需要构建的存在,流的构建通常有以下几种方式
flow
返回的就是一个Flow对象,支持泛型
suspend fun simpleFlow() = flow<Int> {
for (i in 1..5){
delay(1000)
emit(i)
}
}
flowOf
其他它内部的实现,就是上面的这种方式,通过遍历这个集合来完成
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
for (element in elements) {
emit(element)
}
}
asFlow
同样是遍历数据集合来将数据发射出来
public fun <T> Iterable<T>.asFlow(): Flow<T> = flow {
forEach { value ->
emit(value)
}
}
通过这几种构建器,基本可以了解的就是,Flow就是专门为数据集合服务的,用于将集合中的数据通过异步的方式发射出来
Flow将数据emit出来之后,下游通过collect来接收数据,这个时候,上游才启动发射数据,因此Flow也被称为是冷流
调用collect的时候,必须要在协程作用域或者挂起函数中使用
suspend fun testFlow(){
simpleFlow().collect {
Log.e(TAG,"get value $it")
}
}
simpleFlow().onEach {
delay(1000)
}.collect {
Log.e(TAG,"get value is $it")
}
因为流的构建是不用在协程中的,但是流的collect是需要在协程域中的,因此collect所在协程的上下文会保存在Flow构建器中,两者使用同一个协程上下文
那么这里就会有一个问题,如果collect在主线程中收集,那么Flow的构建器也是在主线程中,上游经常做一些耗时操作,势必会阻塞主线程,而且在Flow的构建器中也不能使用withContext来改变Dispatchers,那么该如何切换上下文?
可以使用flowOn
simpleFlow()
.flowOn(Dispatchers.IO)
.onEach {delay(1000)}
.collect {
Log.e(TAG,"get value is $it ${Thread.currentThread().name}")
}
DefaultDispatcher-worker-1
get value is 1 main
这样,上游就在IO线程中执行,下游在主线程中执行
因为上下文的继承问题,下游会在调用方所在的线程,如果想要指定下游所在的线程,使用launchIn来代替collect在指定协程中收集数据
fun simpleFlow() = flow {
Log.e(TAG,"${Thread.currentThread().name}")
for (i in 1..5){
delay(1000)
emit(i)
}
}.flowOn(Dispatchers.IO)
.onEach {
Log.e(TAG,"get value is $it ${Thread.currentThread().name}")
}
在调用时使用launchIn,就不用使用collect,直接在onEach过渡操作符中收集数据即可
simpleFlow().launchIn(CoroutineScope(Dispatchers.IO))
DefaultDispatcher-worker-1
get value is 1 DefaultDispatcher-worker-1
紧接着上边的例子,launchIn返回的是一个Job对象,那么就可以取消
val job = simpleFlow().launchIn(CoroutineScope(Dispatchers.IO))
delay(2000)
job.cancel()
除此之外,常规的流取消操作有:
withTimeout
withTimeout(3000) {
list.onEach {
delay(1000)
}
.collect {
Log.e(TAG, "发射 --- $it")
}
}
当超时之后,流就被取消
cancel
viewModelScope.launch {
simpleFlow().collect {
if(it > 3) cancel()
Log.e(TAG,"emit --- $it")
}
}
为什么使用cancel能够取消,是因为每次emit之前,都会ensureActive,因此当在collect的时候,在某个点取消了协程,那么流也会停止
那么对于一些CPU密集型任务,只是使用cancel是没法直接退出的
val list = mutableListOf(1, 2, 3, 4, 5).asFlow()
fun testFlow() {
viewModelScope.launch {
list.collect {
if(it > 3) cancel()
Log.e(TAG,"emit --- $it")
}
}
}
emit --- 1
emit --- 2
emit --- 3
emit --- 4
emit --- 5
这个时候,需要加上 cancellable 做取消检测,使得Flow能够支持取消
list.cancellable().collect {
if(it > 3) cancel()
Log.e(TAG,"emit --- $it")
}
既然使用到了流,那么背压就是老生常谈的话题。何为背压,例如上地铁、下地铁,你都不需要自己走下去,就有人在背后推着你前进
当生产者的效率大于消费者的效率时,上游发送的数据,下游来不及处理,这就是背压
viewModelScope.launch {
val time = measureTime {
simpleFlow().collect {
delay(1000)
Log.e(TAG,"接收 --- $it")
}
}
Log.e(TAG,"消耗的时间 $time")
}
上游发送数据为0.5s发送一次,在下游1s种接收一次,一共发送5次,总耗时7.5s
viewModelScope.launch {
val time = measureTime {
simpleFlow().buffer().collect {
delay(1000)
Log.e(TAG,"接收 --- $it")
}
}
Log.e(TAG,"消耗的时间 $time")
}
消耗的时间 5.592202s
buffer是一个缓存区,在collect之前,数据会全部发送到缓存区,然后消费者端再依次处理,而不是生产者生产1个,消费者就消费1个,这样效率就提高了一些。
fun simpleFlow() = flow {
Log.e(TAG, "${Thread.currentThread().name}")
for (i in 1..5) {
delay(500)
emit(i)
}
}.flowOn(Dispatchers.Default)
将上游的线程切换到default异步线程中,在后台处理数据,其实跟添加缓存一个效果
viewModelScope.launch {
val time = measureTime {
simpleFlow().buffer().collectLatest {
delay(1000)
Log.e(TAG,"接收 --- $it")
}
}
Log.e(TAG,"消耗的时间 $time")
}
消耗的时间 3.638770s
如果只关注最新的值,那么可以使用collectLatest,只获取最后一次发送的值,省略了中间数据的接收过程
Flow作为函数式编程,操作符自然少不了,如果使用过RxJava之类的框架,想必对于其中的操作符很熟悉了,FLow也有很多对应的操作符
map 转换操作符
simpleFlow()
.buffer()
.map { it * it }
map可以将上游发送来的数据做一次转换,例如将数据平方,或者将数据变换一个类型,Int转换为String,在下游接收的数据也会改为转换后数据类型
take 限长操作符
simpleFlow().buffer().take(2).collect {
delay(1000)
Log.e(TAG, "接收 --- $it")
}
可以取序列的前n个元素打印输出
collect toList 末端操作符
val list = simpleFlow().buffer().take(2).toList()
Log.e(TAG,"$list")
toList toSet … 都可以把接收到的数据转换为List Flow <-----> List
zip 组合操作符
val time = measureTime {
simpleFlow().zip(simpleFlow2()){ f1,f2->
f1 + f2
}.collect {
Log.e(TAG,"it --- $it")
}
}
Log.e(TAG,"消耗的时间 $time")
消耗的时间 7.659763s
将两个流组合到一起,其中simpleFlow发送速度为1s,simpleFlow2发射速度为1.5s,那么两者组合之后,以生产速度最慢的为标准,最终总时长为7.5s
flatMapConcat 展平操作符
两个流,每个值都会触发对另一流的请求
请求的流
fun get(i:Int)= flow<String> {
emit("$i is First")
delay(500)
emit("$i is Second")
}
如果使用map转换,那么拿到的还是一个Flow,收集的时候得调用两次collect收集
但如果使用flatMapConcat,可以直接将数据展平,拿到的还是一个流,就不需要调用两次collect,可以直接输出
E/UserViewModel: map ---- 2 is First
E/UserViewModel: map ---- 2 is Second
E/UserViewModel: map ---- 2 is Third
E/UserViewModel: map ---- 3 is First
E/UserViewModel: map ---- 3 is Second
E/UserViewModel: map ---- 3 is Third
(1..3).asFlow()
.flatMapMerge {
get(it)
}
.collect {
Log.e(TAG, "map ---- $it")
}
map ---- 1 is First
map ---- 2 is First
map ---- 3 is First
map ---- 1 is Second
map ---- 2 is Second
map ---- 3 is Second
E/UserViewModel: map ---- 1 is Third
E/UserViewModel: map ---- 2 is Third
E/UserViewModel: map ---- 3 is Third
其中跟flatMapConcat的区别在于,先执行第一个emit,然后再执行第二个emit …
fun exp() = flow<String> {
emit("123")
throw IllegalArgumentException()
}
通常在上游的处理,通过catch来获取异常信息
exp()
.catch { e->
Log.e(TAG,"exp -- $e")
}
.collect {
Log.e(TAG,"emit --- $it")
}
exp().catch { e->
Log.e(TAG,"exp -- $e")
}
.collect {
try {
Log.e(TAG,"emit --- $it")
throw IOException()
}catch (e:Exception){
Log.e(TAG,"下游异常 $e")
}
}
一般情况下,如果上下游没有出现异常,在finally中就完成了这个流,并释放了资源;或者在onCompletion中,代表这个流已经完成,但是在onCompletion中,不能catch异常,只能通过上述的方式catch异常
exp().onCompletion { e->
Log.e(TAG,"onCompletion $e")
}
.collect {
Log.e(TAG,"emit --- $it")
}