前言
在上一篇文章我们已经了解了Kotlin语言特性中关于协程的概念和要素的运用,但在我们实际应用中,我们不会直接使用类似Continuation
来创建一个协程并启动,因为Kotlin官方给出了框架级别的支持,这样大大提高了我们的开发效率。
一、协程框架概述
语言级别支持 VS 框架级别支持
语言级别支持: 主要提供了Kotlin标准库的API以及对协程提供语义上的支持,其中包括协程上下文
、拦截器
以及挂起函数
。
框架级别支持:提供了更方便的上层业务开发支持,主要是提供了Job
、调度器
、作用域
、Channel
、Flow
以及Select
等特性。
协程框架:kotlinx.coroutines
- 官方协程框架,基于标准库实现的特性封装
- 地址:github.com/Kotlin/kotl…
协程框架的引入
//协程基础库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0"
//协程Android库,提供Android UI调度
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0"
Kotlin协程的启动模式
协程框架kotlinx.coroutines
通过GlobalScope.launch
方法创建和启动协程:
GlobalScope.launch(start = CoroutineStart.DEFAULT){
}
start = CoroutineStart.DEFAULT
代表的是通过什么方式启动协程,而Kotlin协程的启动模式主要分为以下四种:
-
DEFAULT模式: 创建协程后立即开始调度协程体,调度前若取消则直接取消(这里要注意的是当前线程通过lunch发起一个协程后,马上就返回了,当前线程继续执行自己后面的逻辑,即协程开始,发起协程的线程也继续)
-
ATOMIC: 立即开始调度,且在第一个挂起点前不能被取消
-
LAZY: 只有在需要的(start/join/await)时开始调度,类似于使用
createCoroutine
方法仅仅创建了一个协程,然后再需要调用的地方才启动。 -
UNDISPATCHED: 立即在当前线程执行协程体,直到遇到第一个挂起点(后面执行的流程取决于调度器),这里的意思是当前线程的调度流程是会卡住的,要等到第一个挂起点才会回来当前线程的执行流程
Kotlin协程的调度器
协程调度器的本质内涵其实就是加了封装的协程拦截器,它将协程运行在指定的线程上,也可将其分派到线程池中,或者让她无限制的运行,调度器主要有以下四种(针对Java VM平台运行的功能解析):
- Default: 指定协程运行在非主线程,一般用于CPU密集型计算型任务,类似于一些数组排序,数据解析之类
- Main: 指定协程运行在主线程或者在Android中进行UI线程.一般用于处理UI绘制或者一些轻量级任务
- Unconfined: 未指定运行的线程,直接执行
- IO: 指定协程运行在非主线程上,一般用于网络IO任务或者本地文件数据读取,如网络请求、文件读写等
协程框架中channel、Flow、以及Select特性
- Channel: 热数据流,并发安全的通信机制,其中热数据流的意思是不主动触发,数据依旧返回来
- Flow: 冷数据流,协程的响应式API,其中冷数据流的意思是在你需要的时候,去触发了才会有数据回来,类似于RxJava中,只有我们去订阅了才会有数据,就是有驱动。
- Select: 可对多个挂起事件进行等待
二、协程框架的基本用法
协程框架常用的创建协程的方法主要方式是:
CoroutineScope.launch()
CoroutineScope.async()
CoroutineScope
实际上是一个接口,可使用实现了CoroutineScope
的一个单例GlobalScope
来调用launch()
或者async()
方法创建一个协程作用域,而作用域内的代码块就是协程
。
GlobalScope.launch
GlobalScope.launch{
println("协程当前线程:${Thread.currentThread().name}")
}
打印结果:协程当前线程:DefaultDispatcher-worker-1
当不指定协程运行在什么线程上的时候,它使用的默认的调度器:Default
,拦截器是DefaultDispatcher
,其打印的线程名字也是:DefaultDispatcher-worker-1,这个时候运行在非主线程上,android开发中是不可以做UI更新操作。当然,我们也可以在协程中切换线程:
GlobalScope.launch{
println("协程当前线程:${Thread.currentThread().name}")
withContext(Dispatchers.Main){
//协程中切换线程,这里切换到主线程
}
}
GlobalScope.launch(Dispatchers.Main)
使用Dispatchers.Main
可以指定启动的协程运行在主线程上:
GlobalScope.launch(Dispatchers.Main){
Log.e("协程当前线程:", Thread.currentThread().name)
//在Android开发中,此处可更新ui
}
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
GlobalScope
是一个单例,其源码如上面所示,我们可以看到GlobalScope
是没有再关联其他对象和组件的,那在Android应用中如果我们自己不去处理GlobalScope
创建的协程,那这些协程只会在app进程销毁的时候才会跟着销毁,很显然这种做法是不太安全的。这里官方推荐的推荐使用的是Kotlin协程在ktx
上的两个扩展库,lifecycle
扩展对应的是lifecycleScope
,另外一个viewModel
扩展对应的是viewModelScope
。
lifecycleScope和viewModelScope
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
通过lifecycleScope
创建的协程作用域具有感知生命周期的特性,它通过与Lifecycle
绑定实现跟随Lifecycle
销毁而销毁,在Activity/Fragment
使用此协程作用域时,界面销毁该协程也被销毁了,避免了协程泄漏的问题。而viewModelScope
同样具有类似的属性,它创建的协程会在ViewModel
销毁时自动取消,避免造成协程泄漏引起的内存问题。
协程的执行
private fun testLifecycleScope(){
Log.d(TAG,"testLifecycleScope start")
lifecycleScope.launch {
delay(2000)
textView.text = "LifecycleScope "
Log.d(TAG,Thread.currentThread().name)
Log.e(TAG,"协程结束")
}
Log.d(TAG,"testLifecycleScope end")
}
执行结果:
com.qisan.kotlinstu.MainActivity: testLifecycleScope start
com.qisan.kotlinstu.MainActivity: testLifecycleScope end
com.qisan.kotlinstu.MainActivity: main
com.qisan.kotlinstu.MainActivity: 协程结束
这里可以看到,协程内的阻塞不会影响协程外面的执行,另外可以看到lifecycleScope.launch
虽然是异步,但是它可以做UI更新操作,这和直接的线程操作是完全不一样的。
CoroutineScope.async()
async
方法主要作用是获取返回值和并发运行挂起函数。
private suspend fun getContent1():String{
delay(1000)
return "Kotlin"
}
private suspend fun getContent2():String{
delay(1000)
return "协程"
}
我们实现上面两个挂起函数,然后在lifecycleScope.launch
的作用域去执行它,看看它运行和耗时情况:
private fun testLifecycleScope(){
lifecycleScope.launch {
val startTime = System.currentTimeMillis()
val content_1 = getContent1()
val content_2 = getContent2()
Log.d(TAG,"$content_1 $content_2,程序耗时:${System.currentTimeMillis() - startTime}")
}
}
打印结果:
LifecycleScope: Kotlin 协程,程序耗时:2007
由程序耗时的结果我们可以看出来,getContent1()
和getContent2()
两个方法是顺序执行的。那现在我们把getContent1()
和getContent2()
两个方法的顺序调转一下,并且还是希望打印出:Kotlin 协程
,那这个时候就需要用到async
方法了:
private fun testLifecycleScope(){
lifecycleScope.launch {
val startTime = System.currentTimeMillis()
val content_2 = lifecycleScope.async { getContent2() }
val content_1 = lifecycleScope.async { getContent1() }
Log.d(TAG,"${content_1.await()} ${content_2.await()},程序耗时:${System.currentTimeMillis() - startTime}")
}
}
打印结果:
LifecycleScope: Kotlin 协程,程序耗时:1002
这里我们可以看到协程体执行的耗时只有1002毫秒,很明显getContent1()
和getContent2()
两个方法两个方法是同时执行了。这里要注意点是async
是要跟await()
挂起函数相结合使用的。这里要延伸说一下lifecycleScope.async {}
的返回值是Deferred
类型,而Deferred
接口继承自 Job
接口:
Deferred
public interface Deferred<out T> : Job {
public suspend fun await(): T
public val onAwait: SelectClause1<T>
@ExperimentalCoroutinesApi
public fun getCompleted(): T
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?
}
由于Deferred
继承Job
接口,所以Job
相关的操作在Deferred
上也可以用。
协程的取消
使用lifecycleScope
和viewModelScope
去创建协程作用域的时候都会跟调用者的生命周期绑定,一般情况下不用开发者手动去取消协程。我们关注一下使用MainScope()
创建协程的情况,MainScope()
是一个顶层函数,即它没有receiver
,看一下CoroutineScope
的源码:
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
ContextScope(SupervisorJob() + Dispatchers.Main)
参数里面代表的就是协程的作用域,那么它返回的就是一个上下文是SupervisorJob() + Dispatchers.Main
的作用域了,因为作用在主线程,所以在android开发中很多时候用作Activity/Fragment
中,当Activity销毁时调用cancel
方法来取消协程。下面我们使用MainScope()
创建协程作用域,并使用作用域去取消所有协程:
val mainScope = MainScope()
mainScope.launch {
Log.d(TAG,"第一个协程")
}
mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
handler.postDelayed({ mainScope.cancel() },500)
打印结果:
MainScope: 第一个协程
第一个协程没有延时,所以它执行了,第二个协程延时1000ms才打印,这个时候已经被取消了。所以mainScope.cancel()
取消一个协程作用域将同时取消此作用域下的所有子协程。这里要注意的是:一个已取消了的协程作用域内是不可再创建新协程的。 我们再来看看cancel()
方法的源码:
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
job.cancel(cause)
}
可以看到当调用cancel()
方法的时候是通过抛出一个异常类:CancellationException
来处理取消协程的。当然,我们上面的调用是没有传的,所以它创建一个默认的CancellationException
实例。
如果我们要单个取消要怎么处理呢?我们可以获取mainScope.launch{}
的实例Job
,通过
Job
调用cancel()
方法取消当前的协程。我们先看一下launch()
方法实现:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
通过源码可以看到launch()
返回的就是一个Job
,那这样就很好办了:
val mainScope = MainScope()
val job1 = mainScope.launch {
Log.d(TAG,"第一个协程")
}
val job2 = mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
job1.cancel()
打印结果:
MainScope: 第二个协程
通过打印的结果看到,第一个协程是被取消,第二个依旧是执行的,所以当一个协程被取消了不会影响到它同层级的协程。
上面mainScope.launch
是在Activity
调用的,它默认的调度器Dispatchers.Main)
,如果我们把调度器改成Dispatchers.Default
会变成什么样呢?
val mainScope = MainScope()
val job1 = mainScope.launch(Dispatchers.Default) {
Log.d(TAG,"第一个协程")
}
val job2 = mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
job1.cancel()
打印结果:
MainScope: 第一个协程
MainScope: 第二个协程
很可爱,job1取消后,协程体还是执行了打印。这是因为Dispatchers.Default
方式的调度会等待协程任务处理完才会取消。当我们调用cancel()
方法的时候,协程会进入到cancelling的状态,这个时候协程任务依旧会在进行。那么怎么让协程可以被取消呢?我们可以在在处理协程体内部逻辑的时候检查协程是否已取消。
ensureActive()
方法协作取消协程
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}
ensureActive()
方法非常便捷,它会在Job
处于不活跃时立即抛出异常,如果正常就继续往下执行。
val mainScope = MainScope()
val job1 = mainScope.launch(Dispatchers.Default) {
delay(50)
ensureActive()
Log.d(TAG,"第一个协程")
}
val job2 = mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
job1.cancel()
打印结果:
MainScope: 第二个协程
这个时候我们对job1
取消就起到效果了。当然,我们也可以再协程作用域内直接使用isActive
判断协程是否已取消。
yield()
方法协作取消协程
yield()
方法的可以用来检查Job
是否完成,如果Job
完成,则通过抛出CancellationException
异常来退出协程。直接看一下代码:
val job1 = mainScope.launch(Dispatchers.Default) {
delay(50)
yield()
Log.d(TAG,"第一个协程")
}
val job2 = mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
job1.cancel()
打印结果
MainScope: 第二个协程
yield()
方法的主要功能不仅仅是用来检查Job
是否完成,它还有挂起当前任务,释放此线程给其他任务去获取执行权,具体的探究这里就先不赘述了,后面再深入研究。
join()
方法
public suspend fun join()
严格来说,join()
是一个顶层函数哈,并且是一个suspend
函数,所以它只能在协程的作用域内调用。join()
函数的作用是暂停所在的 Coroutine
直到这个Coroutine
执行完再顺序执行其他逻辑代码,所以join
函数一般出现在另外一个Coroutine
(就是作用域),看下面d代码:
val mainScope = MainScope()
mainScope.launch {
val job2 = mainScope.launch {
delay(1000)
Log.d(TAG,"第二个协程")
}
job2.join()
val job1 = mainScope.launch(Dispatchers.Default) {
Log.d(TAG,"第一个协程")
}
}
打印结果:
MainScope: 第二个协程
MainScope: 第一个协程
这里要注意的是调用join()
函数的job
只有在其所有子级的任务完成时,这个挂起等待的任务才能算完成。另外,当调用job.join()
时,job
所处的作用域(Coroutine
)被取消了或者已经完成,这时候join()
将会抛出CancellationException
异常。
三、Channel
Channel的概念
- 非阻塞的通信基础设施,在协程之间完成消息传递
- 通信通道类似于阻塞队列(BlockingQueue)+挂起函数. (BlockingQueue的主要是两个方法是:
E take()
和put(E e)
。E take()
: 取走BlockingQueue里排在首位的对象,若BlockingQueue为空, 阻塞进入等待状态直到BlockingQueue有新的数据被加入;put(E e)
: 把对象 e 加到BlockingQueue里, 如果BlockQueue没有空间,则调用此方法的线程被阻塞,直到BlockingQueue里面有空间再继续)
Channel的分类
- RENDEZVOUS: send调用后挂起直到receive到达
- UNLIMITED: 无限容量,send调用后消息存在Channel并直接返回
- CONFLATED: 保留最新,即Channel里面只放最后一个message,那receive也只能获得最近一次send的值(应用的情景如更新下载进度)
- BUFFERED: 默认容量,可通过程序参数设置默认大小,默认为64
- FIXED: 固定容量,通过参数执行缓存大小,超过缓存大小send就挂起
Channel的使用
RENDEZVOUS方式
直接上代码:
suspend fun testChannel(){
val channel = Channel<Int>(Channel.RENDEZVOUS)
val jobProducer = GlobalScope.launch {
for(i in 1..2){
println("sending $i")
channel.send(i) //如果没有receive,那么send就会挂起
println("sent $i")
}
channel.close()
}
val jobConsumer = GlobalScope.launch {
while (!channel.isClosedForReceive){
println("receiving")
val value = channel.receiveCatching().getOrNull() //如果没有channel在send,receive也会挂起
println("received $value")
}
}
jobProducer.join()
jobConsumer.join()
}
打印结果:
sending 1
receiving
sent 1
sending 2
received 1
receiving
received 2
receiving
sent 2
received null
这个send和receive的顺序看起来是不一致,这是因为他们运行在两个线程,但是能确保的是只有receive接受到值后send才能执行。
UNLIMITED方式
UNLIMITED
方式是不管有没有receive在消费消息,只管发消息,send完就直接返回,但这里还是要注意不能无限往Channel
通道塞消息,这样内存消耗非常大,类似于Android 开发中的RemoteView
刷新添加action一样,其它使用基本跟上面的RENDEZVOUS
方式一样,这里就不再举例了。
CONFLATED方式
CONFLATED
方式是保留最后的消息在Channel通道,我们把上面例子的Channel的类型改成CONFLATED
,这个时候jobConsumer
应该只会接受到2:
val channel = Channel<Int>(Channel.CONFLATED)
val jobProducer = GlobalScope.launch {
for(i in 1..2){
println("sending $i")
channel.send(i)
println("sent $i")
}
channel.close()
}
val jobConsumer = GlobalScope.launch {
while (!channel.isClosedForReceive){
println("receiving")
val value = channel.receiveCatching().getOrNull()
println("received $value")
}
}
jobProducer.join()
jobConsumer.join()
打印结果:
sending 1
sent 1
sending 2
sent 2
receiving
received 2
看运行结果,send是都send出去了,但received的value只有2。
BUFFERED方式和FIXED方式
这两种方式主要是指定Channel
通道的大小,可以存放多少消息,其他的特性和RENDEZVOUS
方式类似。
Channel的关闭
- 调用
close
关闭Channel
- 关闭后调用
send
抛异常,isClosedForSend
返回true
- 关闭后调用
receive
可接收缓存的数据 - 缓存数据消费完后
receive
抛异常,isClosedForReceive
返回true
Channel的迭代
因为Channel
属于BlockingQueue,只是它支持挂起函数,所以一个集合类当然可以做for循环的迭代了。在上面的jobConsumer
也可以使用迭代了接收消息,这里有两种方式:
for循环迭代
for (i in channel){
println("received $i")
}
使用Iterator
迭代
val iterator = channel.iterator()
while (iterator.hasNext()){
val e = iterator.next()
println("receive $e")
}
这里的hasNext
等于代替了!channel.isClosedForReceive
的判断:
- 当有缓存的数据返回时,
hasNext
返回true
- 当未关闭且缓存为空时挂起
- 当正常关闭且缓存为空时返回
false
Channel的协程Builder
协程框架中提供了CoroutineScope.produce
和CoroutineScope.actor
方法来构造发送消息的生产者与接收消费者。
- CoroutineScope.produce: 启动一个生产消费者协程,返回
ReceiveChannel
,其他协程就可以使用这个Channel
来接收数据了 - CoroutineScope.actor: 启动一个消费者协程,返回一个
SendChannel
,其他生产者协程可以用来发送消息 - 以上两种Builder方式启动的协程结束后自动关闭对应的Channel
CoroutineScope.produce
使用:
suspend fun producer(){
//启动一个生产者协程,发送消息,返回接收消息的通道Channel
val receiveChannel = GlobalScope.produce<Int>(capacity = Channel.UNLIMITED) {
for (i in 1..2){
println("sending $i")
send(i)
println("sent $i")
}
}
//接收
val jobConsumer = GlobalScope.launch {
for (i in receiveChannel){
println("received $i")
}
}
jobConsumer.join()
}
打印结果:
sending 1
sent 1
sending 2
sent 2
received 1
received 2
CoroutineScope.actor
使用:
suspend fun consumer(){
//启动一个消费者协程,接收,返回发送的通道Channel
val sendChannel = GlobalScope.actor<Int>(capacity = Channel.UNLIMITED) {
for (i in this){
println("received $i")
}
}
val jobProducer = GlobalScope.launch {
for (i in 1..2){
println("sending $i")
sendChannel.send(i)
println("sent $i")
}
}
jobProducer.join()
}
打印结果:
sending 1
sent 1
sending 2
sent 2
received 1
received 2
BrodcastChannel
Channel
的元素只能被一个消费者消费BrodcastChannel
的元素会分发给所有的订阅者BrodcastChannel
不支持RENDEZVOUS
方式,因为BrodcastChannel
是一对多的模式
创建BrodcastChannel
的方式:
//通过一般channel强转
val channel = Channel<Int>(Channel.BUFFERED)
val broadcastChannel = channel.broadcast()
//直接通过BroadcastChannel构造
val broadcastChannel = BroadcastChannel<Int>(Channel.BUFFERED)
//通过Channel协程的builder模式构造
val broadcastChannel = GlobalScope.broadcast<Int> {
}
一般情况我们会通过CoroutineScope.broadcast
的方式创建一个BrodcastChannel
,这样方便订阅者接受,看下面代码:
val broadcastChannel = GlobalScope.broadcast<Int> {
for (i in 1..2){
send(i)
}
}
val jobConsumer_1 = GlobalScope.launch {
val receiveChannel_1 = broadcastChannel.openSubscription()
for (i in receiveChannel_1){
println("receiveChannel_1 value:$i")
}
}
val jobConsumer_2 = GlobalScope.launch {
val receiveChannel_2 = broadcastChannel.openSubscription()
for (i in receiveChannel_2){
println("receiveChannel_2 value:$i")
}
}
jobConsumer_1.join()
jobConsumer_2.join()
打印结果:
receiveChannel_1 value:1
receiveChannel_2 value:1
receiveChannel_1 value:2
receiveChannel_2 value:2
可以看到每一个消费者订阅后都拿到了生产者发送的消息。
Select-多路复用
Select的概念
Select
是一个IO多路复用的概念,而多路复用就是用一条信道同时传输多路信号- 协程的
Select
用于挂起函数的多路复用
多个Channel的复用
多个Channel
复用的情况就是在多个Channel中有异步任务在执行,谁先返回来发送消息就执行谁,这种情况下我们一般只能去顺序读取,一个一个的去等,效率是非常低的。Select
的好处就是可以优先读取处理最快发送过来Channel
消息。下面模拟一下多个channel复用情况:
val channelList = listOf(Channel<Int>(),Channel<Int>(),Channel<Int>())
GlobalScope.launch {
delay(200)
channelList[0].send(200)
}
GlobalScope.launch {
delay(100)
channelList[1].send(100)
}
GlobalScope.launch {
delay(400)
channelList[2].send(400)
}
val result = select<Int> {
channelList.forEach{ channel ->
channel.onReceive{
it
}
}
}
println(result)
打印结果:
100
我们可以看到顺序下来,send(100)
的Channel
是第二个通道,但它是最先返回来的,所以Select
会接收到最快的Channel
消息。
await的多个复用
await
的多个复用适合我们用在网络的请求的时候,我们可以模拟一下分别从缓存中和网络中请求数据的情况,谁先返回就用谁,上代码:
//模拟缓存中拿数据,缓存中拿肯定快,延时100ms
fun CoroutineScope.getDataFromCache() = async {
delay(100)
"getDataFromCache"
}
//模拟网络拿数据,延时500s返回
fun CoroutineScope.getDataFromNet() = async {
delay(500)
"getDataFromNet"
}
//执行代码
suspend fun main() {
GlobalScope.launch {
val dataFromCache = getDataFromCache()
val dataFromNet = getDataFromNet()
//使用select来等待
val result = select<String>{
dataFromCache.onAwait{
it
}
dataFromNet.onAwait{
it
}
}
println(result)
}.join()
}
打印结果:
getDataFromCache
这里要注意的是select
是优先去最快到的,如果多个同时到达,那select
会根据Channel
的顺序优先取第一个。也因为如此,官方还给出了另外一个Api:selectUnbiased
,Unbiased
的意思是没有偏见的,如果多个消息同时到达,selectUnbiased
会随机选一个。
四、Flow
Flow的概念
Flow
是一种异步的数据流,会按顺序发出值并完成,它是Kotlin协程的响应式API,是与响应式编程相结合的产物。
Flow
的内部是按照顺序执行的,这跟序列生成器sequence
基本是一致的。而Flow
跟Sequence
的最大区别就在于Flow
不会阻塞主线程,因为Flow
完全是使用协程构建的,通过使用协程的suspend
和resume
机制,可以将生产方flow
的执行与使用方collect
同步,而Sequence
会阻塞主线程。
Flow的创建方式
flow builder
: 直接调用flow<T>{}
,在给定的一个挂起函数创建一个冷数据流flowOf(vararg elements: T):
使用可变数组快速创建flow
asFlow():
将其他数据转换成普通的flow
,例如List
向Flow
的转换channelFlow():
支持缓冲通道,线程安全,允许不同的CorotineContext
发送事件
创建Flow
val mFlow = flow<Int> {
(1..2).forEach{
emit(it)
delay(200)
}
}
emit
函数是提供元素给消费者,而且Flow
的代码块内部也可以调用其他挂起函数。
Flow
也可指定它运行时使用的调度器:
//intFlow的构造逻辑会在IO线程上执行
mFlow.flowOn(Dispatchers.IO)
消费Flow
消费mFlow
只要调用collect
函数,该函数也是一个挂起函数:
mFlow.flowOn(Dispatchers.IO).collect {
println(it)
}
打印:
1
2
比较链式顺序的调用,我们这样写就好了:
val mFlow = flow<Int> {
(1..2).forEach{
emit(it)
delay(200)
}
}.flowOn(Dispatchers.IO).collect {
println(it)
}
和RxJava的线程切换对比
RxJava
使用的是observeOn
和subscribeOn
来切换线程,而Flow
则相对来说更简单,只需使用flowOn
。flowOn
与 subscribeOn
是对应的,而collect
所在协程的调度器则与 observeOn
指定的调度器对应。
冷数据流
冷数据流其实类似于懒汉式的设计模式,就是Flow
被创建之后,不消费则不生产,多次消费则多次生产,生产和消费相对应的。这一点和Channel
刚好是正对应:Channel
的发送端并不依赖于接收端。
异常处理
Flow 的异常处理可直接调用catch
函数,也可以使用传统的try...catch
来捕获异常:
val mFlow = flow<Int> {
(1..2).forEach{
emit(it)
throw RuntimeException()
}
}.catch {
t ->
println("RuntimeException: $t")
}.collect {
println(it)
}
打印结果:
1
RuntimeException: java.lang.RuntimeException
数据流完成时的操作
看下面代码:
flow<Int> {
(1..2).forEach{
emit(it)
throw RuntimeException()
}
}.catch {
t ->
println("RuntimeException: $t")
}.onCompletion {
t ->
println("finally")
}.collect {
println(it)
}
onCompletion
是类似 try ...catch...finally
中的finally
,无论前面是否存在异常,它在数据流完成时最终都会被调用,参数t
则是前面未捕获的异常。
末端操作符
collect
就是一个最基本的末端操作符,除此之外,其他常见的末端操作符还分为两大类:
- 集合类型转换操作,包括
toList
、toSet
等 - 聚合操作,包括将
Flow
规约到单值的reduce
、fold
等操作,以及获得单个元素的操作包括single
、singleOrNull
、first
等
transform
transform
操作符,可任意多次调用emit,并发射任何值:
(1..3).asFlow()
.transform {
//多次调用
emit(it)
delay(200)
//发射String的值
emit("value $it")
}.collect { println(it) }
打印结果:
1
value 1
2
value 2
3
value 3
take
take
操作符只取指定前几个emit发射过来的值:
(1..3).asFlow()
.take(2)
.collect {
println(it)
}
打印结果:
1
2
reduce
reduce
操作符可以对集合进行计算,具体就是当前两个元素操作获得值再跟下一个元素按逻辑执行,知道最后一个元素,并得到最终值。
val sum = (1..5).asFlow()
.reduce { a, b ->
a + b
}
println(sum)
打印结果:
15
得到最红的结果是15,其实就是1加5。
fold
fold
操作符类似于Kotlin
集合的fold
函数,fold
也需要设置初始值。
val sum = (1..5).asFlow()
.fold(0) { a, b -> a + b }
println(sum)
打印结果:
15
zip
zip
操作符可以将两个个flow
进行合并。
val flowA = (1..3).asFlow()
val flowB = flowOf("A", "B", "c")
flowA.zip(flowB){ a,b->
"$a -> $b" //这里的值就代表每一个对应合并的值
}.collect{
println(it)
}
打印结果:
1 -> A
2 -> B
3 -> c
combine
combine
操作符也是合并的操作,但跟zip
有一些不一样的就是combine
合并时,flowA
最新的发射出的值只会与flowB
最新发射出的值合并,而不是根据顺序一一对应。
val flowA = (1..3).asFlow().onEach { delay(100) }
val flowB = flowOf("A", "B", "C").onEach { delay(200) }
flowA.combine(flowB) { a, b ->
"$a -> $b"
}.collect {
println(it)
}
打印结果:
1 -> A
2 -> A
3 -> A
3 -> B
3 -> C
flattenMerge
flattenMerge
操作符是将flow
单个执行完。
val flowA = (1..3).asFlow()
val flowB = flowOf("A", "B", "C")
flowOf(flowA, flowB)
.flattenMerge()
.collect {
println(it)
}
打印结果:
A
1
2
3
B
C
如果我们要顺序执行完,只要换成flattenConcat
方法就行。
操作符就介绍到这里,还有其他操作符就不一一介绍了。
flow消费和触发的分离
除了通过collect
消费Flow
的元素外,onEach
也可以被用来消费Flow
的元素。用onEach
消费Flow
元素的好处就是不需要与末端操作符放到一起,collect
函数可以放到其他任意位置调用,这样Flow的消费和触发就分离了
suspend fun main() {
getFlow().collect()
}
fun getFlow() = flow<Int> {
(1..3).forEach {
emit(it)
delay(200)
}
}.onEach {
println(it)
}
Flow的取消
Flow
没有提供取消操作的方法。那我们是不是就不能取消Flow
呢?当然不是~因为Flow
的消费依赖于collect
这样的末端操作符,而它们又必须在协程当中调用,因此Flow
的取消主要依赖于末端操作符所在的协程的状态。
val job = GlobalScope.launch {
val mFlow = flow {
(1..3).forEach {
delay(500)
emit(it)
}
}
mFlow.collect { println(it) }
}
delay(1500)
job.cancelAndJoin()
打印结果:
1
2
从运行结果看,要取消Flow
只需要取消它所在的协程即可。
channelFlow函数创建Flow
hannelFlow
函数创建Flow
对比其他方式最大的一点就是它在生成元素时切换调度器:
channelFlow {
send(1)
withContext(Dispatchers.IO) {
send(2)
}
}
Flow的背压
背压问题是响应式编程的痛点,其主要在生产者的生产速率高于消费者的处理速率时出现。对于解决背压问题,我们可以考虑增加缓存来保证数据的不丢失。
给Flow
添加缓存
flow<Int> {
(1..100).forEach {
emit(it)
}
}.buffer()
单纯地给Flow
添加缓存是个指标不治本的办法,因为数据积压的隐患依然是存在的。问题根本还是生产和消费速率的不匹配,除直接优化消费者的性能以外,也可以采取一些取舍的手段。第一种是 conflate
。与 Channel
的 Conflate
模式一致,新数据会覆盖老数据。观察下面代码:
flow<Int> {
(1..100).forEach {
emit(it)
}
}.conflate().collect {
delay(100)
println("collect value $it")
}
打印结果:
collect value 1
collect value 100
发送元素是延时的,所以很快就发完了,但接收到的元素只有两个。
另外一种方式:collectLates
collectLates
方式只处理最新数据,但和conflate
不同的是它并不会直接用新数据覆盖老数据,而是每一个数据都会去处理,只是前一个数据还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。
flow<Int> {
//方便展示结果,只发送5个数据
(1..5).forEach {
emit(it)
}
}.collectLatest{
println("Collecting $it")
delay(100)
println("$it collected")
}
运行结果:
Collecting 1
Collecting 2
Collecting 3
Collecting 4
Collecting 5
5 collected
这里可以看懂Collecting
输出了1~5的所有结果,而但collected
只有5,这是后面的数据到达时,处理上一个数据的操作正好被delay
挂起了。除了collectLatest
之外还有 mapLatest
、flatMapLatest
都具有这个作用。
Flow的变换
Flow 的元素变换
使用map
对Flow
的元素进行变换,跟list
的map
转换基本一致:
flow<Int> {
(1..3).forEach {
emit(it)
}
}.map {
it*2
}.collect(){
print("$it,")
}
运行结果:
2,4,6,
Flow 的嵌套和拼接
flow {
(1..3).forEach { emit(it) }
}.map {
flow {
(1..it).forEach { emit(it) }
}
}
这里得到的是一个数据类型为Flow
的Flow
,有3个Flow
,其内部分别发送的1、2、3个元素,我们再使用flattenConcat
把这几个Flow
拼接合并起来:
flow {
(1..3).forEach { emit(it) }
}.map {
flow {
(1..it).forEach { emit(it) }
}
}.flattenConcat()
.collect { println(it) }
运行结果:
1
1
2
1
2
3
Flow实现多路复用
Flow实现对await的多路复用
还是用回select
中模拟本地和网络获取数据的情况:
//模拟从缓存中拿数据
fun CoroutineScope.getDataFromCache() = async {
delay(100)
"getDataFromCache"
}
//模拟网络拿数据,延时500s返回
fun CoroutineScope.getDataFromNet() = async {
delay(500)
"getDataFromNet"
}
suspend fun main() {
GlobalScope.launch {
listOf(::getDataFromCache, ::getDataFromNet)
.map { function ->
function.call()
}.map { deferred ->
flow { emit(deferred.await()) }
}.merge()
.collect {
println(it)
}
}.join()
}
运行结果:
getDataFromCache
getDataFromNet
这里和select
不同的是,Flow
会把两个数据都收集起来。
小结
至此,本文关于协程的基本使用和部分进阶的学习就到此结束了,不知不觉写了这么多。当然,协程的知识点,尤其是难点也还有很多,后续要继续深入学习探索。本文主要是记录学习协程的过程,如果大家翻阅到有帮助,那就真的是好。接下来会边继续深入学习Kotlin和在实际项目应用,有时间也会分享更多Kotlin学习的经历!