探讨 Kotlin 协程中 suspendCoroutine 的最佳实践与优化

Kotlin 协程为异步编程提供了强大而灵活的工具,而suspendCoroutine 或者 suspendCancellableCoroutine(下面统称suspendCoroutine)函数则是其核心之一。它允许我们将传统的回调风格的异步操作转换为挂起函数,从而使代码更加简洁和易于理解。然而,正确使用suspendCoroutine并不容易,需要遵循一些最佳实践和优化技巧。

前言

在 Kotlin 的协程编程中,suspendCoroutine 函数是一项重要的利器。它使我们可以在协程中调用传统的回调式异步 API,而无需嵌套复杂的回调函数。然而,虽然suspendCoroutine功能强大,但在实际应用中的正确使用却需要谨慎处理。本文将深入探讨如何正确地使用suspendCoroutine函数,并介绍一些最佳实践和优化策略。

一段奇怪的代码

suspend fun doActions(str: String): Boolean {
    when (str) {
        "test" -> {
            return performAction1()
        }
    }
    return false
}

suspend fun performAction1(): Boolean {
    return suspendCoroutine { continuation ->
        setActionSetting { isSuccess ->
            if (!isSuccess) {
                log("操作失败!")
            }
            continuation.resume(isSuccess)
        }
    }
}

fun setActionSetting(response: ((isSuccess: Boolean) -> Unit)?) {
    ioScope.launch {
        flowOf(::setActionFinal).onEach { delay(100) }
            .collectIndexed { _, value ->
                value {
                    log(value)
                    response?.invoke(it)
                }
            }
    }
}

fun setActionFinal(response: ((isSuccess: Boolean) -> Unit)) {
    ActionFuc.sendAsyncRequest { res ->
        if (res == "success") {
            response.invoke(true)
        } else {
            response.invoke(false)
        }
    }
}

object ActionFuc : DomainRequest {
    override fun sendAsyncRequest(callback: (bundle: String) -> Unit) {
        ioScope.launch {
            // 模拟耗时操作
            delay(1000)
            callback.invoke("success")
        }
    }
}

val ioScope = CoroutineScope(Dispatchers.IO)
interface DomainRequest {
    fun sendAsyncRequest(callback: (bundle: String) -> Unit)
}

在这段代码中,我们看到了一系列挂起函数的调用和回调函数的嵌套,让代码变得复杂且难以理解。它的工作方式可能会让人感到困惑:在doActions函数中,根据传入的字符串参数决定是否调用performAction1函数,而在performAction1函数中,又通过suspendCoroutine函数将回调函数转换为挂起函数。 doActions 会做很多动作的适配,这个代码每当我增加一个action 就要写一个 performAction*的适配,每一个 performAction* 都要写suspendCoroutine 这显然是不合理的。

suspendCoroutine 的基本概念

suspendCoroutine 函数是 Kotlin 协程中非常重要的一个函数,它允许我们将传统的回调式异步操作转换为挂起函数。下面是该函数的代码实现:

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    return suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }
}

让我们逐步解释这个函数的实现:

  1. suspendCoroutine 是一个内联函数,它接受一个 Lambda 表达式 block 作为参数。这个 Lambda 表达式的参数是一个 Continuation<T> 对象,用于恢复挂起的协程并传递异步操作的结果。

  2. contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } 用于指定函数调用的契约,确保传入的 block 函数在此处被调用一次且仅一次。

  3. suspendCoroutineUninterceptedOrReturn 函数用于执行挂起的协程。它接受一个 Continuation<T> 对象作为参数,并返回一个泛型类型 T 的值或者抛出异常。

  4. suspendCoroutineUninterceptedOrReturn 函数内部,我们创建了一个 SafeContinuation 对象 safe,它接受传入的 Continuation 对象,并保证了异常的安全处理。

  5. 然后,我们调用 block(safe),将 safe 对象传递给 Lambda 表达式 block,以执行异步操作。

  6. 最后,我们调用 safe.getOrThrow() 来获取异步操作的结果,或者在发生异常时抛出异常。

通过这个函数,我们可以方便地将传统的回调式异步操作转换为挂起函数,从而使得协程中的异步编程更加简洁和易于理解。

  • suspendCoroutine 是一个挂起函数,用于将传统的回调式异步操作转换为挂起函数。
  • 它接受一个 Lambda 表达式作为参数,该 Lambda 表达式的参数是一个 Continuation 对象,用于恢复挂起的协程并传递异步操作的结果。
  • 在 Lambda 表达式中,我们可以使用 Continuation 对象的 resume 方法来恢复挂起的协程,并传递异步操作的结果;或者使用 resumeWithException 方法来传递异步操作的异常。
  • suspendCoroutine 函数的返回值类型与传入 Lambda 表达式的返回值类型相同,用于表示异步操作的结果。

使用场景

  • 当需要在协程中调用传统的回调式异步 API 时,可以使用 suspendCoroutine 函数将其转换为挂起函数,以便在协程中方便地处理异步操作。
  • suspendCoroutine 适用于需要与传统的异步 API 进行交互的场景,例如网络请求、文件操作、数据库查询等等。

suspendCoroutine 的确在底层线程与协程之间进行转换时非常有用。 在 Kotlin 的协程中,suspendCoroutine 提供了一种便捷的方式将现有的回调式异步 API 转换为挂起函数。当你需要在协程中调用这些底层的异步 API(比如网络请求、数据库查询等)时,suspendCoroutine 可以帮助你更好地管理这些操作,并将这些操作的结果或者异常恢复到协程中。

