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老皮!!!欢迎大家来找我探讨交流👀

相关推荐
花开路口3 小时前
深入理解 Java/Kotlin 协变与逆变
java·kotlin
_祝你今天愉快3 小时前
UI核心 - UI绘制流程及原理
android
mmsx3 小时前
Android 一个设备多个 App 抢:跨进程共享服务的架构设计
android·kotlin·app
mmsx3 小时前
Android GIS系列 栅格怎么存、格网怎么归算:两个容易混淆的 GIS 链路
android·性能优化·app
陈柒吖3 小时前
安卓代码加固(1):加密DEX
android·前端
mmsx3 小时前
Android GIS系列 串口、蓝牙、USB、网络:一套统一抽象怎么扛住四种通道
android·app·产品
二流小码农3 小时前
鸿蒙开发:ArrayList,可不会让UI更新哦
android·ios·harmonyos
hai_android3 小时前
深入理解 Java 的四种引用:强引用、软引用、弱引用、虚引用
android·kotlin·android jetpack
恋猫de小郭3 小时前
Flutter Golden Tests:给 AI Agent 的 UI 测试系统
android·前端·flutter
知昂七昂3 小时前
AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?
android