七、suspendCoroutine与suspendCancellableCoroutine —— 回调转挂起函数

用一个函数把传统回调式 API 转换成挂起函数,让异步代码回归顺序书写;suspendCancellableCoroutine 更进一步支持取消回调

suspendCoroutine suspendCancellableCoroutine 回调 挂起 Continuation invokeOnCancellation

目录导航(点击跳转)

一、从回调到挂起:suspendCoroutine 的基本用法

1传统回调式代码的痛点

在协程出现之前,异步操作通常用回调接口来处理结果。以 Retrofit 的网络请求为例:

// 传统回调式写法
private fun callbackStyle() {
    apiService.getResult("params")
        .enqueue(object : Callback<Result> {
            override fun onResponse(
                call: Call<Result>,
                response: Response<Result>
            ) {
                showResult(response.body()!!)   // 成功回调
            }

            override fun onFailure(
                call: Call<Result>,
                t: Throwable
            ) {
                t.printStackTrace()              // 失败回调
            }
        })
}

这种写法的典型问题:

  • 成功和失败的逻辑分散在两个回调方法里
  • 如果有多个请求需要串行,就会形成"回调地狱"
  • 无法使用 try-catch 统一处理异常
  • 生命周期管理困难
2suspendCoroutine:在挂起函数中调用回调式 API

suspendCoroutine 是一个挂起函数,你在它的 lambda 里面调用传统回调式的函数,就可以把这个回调式的函数转换成挂起函数

// 用 suspendCoroutine 将回调转成挂起
lifecycleScope.launch {
    val result = suspendCoroutine { continuation ->
        apiService.getResult("params")
            .enqueue(object : Callback<Result> {
                override fun onResponse(
                    call: Call<Result>,
                    response: Response<Result>
                ) {
                    continuation.resume(response.body()!!)   // 成功:恢复协程,返回结果
                }

                override fun onFailure(
                    call: Call<Result>,
                    t: Throwable
                ) {
                    continuation.resumeWithException(t)      // 失败:恢复协程并抛出异常
                }
            })
    }
    // result 直接可用,像同步代码一样
    showResult(result)
}
协程启动
suspendCoroutine
挂起 + 发起请求
协程挂起
等待回调
onResponse
→ resumeresult
/
onFailure
→ resumeWithException
协程恢复
拿到 result

continuation 是协程的续体,它有两个关键方法:resume(value) 恢复协程并返回成功结果;resumeWithException(t) 恢复协程并抛出异常。两者都会让挂起的协程从 suspendCoroutine 调用处恢复执行

二、最佳实践:封装为独立挂起函数

3把转换逻辑抽成独立函数,调用方干净整洁

直接在 launch 中写 suspendCoroutine 会让调用方代码臃肿。最佳实践是把转换逻辑封装为一个独立的挂起函数

// 调用方:像同步代码一样干净
lifecycleScope.launch {
    try {
        val result = callbackToSuspend()
        showResult(result)
    } catch (e: Exception) {
        textView.text = e.message       // 统一异常处理
    }
}

// 封装:将 Retrofit 回调转为挂起函数
private suspend fun callbackToSuspend() = suspendCoroutine { continuation ->
    apiService.getResult("params")
        .enqueue(object : Callback<Result> {
            override fun onResponse(
                call: Call<Result>,
                response: Response<Result>
            ) {
                continuation.resume(response.body()!!)
            }

            override fun onFailure(
                call: Call<Result>,
                t: Throwable
            ) {
                continuation.resumeWithException(t)
            }
        })
}

这样封装的好处:

  • 调用方只关心业务逻辑,不关心底层是 Retrofit 还是别的什么
  • 异常可以通过 try-catch 统一处理,和同步代码一样自然
  • 如果将来换成其他网络库(如 OkHttp 直接调用),只需改 callbackToSuspend 内部实现
  • 函数可以被多处复用

这就是协程的核心价值之一:把异步代码写成同步的样子。suspendCoroutine 是连接传统回调世界和协程世界的桥梁。

三、suspendCancellableCoroutine:支持取消的回调转换

4suspendCoroutine 不配合 cancel

suspendCoroutine 有一个重要局限:它不响应协程的取消。当外部调用 job.cancel() 时,suspendCoroutine 内部发起的网络请求仍然在进行,不会自动中断。

如果希望协程被取消时也能中断底层的网络请求,应该使用 suspendCancellableCoroutine,它支持取消回调

// 调用方:支持取消
lifecycleScope.launch {
    try {
        val result = callbackToSuspend()
        showResult(result)
    } catch (e: Exception) {
        textView.text = e.message
    }
}

// 封装:使用 suspendCancellableCoroutine
private suspend fun callbackToSuspend() = suspendCancellableCoroutine { cont ->
    val call = apiService.getResult("params")

    // 核心:协程取消时,同步取消底层网络请求
    cont.invokeOnCancellation {
        call.cancel()     // 取消 OkHttp 的 Call
    }

    call.enqueue(object : Callback<Result> {
        override fun onResponse(
            call: Call<Result>,
            response: Response<Result>
        ) {
            cont.resume(response.body()!!)
        }

        override fun onFailure(
            call: Call<Result>,
            t: Throwable
        ) {
            cont.resumeWithException(t)
        }
    })
}

suspendCoroutine

  • 不响应协程取消
  • invokeOnCancellation
  • job.cancel() 后网络请求仍在进行
  • 可能造成资源浪费

suspendCancellableCoroutine

  • 响应协程取消
  • 通过 invokeOnCancellation 注册取消回调
  • job.cancel() 时自动取消底层请求
  • 资源及时释放
协程 suspendCancellableCoroutine 网络请求 (Call)
1 调用挂起函数,协程挂起
2 发起网络请求,注册 invokeOnCancellation
3 外部调用 job.cancel()
4 invokeOnCancellation 触发
5 call.cancel() 取消网络请求
6 协程以 CancellationException 结束

invokeOnCancellation 是 suspendCancellableCoroutine 提供的唯一额外能力,也是它和 suspendCoroutine 的唯一区别。它注册的回调会在协程被取消时执行,给你一个清理资源的机会。

四、核心思想总结

核心要点

一句话总结

suspendCoroutine 把回调变成挂起,suspendCancellableCoroutine 进一步让挂起可以被取消 —— 封装成独立函数,调用方就像写同步代码一样处理异步结果。