不过,也要注意,虽然 suspendCoroutine 非常强大,但并不是所有的异步操作都需要使用它。在很多情况下,Kotlin 的协程库提供了更高级的函数(如 asynclaunchwithContext 等),它们可以更简单、更安全地实现协程的并发和切换上下文。

总的来说,suspendCoroutine 非常适合在底层线程中使用,例如下面这个例子:

object ClientManager {

    var executor: Executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
    val customDispatchers = executor.asCoroutineDispatcher()

    /**
     * getUser
     */
    fun getUser(userId: Int, callback: (User) -> Unit) {
        executor.execute {
            val sleepTime = Random().nextInt(500)
            Thread.sleep(sleepTime.toLong())
            callback(User(userId, sleepTime.toString(), "$userId-->avatar", ""))
        }
    }

    /**
     * 异步同步化
     */
    suspend fun getUserAsync(userId: Int): User = suspendCoroutine { continuation ->
        getUser(userId) {
            continuation.resume(it)
        }
    }
}

在上述示例中,getUserAsync 函数就是将底层线程和回调式异步 API 转换为挂起函数的典型应用。

最佳实践与优化策略

  • 在使用suspendCoroutine时,应该遵循 Kotlin 协程的最佳实践,确保正确地处理异步操作的结果和异常,以及适时地取消挂起的协程。
  • 尽可能将异步操作封装在专门的挂起函数中,以提高代码的可读性和维护性。
  • 避免在suspendCoroutine中进行复杂的逻辑或深层次的嵌套,以免造成代码难以理解和调试。

在大多数上层应用中,我们通常不需要直接使用 suspendCoroutine。Kotlin 的协程库提供了许多更高级的构造函数,如 launchasyncwithContext 等,它们已经内部处理了协程的挂起和恢复,使得我们可以更简单、更安全地进行协程编程。

让我们回头看看上面代码的两大问题,第一 setActionSettingsetActionFinal 应该是挂起函数而不是普通函数,setActionSetting 改为 挂起函数 suspendCoroutine 完全可以消除,第二,上面的代码有太多的异步回调 ,包括接口的设计,这显然是不合适的,协程做的就是异步回调代码的同步化。

在这个优化中,主要的变化是将原来的回调风格的异步操作转换为了挂起函数,这样做的优点包括:

  1. 更加简洁:使用挂起函数和Flow可以将异步操作的处理逻辑更加清晰地表达出来,避免了回调地狱的情况。

  2. 更好的可读性:将异步操作的结果转换为挂起函数的返回值,可以让代码更加直观和易读。

  3. 更好的可维护性:将异步操作的处理逻辑封装在挂起函数中,使得代码更加模块化,易于维护和修改。

通过将回调风格的异步操作转换为挂起函数,可以使得代码更加简洁、清晰、易读和易维护。

优化代码

在使用协程处理异步任务时,代码结构的清晰性和简洁性是非常重要的。通过优化代码结构,我们可以使代码更易于理解和维护。

下面是一个优化后的示例代码:

interface DomainRequest2 {
    suspend fun sendAsyncRequest2(): String
}

suspend fun doActions(str: String): Boolean {
    return when (str) {
        "test" -> performAction1()
        else -> false
    }
}

suspend fun performAction1(): Boolean {
    return setActionSetting2()
}

suspend fun setActionSetting2(): Boolean {
    return flowOf(::setActionFinal2)
        .onEach { delay(100) }
        .map { it.invoke() }
        .single()
}

suspend fun setActionFinal2(): Boolean {
    val res = ActionFuc2.sendAsyncRequest2()
    return res == "success"
}

object ActionFuc2 : DomainRequest2 {
    override suspend fun sendAsyncRequest2(): String = withContext(Dispatchers.IO) {
        delay(1000)
        return@withContext "success"
    }
}

通过将代码分解为更小的函数和类,我们可以更清晰地表达每个功能的含义和作用。这样做不仅使代码更易于理解,还使得后续的修改和维护更加方便。

优化的内容如下所示:

  1. 减少回调嵌套: 优化前的代码存在回调嵌套的情况,导致代码复杂难懂。通过将异步操作封装成挂起函数,可以减少回调嵌套,使代码结构更清晰。

  2. 将普通函数改为挂起函数: 在优化后的代码中,setActionSettingsetActionFinal 被改为挂起函数,以便在协程中调用,避免了回调风格的异步操作。

  3. 优化接口设计: 优化后的代码将异步操作的结果直接作为挂起函数的返回值返回,而不是通过回调函数传递。这样可以更好地利用 Kotlin 协程的特性,简化代码逻辑。

通过以上优化,代码变得更加简洁、清晰和易于理解。同时,使用挂起函数和 Flow 来处理异步操作,使得代码的可读性和可维护性得到了提高。

总结

在深入探讨了 suspendCoroutine 的使用场合和最佳实践后,我们可以得出以下结论:

  1. suspendCoroutine 在连接底层异步 API 和协程之间时非常有用,它促进了同步风格的代码编写,同时保留了异步操作的非阻塞特性。
  2. 尽管 suspendCoroutine 强大,但并非所有异步操作都需要它。Kotlin 协程库提供了大量的构建块,如 launchasyncwithContext 等,这些通常足以应对日常的异步需求。
  3. 上层代码中,尽量避免使用 suspendCoroutine。在许多情况下,它可能会导致不必要的复杂性,增加代码的理解和维护负担。只有当遇到无法通过现有协程构建块解决的异步交互需求时,才考虑使用 suspendCoroutine

遵循这些指导原则,开发者可以在确保代码健壮性和可维护性的同时,充分利用 suspendCoroutine 带来的好处,创造清晰、高效、可维护的异步协程代码。

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题
图片

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值