在使用kotlin 封装SDK将回调改写为协程以便调用主题中,我们先澄清一下同步 异步 阻塞 非阻塞 的关联区别。
同步、异步:
从 API 形式上来看,同步和异步的区别在于:
- 同步:函数直接返回结果
- 异步:调用函数时同时传递一个 Callback,或者函数返回的一个 Feature
阻塞、非阻塞:
一般情况都是由于 IO 操作导致阻塞和非阻塞。你可以站在程序或者线程的角度看待这个问题,但是站在程序的立场,很难解释非阻塞这个词汇,所以我们站在线程这个角度来看。
-
阻塞:我是一个线程,当程序进行某一个调用时,我被阻塞(绑架)了,我不能去干其他事情,只能等待
-
非阻塞 :我是一个线程,当程序进行某一个 IO 调用时,我知道他需要一些时间,所以我先去执行其他程序
显然非阻塞要能实现,程序必须是复杂的,必须需要运行时环境提供支持。至于运行时环境如何高效实现非阻塞,则必须和操作系统相互配合。(Java 的 NIO、linux 的 select、poll、epoll )
此刻记在你脑子里的是,是否阻塞是底层系统在 IO 调用时给你的能力,同步和异步只是 API 的形式。
从编程难易程度上来看,同步比异步要简单很多,也和人类思维过程比较接近。从系统性能角度来看,非阻塞效率相当高,在互联网高并发场景下,这尤为重要。
对Java开发者而言,“回调”是再常见不过的概念了。从各种SDK到我们自己开发的代码,处处充满了回调。某个任务需要长时间执行,同时我们希望能在任务完成时得到通知,在函数参数里加上一个回调对象,用以收取结果,是十分常见的解决方案。
- 嵌套太多,成为“回调地狱”;
- 传入的callback如果是Activity会引起泄露;
- 代码阅读起来不直观。
这几个隐患不详述了,接下来看看在Kotlin中如何将异步回调转换为同步请求。
下面看如何将耗时函数的回调写法利用协程转换为挂起函数。
举例一:OkHttp 的网络请求转换为挂起函数
suspend fun <T> Call<T>.await(): T =
suspendCoroutine { cont ->
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
cont.resume(response.body()!!)
} else {
cont.resumeWithException(ErrorResponse(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
cont.resumeWithException(t)
}
})
}
上面的await()是一个Call
的拓展函数调用时,使用 suspendCoroutine{} 将请求挂起,然后执行enqueue
将网络请求放入队列中,当请求成功时,耗时操作完成后,通过cont.resume(response.body()!!)
来恢复之前的协程。resume传递执行结果,resumeWithExeption传递异常。
调用处写法如下:这里假设Service.loadData()会返回一个Call对象
GlobalScope.launch(Dispatchers.Main) {
try {
/**
这里假设Service.loadData()会返回一个Call<T>对象.
**/
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 <T> Call<T>.await(): T = suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
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<T>对象.
**/
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<Int> {
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...
当中捕获,那么我们可以说真正做到了把异步代码变成了同步的写法。