Kotlin 协程的 select 特性及其应用

select 和 Deferred 的结合

Deferred 是一种表示异步计算结果的对象,它可以通过 await 方法来获取结果,或者通过 isCompleted 属性来判断是否完成。如果有多个 Deferred 对象,我们可能想要获取最快的那个结果,或者优先显示缓存数据,再显示网络数据。这时,我们可以使用 select 来实现这样的逻辑。

select 是一个挂起函数,它接收一个 lambda 表达式作为参数,在 lambda 表达式中,我们可以使用 onAwait 方法来获取 Deferred 对象的结果,并返回一个值。select 会等待所有的 onAwait 回调,并选择最快的那个返回值作为结果。例如:
select+Deferred

java 复制代码
// 模拟从缓存中获取物品信息
suspend fun getCacheInfo(productId: String): Product {
    delay(100L)
    return Product(productId, 9.9, true)
}

// 模拟从网络中获取物品信息
suspend fun getNetworkInfo(productId: String): Product {
    delay(200L)
    return Product(productId, 9.8, false)
}

// 模拟更新UI
fun updateUI(product: Product) {
    println("${product.productId} == ${product.price} == ${product.isCache}")
}

// 数据类,来表示一个商品
data class Product(
    val productId: String,
    val price: Double,
    val isCache: Boolean = false
)

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val productId = "11211"
    // 开始两个非阻塞任务
    val cacheDeferred = async { getCacheInfo(productId) }
    val networkDeferred = async { getNetworkInfo(productId) }
    // 使用 select 来获取最快的结果
    val product = select<Product> {
        cacheDeferred.onAwait { it }
        networkDeferred.onAwait { it }
    }
    // 更新UI
    updateUI(product)
    println("total time : ${System.currentTimeMillis() - startTime}")
    // 如果当前是缓存信息,则再去获取网络信息
    if (product.isCache) {
        val latest = networkDeferred.await()
        updateUI(latest)
        println("all total time : ${System.currentTimeMillis() - startTime}")
    }
}

上面的代码中,我们模拟了从缓存和网络中获取商品信息的场景,由于缓存较快,我们使用 select 来优先显示缓存信息,然后再显示网络信息。运行结果如下:

java 复制代码
11211 == 9.9 == true
total time : 134
11211 == 9.8 == false
all total time : 235

可以看到,select 可以帮助我们实现优化缓存和网络数据的显示逻辑。

select 和 Channel 的结合

Channel 是一种用于协程间通信的数据结构,它可以发送和接收数据,并支持关闭和迭代。如果有多个 Channel ,我们可能想要获取最先发送数据的那个 Channel ,或者根据不同的 Channel 来执行不同的逻辑。这时,我们也可以使用 select 来实现这样的逻辑。

select 也可以接收 Channel 对象作为参数,在 lambda 表达式中,我们可以使用 onReceiveCatching 方法来获取 Channel 对象发送的数据,并返回一个值。select 会等待所有的 onReceiveCatching 回调,并选择最先发送数据的那个返回值作为结果。例如:


select +Channel

java 复制代码
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 ,接收两个 Channel
    suspend fun selectChannel(
        channel1: ReceiveChannel<String>,
        channel2: ReceiveChannel<String>
    ): String = select {
        // 这里同样使用类 onXXX 的 API
        channel1.onReceiveCatching { it.getOrNull() ?: "channel1 is closed!"¹[1] }
        channel2.onReceiveCatching { it.getOrNull() ?: "channel2 is closed!"¹[1] }
    }
    // 连续选择 6 次
    repeat(6) {
        val result = selectChannel(channel1, channel2)
        println(result)
    }
    // 最后再把协程取消,因为前面设置的有 1000ms 延迟
    channel1.cancel()
    channel2.cancel()
    println("Time cost: ${System.currentTimeMillis() - startTime}")
}

上面的代码中,我们模拟了两个 Channel 同时发送数据的场景,由于 Channel1 较快,我们使用 select 来优先接收 Channel1 的数据。运行结果如下:

java 复制代码
A
a
B
b
C
c
Time cost: 553

可以看到,select 可以帮助我们实现协程间的通信选择。

select 的使用注意事项和原理

select 的使用有一些注意事项和原理,我们需要了解一下:

  • select 是一个挂起函数,需要在协程中使用。
  • select 中的回调方法不是 await 或 receive ,而是 onAwait 或 onReceiveCatching 。
  • select 还不支持 Flow ,因为 Flow 已经有了类似的 API 。
  • select 的原理是基于协程的状态机和回调机制,它会注册所有的回调,并等待其中一个回调触发,然后取消其他的回调,并返回结果。

select 是一个非常有用的特性,它可以让我们在多个异步任务或通道中做出选择,从而实现优化或选择的逻辑。

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀

相关推荐
Estar.Lee3 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯41 分钟前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟4 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程6 小时前
初级数据结构——树
android·java·数据结构
闲暇部落8 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX10 小时前
Android 分区相关介绍
android
大白要努力!11 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle