Skip to content

桥接与回调转换

源:suspendCancellableCoroutine 官方指南

将现有的基于回调的异步 API(如 OkHttp Call, Google Play Services Task)转换为挂起函数,是 Kotlin 协程最常见的实战场景。

核心契约:只能恢复一次

协程最核心的契约是:一个 Continuation 必须且只能被恢复一次(resume)

suspendCancellableCoroutine

在生产环境中,严禁使用 suspendCoroutine。必须始终使用 suspendCancellableCoroutine

为什么选择它?

  1. 生命周期感知:它能感知协程的取消。如果用户退出了界面,协程被取消,你必须也通知底层 SDK 停止工作。
  2. 原子性保护:它内部封装了状态控制。如果协程已经被取消(Cancelled),再次调用 resume 会被静默丢弃,而不会导致崩溃。

标准模板

kotlin
suspend fun fetchUser(id: String): User = suspendCancellableCoroutine { cont ->
    // 1. 发起异步请求
    val call = api.getUser(id, object : Callback<User> {
        override fun onSuccess(user: User) {
            // 2. 只有当 cont 依然活跃时才恢复
            // suspendCancellableCoroutine 内部处理了竞争,确保此处安全
            cont.resume(user)
        }

        override fun onError(e: Exception) {
            // 3. 失败恢复
            cont.resumeWithException(e)
        }
    })

    // 4. 关键:注册取消回调
    // 当协程所在的作用域被取消时,立即停止底层网络请求
    cont.invokeOnCancellation {
        call.cancel()
    }
}

常见桥接场景

Google Play Services / Firebase (Task API)

kotlin
suspend fun <T> Task<T>.await(): T = suspendCancellableCoroutine { cont ->
    addOnCompleteListener { task ->
        if (task.isSuccessful) {
            cont.resume(task.result)
        } else {
            cont.resumeWithException(task.exception ?: RuntimeException("Unknown error"))
        }
    }
    // 注意:Task 不一定支持取消,具体取决于底层实现
}

View 监听器 (一次性)

kotlin
suspend fun View.awaitNextLayout() = suspendCancellableCoroutine<Unit> { cont ->
    val listener = object : View.OnLayoutChangeListener {
        override fun onLayoutChange(...) {
            removeOnLayoutChangeListener(this)
            cont.resume(Unit)
        }
    }
    addOnLayoutChangeListener(listener)
    cont.invokeOnCancellation { removeOnLayoutChangeListener(listener) }
}

如果意外调用了两次 resume 会怎么样?

当你因为逻辑失误或第三方 SDK 的 Bug 调用了两次恢复方法(如 resume 之后又调用了 resumeWithException)时:

  1. 内部保护(稳):得益于底层的 SafeContinuation 机制,协程内部的状态机是安全的。它会原子性地忽略第二次恢复请求,确保协程体内的后续代码不会被执行两次
  2. 外部报警(崩):虽然协程安全了,但为了让开发者感知到这种非法的逻辑流,resume 方法会主动抛出 IllegalStateException: Already resumed
  3. 崩溃位置:注意!这个异常不是在协程内部抛出的,而是抛给调用 resume 的那个线程。如果这个线程是主线程,你的 App 会直接闪退;如果是 SDK 的私有线程,可能会导致该线程死掉或触发 SDK 的未捕获异常逻辑。

崩溃模拟

kotlin
suspend fun fetchBadData(): String = suspendCancellableCoroutine { cont ->
    api.doWork { result ->
        // 假设 SDK 逻辑有误,在 onSuccess 后又触发了 onFailure
        cont.resume("Success")
        
        // 这一行执行时,协程会抛出 IllegalStateException 导致调用者崩溃!
        cont.resumeWithException(RuntimeException("Late Error")) 
    }
}

应对“不守规矩”的第三方 SDK

有些老旧库可能会在极短时间内多次回调,甚至在不同线程并发回调。为了绝对安全,你可以使用 runCatching 或手动检查状态。

kotlin
suspend fun <T> Callback<T>.await(): T = suspendCancellableCoroutine { cont ->
    onEvent { result ->
        // 如果你极度不信任第三方库,可以手动检查 isActive
        if (cont.isActive) {
            cont.resume(result)
        }
    }
}
kotlin
// 虽然 cont.resume 内部已通过 SafeContinuation 保护
// 但如果想防止 resume 导致的 IllegalStateException 杀掉回调线程
onEvent { result ->
    runCatching { cont.resume(result) }
}

总结

  • 崩溃风险:重复恢复会抛出 IllegalStateException,且崩溃发生在回调线程。
  • 取消风险:不处理 invokeOnCancellation 会导致任务在协程取消后继续运行,浪费资源。
  • 方案选择:99% 的场景请使用 suspendCancellableCoroutine。只有在极其简单的、不涉及取消且你能完全掌控恢复次数的底层原语编写中,才考虑 suspendCoroutine