前语
上一篇关于Flow
的文章,其实只是Flow
的入门运用,想让Flow
真实完全发挥出它的作用,在了解其原理后,咱们再了解一些其他扩展的Flow
知识才能够。
本篇文章咱们来说一个还在实验性值的特性:select
,这个东西在实践事务中很有用,尤其是合作async
办法和Channel
,这儿会说为什么和Flow
没关系,那是由于Flow
触及的东西太多了,不需求select
,后边咱们专门来说。
正文
select
的直接翻译便是挑选,那它到底挑选的是什么呢?
咱们能够思考一个事务场景,便是一个展示产品信息的页面,可是这个信息能够从网络获取,也能够从本地缓存获取(之前浏览过),为了不让页面出现空白,咱们大可先显现缓存信息,等网络数据回来后,再显现网络最新信息。
咱们先写出下面测试代码:
/**
* 模仿从缓存中获取物品信息
* @param productId 产品Id
* @return 回来物品信息[Product]
* */
suspend fun getCacheInfo(productId: String): Product {
delay(100L)
return Product(productId, 9.9)
}
/**
* 模仿从网络中获取物品信息
* @param productId 产品Id
* @return 回来物品信息[Product]
* */
suspend fun getNetworkInfo(productId: String): Product {
delay(200L)
return Product(productId, 9.8)
}
/**
* 模仿更新UI
* @param product 产品信息
* */
fun updateUI(product: Product) {
println("${product.productId}==${product.price}")
}
data class Product(val productId: String,val price: Double)
这儿咱们模仿网络请求和获取缓存信息,由于缓存较快,咱们模仿了100ms,网络是200ms,按照之前的思路,咱们写出如下代码:
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val productId = "11211"
val cacheProduct = getCacheInfo(productId)
updateUI(cacheProduct)
val networkProduct = getNetworkInfo(productId)
updateUI(networkProduct)
println("total time : ${System.currentTimeMillis() - startTime}")
}
}
这儿会先显现缓存信息,再显现网络信息,打印如下:
11211==9.9
11211==9.8
total time : 317
可是这儿有个问题,便是getCacheInfo
是一个挂起函数,假设该函数出现了问题,或许回来时刻十分长,这时用户就会等候好久,或许直接显现不出来。
这时,咱们就面临挑选了,也便是这2个网络请求,谁更快,我便是运用哪个。由于是在2个协程中履行的使命,想要获取哪个快,就必须加入一个flag
变量,来判别是否有一个现已完成了,然后经过while
循环来获取值,代码如下:
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val productId = "11211"
var finished = false
var product: Product? = null
//开端2个非堵塞使命
val cacheDeferred = async { getCacheInfo(productId) }
val networkDeferred = async { getNetworkInfo(productId) }
//敞开2个非堵塞使命获取结果
launch {
product = cacheDeferred.await()
finished = true
}
launch {
product = networkDeferred.await()
finished = true
}
//等候哪个先履行完
while (!finished){
delay(1)
}
//显现UI
product?.let { updateUI(it) }
println("total time : ${System.currentTimeMillis() - startTime}")
}
}
上面代码直接看注释就能了解,是谁先回来,就用哪个。打印如下:
11211==9.9
total time : 129
能够发现耗时为129ms,阐明确实是2个使命一同履行,并且回来先履行完的。
select
和Deferred
合作运用
上面代码尽管能够实现功能,可是引入了flag
,并且代码十分多,这时就需求select
的运用了,运用select
优化完代码如下:
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val productId = "11211"
val product = select {
async { getCacheInfo(productId) }
.onAwait{
it
}
async { getNetworkInfo(productId) }
.onAwait{
it
}
}
updateUI(product)
println("total time : ${System.currentTimeMillis() - startTime}")
}
}
上述用select
优化完,打印如下:
11211==9.9
total time : 132
这儿咱们直接运用select
包裹多个Deferred
对象,然后在里边的对每个Deferred
对象不是运用咱们之前常用的await()
挂起函数来获取结果,仍是运用onAwait
来获取结果,这儿要注意。
关于这个select
是怎么选取里边跑的最快的原理咱们不做深化探求,咱们单从上面名字来看await
和onAwait
来看出一点端倪,咱们平时运用onXXX
办法时,一般都是认为是一个回调,而这儿的意思也是相似的,即多个Deferred
有哪个回调了,就把值回调出去,然后找到最快的。
上面代码尽管能够完美获取运转最快的Deferred
,可是事务上仍是有点问题,即假设缓存回来很快、网络回来较慢,咱们只会显现缓存的,无法显现最新的网络的信息,所以咱们只需求略微修正一下,判别数据是否来自缓存即可,修正如下:
/**
* 数据类,来表明一个产品
* @param isCache 是否是缓存数据
* */
data class Product(val productId: String,val price: Double,val isCache: Boolean = false)
先给数据类加个特点,表明是否是缓存,这儿咱们给isCache
设置的是val
变量,这儿是遵从Kotlin的不变性原则:类尽量要削减对外露出不必要api,以削减改变可能性。一同数据类的赋值,尽量都是运用构造函数,而不要独自去修正变量值。
那怎么修正这个isCache
变量呢?主张运用copy
的方式,比方优化后的代码:
fun main(){
runBlocking {
val startTime = System.currentTimeMillis()
val productId = "11211"
val cacheDeferred = async { getCacheInfo(productId) }
val networkDeferred = async { getNetworkInfo(productId) }
val product = select {
cacheDeferred.onAwait{
//这儿有改变
it.copy(isCache = true)
}
networkDeferred.onAwait{
it.copy(isCache = false)
}
}
updateUI(product)
println("total time : ${System.currentTimeMillis() - startTime}")
//如果当前是缓存信息,则再去获取网络信息
if (product.isCache){
val latest = networkDeferred.await()
updateUI(latest)
println("all total time : ${System.currentTimeMillis() - startTime}")
}
}
}
这儿咱们在缓存和网络中获取数据时,给加了判别,这儿运用copy
来削减数据改变性,一同在后边进行判别,是否是缓存信息,如果是的话,再去获取网络信息,打印如下:
11211==9.9
total time : 134
11211==9.8
all total time : 235
这样咱们就写出了契合事务的代码了。
select
和Channel
合作运用
上面咱们以select
和Deferred
结合运用来获取最快的那个,这儿咱们再进阶一下,select
还能够和Channel
一同运用。
咱们想一下,Channel
是一个密闭的管道,用于协程间的通讯,这时假设有多个管道,就相似于上面比如中每个协程会回来多个值,这时咱们能够挑选合适的值。
至于问为什么Flow
不支持用select
,只能说Flow
自身触及的东西十分多,官方库现已为其规划了相似的API了,这个咱们后边遇到了细说。
咱们以一个简单比如来看看怎么运用:
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
//敞开一个协程,往channel1中发送数据,这儿发送完 ABC需求450ms,
val channel1 = produce {
delay(50L)
send("A")
delay(150)
send("B")
delay(250)
send("C")
//推迟1000ms是为了这个Channel不那么快close
//由于produce高阶函数敞开协程,当履行完时,会自动close
delay(1000)
}
//敞开一个协程,往channel2中发送数据,发送完abc需求500ms
val channel2 = produce {
delay(100L)
send("a")
delay(200L)
send("b")
delay(200L)
send("c")
delay(1000)
}
//挑选Channel,接纳2个Channel
suspend fun selectChannel(channel1: ReceiveChannel<String>, channel2: ReceiveChannel<String>): String =
select {
//这儿相同运用类onXXX的API
channel1.onReceiveCatching {
it.getOrNull() ?: "channel1 is closed!"
}
channel2.onReceiveCatching {
it.getOrNull() ?: "channel2 is closed!"
}
}
//连续挑选6次
repeat(6) {
val result = selectChannel(channel1, channel2)
println(result)
}
//最终再把协程撤销,由于前面设置的有1000ms推迟
channel1.cancel()
channel2.cancel()
println("Time cost: ${System.currentTimeMillis() - startTime}")
}
上面代码的运转结果如下:
A
a
B
b
C
c
Time cost: 553
首要咱们剖析一下结果,耗时是553,这阐明select
在挑选时,2个Channel
替换给出数据,这也便是并发的体现。其次便是上面代码的了解,有一些注释需求细心看看,现在来简单阐明一下:
-
produce
函数的回来值,以及为什么要在最终delay(1000)
。咱们看一下函数界说:public fun <E> CoroutineScope.produce( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, @BuilderInference block: suspend ProducerScope<E>.() -> Unit ): ReceiveChannel<E> = produce(context , capacity , BufferOverflow.SUSPEND , CoroutineStart.DEFAULT , onCompletion = null , block = block)
这个函数的回来值类型是ReceiveChannel
,根据Channel
章节的学习,咱们知道这个是用来接纳数据的。而block
高阶函数类型的接纳者是ProducerScope
,界说如下:
public interface ProducerScope<in E> : CoroutineScope, SendChannel<E> {
public val channel: SendChannel<E>
}
这是一个接口,接口中有一个默认实现的channel
,所以咱们能够在block
代码中调用send
办法来发送数据。
一同该函数会发动一个协程,然后把数据发送到回来值的ReceiveChannel
中,当发送完毕后,会调用close
办法,这也便是为什么咱们要在后边delay(1000)
的原因,是避免咱们取数据的时分,这个channel
现已封闭了。
- 在
selectChannel()
办法中中,咱们运用select
包裹了2个channel
,注意这儿咱们运用的办法回调是onReceiveCatching
,当然它也有onReceive
办法,之所以这样是由于能够避免channel
被封闭后,导致的异常。 - 最终便是这儿的
send
、select
都是挂起函数,所以这儿的思考的时分就需求发挥一点想象力。首要第一次调用select
来挑选时,发现没得选,都没有回来,这时就会挂起。等候一会后,channel1就会
回调第一个数据A
,然后select
得到数据。紧接着,又是第2次调用select
,依旧会等候,又过了一会,channel2
管道发送了一个数据。
所以这个过程便是俩根管道一同往一个池子中放球,而select
便是旁边拿球的人,每调用一次就来取一次。
总结
本篇文章介绍了十分有实践用处的select
组件,当咱们在日常开发中遇到了异步挑选难题,能够运用select
来简化咱们的代码。