Kotlin协程的那些事 ---- 流 Flow

在协程中,如果需要通过异步的方式来返回数据,可以通过async的方式,但是这种方式存在的局限性在于,只能返回一个值,如果通过异步的方式1次返回多个值,可以使用Flow

Flow 冷流

  • 1 Flow的认识
    • 1.1 Flow构建器
    • 1.2 数据的采集
  • 2 Flow的上下文
  • 3 Flow的取消
  • 4 Flow的背压
    • 4.1 背压的处理方式 --- buffer
    • 4.2 背压的处理方式 --- flowOn
    • 4.2 背压的处理方式 --- collectLatest
  • 5 Flow的操作符
  • 6 Flow的异常处理
    • 6.1 上游异常处理 --- catch
    • 6.2 下游异常处理 --- try catch
  • 7 Flow的完成 --- onCompletion

1 Flow的认识

Flow是通过异步的方式,1次返回多个值,和异步队列相对比,异步队列是阻塞线程的,但是Flow是挂起而不是阻塞

1.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就是专门为数据集合服务的,用于将集合中的数据通过异步的方式发射出来

1.2 数据的采集

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")
 }

2 Flow的上下文

因为流的构建是不用在协程中的,但是流的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

3 Flow的取消

紧接着上边的例子,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")
}

4 Flow的背压

既然使用到了流,那么背压就是老生常谈的话题。何为背压,例如上地铁、下地铁,你都不需要自己走下去,就有人在背后推着你前进
Kotlin协程的那些事 ---- 流 Flow_第1张图片
当生产者的效率大于消费者的效率时,上游发送的数据,下游来不及处理,这就是背压

viewModelScope.launch {
   val time = measureTime {
        simpleFlow().collect {
            delay(1000)
            Log.e(TAG,"接收 --- $it")
        }
    }
    Log.e(TAG,"消耗的时间 $time")
}

上游发送数据为0.5s发送一次,在下游1s种接收一次,一共发送5次,总耗时7.5s

4.1 背压的处理方式 — buffer

viewModelScope.launch {
   val time = measureTime {
        simpleFlow().buffer().collect {
            delay(1000)
            Log.e(TAG,"接收 --- $it")
        }
    }
    Log.e(TAG,"消耗的时间 $time")
}
消耗的时间 5.592202s

buffer是一个缓存区,在collect之前,数据会全部发送到缓存区,然后消费者端再依次处理,而不是生产者生产1个,消费者就消费1个,这样效率就提高了一些。

4.2 背压的处理方式 — flowOn

fun simpleFlow() = flow {

    Log.e(TAG, "${Thread.currentThread().name}")
    for (i in 1..5) {
        delay(500)
        emit(i)
    }
}.flowOn(Dispatchers.Default)

将上游的线程切换到default异步线程中,在后台处理数据,其实跟添加缓存一个效果

4.2 背压的处理方式 — collectLatest

viewModelScope.launch {
    val time = measureTime {
        simpleFlow().buffer().collectLatest {
            delay(1000)
            Log.e(TAG,"接收 --- $it")
        }
    }
    Log.e(TAG,"消耗的时间 $time")
}

消耗的时间 3.638770s

如果只关注最新的值,那么可以使用collectLatest,只获取最后一次发送的值,省略了中间数据的接收过程

5 Flow的操作符

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收集
Kotlin协程的那些事 ---- 流 Flow_第2张图片
但如果使用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

Kotlin协程的那些事 ---- 流 Flow_第3张图片
除此之外,还有 flatMapMerge

(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 …

6 Flow的异常处理

6.1 上游异常处理 — catch

fun exp() = flow<String> {
   emit("123")
   throw IllegalArgumentException()
}

通常在上游的处理,通过catch来获取异常信息

exp()
    .catch { e->
        Log.e(TAG,"exp -- $e")
    }
    .collect {
    Log.e(TAG,"emit --- $it")
}

6.2 下游异常处理 — try catch

exp().catch { e->
    Log.e(TAG,"exp -- $e")
}
.collect {
    try {
        Log.e(TAG,"emit --- $it")
        throw IOException()
    }catch (e:Exception){
        Log.e(TAG,"下游异常 $e")
    }
}

7 Flow的完成 — onCompletion

一般情况下,如果上下游没有出现异常,在finally中就完成了这个流,并释放了资源;或者在onCompletion中,代表这个流已经完成,但是在onCompletion中,不能catch异常,只能通过上述的方式catch异常

exp().onCompletion { e->
    Log.e(TAG,"onCompletion $e")
}
.collect {
   Log.e(TAG,"emit --- $it")
}

你可能感兴趣的:(技术,kotlin,java,开发语言,flow,协程)