前语

上一篇关于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个使命一同履行,并且回来先履行完的。

selectDeferred合作运用

上面代码尽管能够实现功能,可是引入了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是怎么选取里边跑的最快的原理咱们不做深化探求,咱们单从上面名字来看awaitonAwait来看出一点端倪,咱们平时运用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

这样咱们就写出了契合事务的代码了。

selectChannel合作运用

上面咱们以selectDeferred结合运用来获取最快的那个,这儿咱们再进阶一下,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被封闭后,导致的异常。
  • 最终便是这儿的sendselect都是挂起函数,所以这儿的思考的时分就需求发挥一点想象力。首要第一次调用select来挑选时,发现没得选,都没有回来,这时就会挂起。等候一会后,channel1就会回调第一个数据A,然后select得到数据。紧接着,又是第2次调用select,依旧会等候,又过了一会,channel2管道发送了一个数据。

所以这个过程便是俩根管道一同往一个池子中放球,而select便是旁边拿球的人,每调用一次就来取一次。

总结

本篇文章介绍了十分有实践用处的select组件,当咱们在日常开发中遇到了异步挑选难题,能够运用select来简化咱们的代码。