桥接与回调转换
源:suspendCancellableCoroutine 官方指南
将现有的基于回调的异步 API(如 OkHttp Call, Google Play Services Task)转换为挂起函数,是 Kotlin 协程最常见的实战场景。
核心契约:只能恢复一次
协程最核心的契约是:一个 Continuation 必须且只能被恢复一次(resume)。
suspendCancellableCoroutine
在生产环境中,严禁使用 suspendCoroutine。必须始终使用 suspendCancellableCoroutine。
为什么选择它?
- 生命周期感知:它能感知协程的取消。如果用户退出了界面,协程被取消,你必须也通知底层 SDK 停止工作。
- 原子性保护:它内部封装了状态控制。如果协程已经被取消(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)时:
- 内部保护(稳):得益于底层的
SafeContinuation机制,协程内部的状态机是安全的。它会原子性地忽略第二次恢复请求,确保协程体内的后续代码不会被执行两次。 - 外部报警(崩):虽然协程安全了,但为了让开发者感知到这种非法的逻辑流,
resume方法会主动抛出IllegalStateException: Already resumed。 - 崩溃位置:注意!这个异常不是在协程内部抛出的,而是抛给调用 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。