将回调转换为协程
背景:很多库(尤其是Java/Android遗留库)使用基于回调(Callback)的API处理异步操作(如网络请求,数据库操作)。这在代码中会导致回调地狱。我们需要一种方法将这种回调风格的API转换成挂起函数(suspend fun)
,使其可以在协程中像顺序代码一样使用。
核心函数:
suspendCoroutine
(简单转换,无内置取消支持)
suspend fun <T> convertCallbackToSuspendingFunc(): T = suspendCoroutine {
continuation ->
//1. 启动异步操作
val callback = object : Callback<T> {
override fun onSuccess(result: T) {
//2. 成功时恢复协程,传递结果
continuation.resume(result)
}
override fun onFailure(error: Throwable) {
//3. 失败时恢复协程,传递异常
continuation.resumeWithException(error)
}
}
someAsyncOperation(callback)
}
- continuation: 表示当前被挂起的协程。它提供了恢复协程的方法:resume(value)和resumeWithException(exception)。
- 缺点:如果调用这个suspend fun 的协程被取消。someAsyncOperation不会自动终止。这可能导致资源浪费或意外结果。只适用于异步操作本身非常快或你不关心取消的情况。
suspendCancellableCoroutine
(支持取消)
suspend fun <T> convertCallbackToSuspendingFuncSafely(): T = suspendCancellableCoroutine { cancellableContinuation ->
// 1. 启动异步操作
val callback = object : Callback<T> {
override fun onSuccess(result: T) {
cancellableContinuation.resume(result)
}
override fun onFailure(error: Throwable) {
cancellableContinuation.resumeWithException(error)
}
}
someAsyncOperation(callback)
// 2. ⭐ 关键步骤:注册取消监听器 ⭐
cancellableContinuation.invokeOnCancellation {
// 当协程被取消时,尝试取消底层的异步操作
// 通常调用库提供的取消方法,例如:cancelOperation(callback)
cancelOperation(callback)
}
}
- 当调用这个
suspend fun
的协程被取消时:- 协程的取消状态会传播到这个
CancellableContinuation
。 - 触发
invokeOnCancellation {}
块。 - 在块内,你调用库的取消方法去清理资源并停止操作。
- 协程的取消状态会传播到这个
- 这是将回调 API 集成到协程世界的最佳实践。
协程作用域
1、什么是协程作用域(CoroutineScope)
CoroutineScope
不是协程本身,而是一个定义新协程运行环境的接口- 它为在其内部启动的所有子协程提供了一个统一的
CoroutineContext
基础 - 它将所有在其内部启动的协程组织在一个结构中,以便进行生命周期管理
核心构成:
CoroutineScope接口只有一个属性:
interface CoroutineScope {
val coroutineContext: CotoutineContext
}
这个coroutineContext
是启动在该作用域内的所有子协程的默认上下文。
协程作用域的核心优点
- 生命周期的自动管理(取消)
- 避免内存泄漏
- 简化资源清理
- 安全性与清晰性
- 取消传播:作用域的取消会自动且可靠地传播到所有子协程,无需开发者手动级联取消
- 异常处理控制:作用域定义了异常传播的策略(通过其关联的
Job
是普通Job
还是SupervisorJob
)。SupervisorJob
允许子协程独立失败而不影响其他兄弟协程和父作用域(常用于UI组件中单独的后台任务)。 - 代码组织清晰:将任务划分到不同的作用域中,体现了任务的逻辑和生命周期归属。
- 上下文继承与共享
- 在作用域内启动的协程默认继承作用域的coroutineContext
- 可以指定不同的CoroutineContext元素来覆盖上下文
- 协同等待(job.join(), coroutineScope {})
- 你可以通过作用域的关联
Job
(调用scope.coroutineContext[Job]!!.join()
或更简单地使用coroutineScope {}
构建器) 来等待作用域内所有子协程全部完成。这保证了任务内部的并发操作在外部看来是原子性的(即父协程等待所有子协程完成后自己才完成)。 coroutineScope {}
构建器本身也会创建一个新的子作用域,并等待其内部所有子协程完成
- 你可以通过作用域的关联
async的行为
1. async的立即启动特性
- async不是等待await才执行:当使用async { … } 创建协程时,其中的代码会立即开始执行,而不是等待调用
await()
。即使不调用await(),协程体也会在后台启动。 - 并发执行:多个async协程默认并行执行(取决于调度器和上下文)。例如:
val deferred1 = async { task1() } // 立即开始执行
val deferred2 = async { task2() } // 立即开始执行(可能与 task1 同时运行)
2. await()的作用
- 同步等待结果:await()是一个挂起点:
- 如果协程已完成:直接返回结果
- 如果协程未完成:挂起当前协程,等待结果但不阻止其他协程执行
- 顺序控制:await()可强制等待顺序:
val result1 = deferred1.await() // 等待 task1 完成
val result2 = deferred2.await() // 再等待 task2 完成
Q&A
-
父协程的挂起会影响子协程的运行吗?
- 通常不会影响子协程的运行,父协程与子协程是独立的任务单元,只要子协程未完成且未被取消,他们会继续由调度器分配线程执行。取消是取消,挂起时让出当前线程
-
父作用域会等待所有子协程完成后才继续执行?
- 是的,一个是隐式等待,一个是由coroutineScope构建的显式等待。
suspend fun complexTask() = coroutineScope { // ⭐父作用域⭐
val deferred1 = async {
delay(1000)
"结果1"
}
val deferred2 = async {
delay(1500)
"结果2"
}
println("组合结果: ${deferred1.await()} + ${deferred2.await()}")
// 🚫 会等待两个async都完成才执行
}
runBlocking {
println("开始任务")
complexTask() // 👇挂起点👇
println("✅ 所有嵌套协程完成") // 在complexTask内部完成后执行
}