回调的写法在Java中再常见不过了。但是它却有着不小的隐患:
- 嵌套太多,成为“回调地狱”;
- 传入的callback如果是Activity会引起泄露;
- 代码阅读起来不直观。
主要靠两个函数:
不支持取消的suspendCoroutine{}
支持取消的suspendCancellableCoroutine{};
回调结果回传使用的continuation.resumeWithException,continuation.resume;continuation.resumeWith
回调函数转协程通常使用两个协程相关的类:suspendCancellableCoroutine和suspendCoroutine,前者可以通过cancel()方法手动取消协程的执行,而suspendCoroutine没有该方法,调用cancel()后协程不再往下执行,抛出 CancellationException 异常,但是程序不会崩溃,这样会更加安全,通常推荐使用suspendCancellableCoroutine
举例一:OkHttp 的网络请求转换为挂起函数suspend fun Call.await(): T =
suspendCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
continuation.resume(response.body()!!)
} else {
continuation.resumeWithException(ErrorResponse(response))
}
}
override fun onFailure(call: Call, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
上面的await()是一个Call
的拓展函数调用时,使用 suspendCoroutine{} 将请求挂起,然后执行enqueue
将网络请求放入队列中,当请求成功时,耗时操作完成后,通过cont.resume(response.body()!!)
来恢复之前的协程。resume传递执行结果,resumeWithExeption传递异常。
调用处写法如下:这里假设Service.loadData()会返回一个Call对象
GlobalScope.launch(Dispatchers.Main) {
try {
/**
这里假设Service.loadData()会返回一个Call对象.
**/
val result = Service.loadData().await()
} catch (e: Exception) {
userNameView.text = "Get User Error: $e"
}
}
举例二:支持取消的挂起函数
import kotlinx.coroutines.suspendCancellableCoroutine
import retrofit2.Call
import retrofit2.Callback
import retrofit2.HttpException
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun Call.await(): T = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback {
override fun onFailure(call: Call, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call, response: Response) {
response.takeIf { it.isSuccessful }?.body()?.also {
continuation.resume(it)
} ?: continuation.resumeWithException(HttpException(response))
}
})
}
支持挂起函数的取消和不支持取消的差异点在于:
- 使用suspendCancellableCoroutine{}
- 需要调用Call的取消方法cancel(),也就是被扩展的取消方法,这里的cancel是retrofit中的。
调用方法,最后可以不加cancelAndJoin(),加上的话会立即取消掉而没有任何效果:
GlobalScope.launch(Dispatchers.Main) {
try {
/**
这里假设Service.loadData()会返回一个Call对象.
**/
val result = Service.loadData().await()
} catch (e: Exception) {
userNameView.text = "Get User Error: $e"
}
}.cancelAndJoin()
举例三:将耗时计算的回调转化为协程写法
类似的,有个 calcSlowlySync 为耗时方法,改写后如下:
suspend fun calcSlowlySync(inp: Int): Int =
suspendCoroutine { cont ->
calcSlowly(inp, object: CalcTaskCallback {
override fun onSuccess(result: Int) {
cont.resume(result)
}
override fun onFailure(code: Int, msg: String) {
cont.resumeWithException(Exception("code=$code, msg=$msg"))
}
})
}
调用写法如下:
CoroutineScope(Dispatchers.Main).launch {
try {
val result = calcSlowlySync(100)
println("result=$result")
} catch (e: Exception) {
LogUtils.e(TAG, "result exception: ", e)
}
}
注意如下几点即可:
1、在耗时方法fun前需要添加suspend关键字,表示挂起函数,标注这里是一个耗时操作;
2、在回调函数成功或者失败时,需要将结果返回出去,利用resume传递执行结果,resumeWithExeption传递异常;
3、在外面使用的地方,需要在协程作用域中调用,例如例子中的CoroutineScope(Dispatchers.Main).launch{}内;
4、异步回调的异常处理改写为同步后是通过try{}catch(){}代码块进行捕捉的:一个异步的请求异常,我们只需要在我们的代码中捕获就可以了,这样做的好处就是,请求的全流程异常都可以在一个 try...catch...
当中捕获,那么我们可以说真正做到了把异步代码变成了同步的写法。
参考:破解 Kotlin 协程(4) - 异常处理篇 - 腾讯云开发者社区-腾讯云