Kotlin 朱涛-18 实战 Retrofit 协程 挂起函数 异步回调
目录
18 | 实战:让KtHttp支持挂起函数
因为业界对 Kotlin 函数式编程
接纳度并不高,所以这节课,我们将基于 1.0 版本命令式风格
的代码继续改造:
- 3.0 版本,在 1.0 版本的基础上,扩展出异步请求的能力
- 4.0 版本,进一步扩展异步请求的能力,让它支持挂起函数
支持异步请求 Call
挂起函数本质就是 Callback,为了让 KtHttp 支持挂起函数,我们要先让 KtHttp 支持异步请求
,也就是 Callback 请求。
回调接口 Callback
可以在这个 Callback 中拿到 API 请求的结果。
interface Callback<T: Any> { // 泛型边界 Any 可以保证 T 非空
fun onSuccess(data: T)
fun onFail(throwable: Throwable)
}
结果回调 KtCall
KtCall 用来发起网络请求、解析请求结果,并将最终结果回调给 Callback。
class KtCall<T: Any>( // 泛型边界 Any 可以保证 T 非空
val call: Call, // OkHttp 的 Call 对象,用于发起网络请求(同步或异步)
val type: Type // Gson 解析时指定的反射类型
) {
fun call(callback: Callback<T>) {
call.enqueue(object : okhttp3.Callback { // 使用 call 异步请求 API
override fun onFailure(call: Call, e: IOException) {
callback.onFail(e) // 根据请求结果,调用 callback 的回调方法
}
override fun onResponse(call: Call, response: Response) {
try {
val text = response.body?.string() // 网络请求结果
val t = Gson().fromJson<T>(text, type) // Gson 解析
callback.onSuccess(t)
} catch (e: Exception) {
callback.onFail(e)
}
}
})
}
}
请求参数 ApiService
和 1.0 版本的唯一区别就是方法的返回值类型。
interface ApiServiceV3 {
@GET("/repo")
fun repos(@Field("lang") lang: String, @Field("since") since: String): KtCall<RepoList>
}
请求封装 OkHttp
// 改动点 ① :这里对泛型 T 增加了泛型边界 Any 的限制
private fun <T: Any> request(path: String, method: Method, args: Array<Any>): Any? {
val annotations = method.parameterAnnotations
if (annotations.size != args.size) return null
var url = path
for (indice in annotations.indices) {
for (parameterAnnotation in annotations[indice]) {
if (parameterAnnotation is Field) {
val key = parameterAnnotation.value
val value = args[indice].toString()
val param = "$key=$value"
url += if (url.contains("?")) "&$param" else "?$param"
}
}
}
val request = Request.Builder().url(url).build()
// -------------------------- 改动点 ② --------------------------
val call = OkHttpClient().newCall(request) // 创建 OkHttp 的 Call 对象
val pType = method.genericReturnType as ParameterizedType // 带有类型参数的类型
val type = pType.actualTypeArguments[0] // 获取 X<T, P> 里的类型参数的类型(T、P)
return KtCall<T>(call, type) // 例如:KtCall<RepoList> 中的 RepoList 类型
}
动态代理 Proxy
// 改动点 ① :这里对泛型 T 增加了泛型边界 Any 的限制
fun <T : Any> create(service: Class<T>): T {
val loader: ClassLoader = service.classLoader
val interfaces = arrayOf<Class<*>>(service)
val any: Any? = Proxy.newProxyInstance(loader, interfaces) { _, method, args ->
for (annotation in method.annotations) {
if (annotation is GET) {
val value = annotation.value
val url = "https://trendings.herokuapp.com{annotation.value}"
return@newProxyInstance request<T>(url, method, args!!) // 改动点 ②
}
}
return@newProxyInstance null
}
@Suppress("UNCHECKED_CAST")
return any as T
}
使用
fun main() {
val api: ApiServiceV3 = create(ApiServiceV3::class.java) // 动态代理
val ktCall: KtCall<RepoList> = api.repos(lang = "Kotlin", since = "weekly")
ktCall.call(object : Callback<RepoList> { // 发起异步请求
override fun onSuccess(data: RepoList) = println(Gson().toJson(data))
override fun onFail(throwable: Throwable) = println(throwable)
})
}
熟练之后就可以使用
链式调用
了,但是如果不熟练的话,使用链式调用
会让人一脸懵逼。
支持挂起函数
如果底层框架是用 Callback 写的,不支持挂起函数
,上层业务开发人员,可以通过如下两种方法使用协程
:
- 第一种方式,不改动 SDK 内部的实现,在 SDK 的基础上扩展出协程的能力
- 这种方式在工作中十分常见
- 第二种方式,直接修改 SDK 内部的实现,让 SDK 直接支持挂起函数
- 由于涉及到挂起函数更底层的一些知识,具体方案会在源码篇的第 27 讲介绍
扩展一个挂起函数
首先是为 KtCall
扩展出一个挂起函数
,挂起函数的返回值类型是泛型 T
。
import kotlin.coroutines.Continuation
import kotlin.coroutines.suspendCoroutine
suspend fun <T : Any> KtCall<T>.await(): T = // 扩展一个 带返回值的挂起函数
suspendCoroutine { c: Continuation<T> -> // 调用一个 高阶、挂起函数
call(object : Callback<T> { // 调用 call,传入一个 Callback
override fun onSuccess(data: T) = c.resumeWith(Result.success(data))
override fun onFail(t: Throwable) = c.resumeWith(Result.failure(t))
})
}
以上代码的含义是:
- 首先给
KtCall
扩展一个挂起函数
- 此挂起函数的返回值类型是
KtCall<T>
中声明的T
,即KtCall<RepoList>
中的RepoList
- 调用扩展函数后,实际上会调用
suspendCoroutine()
方法,这是一个高阶、挂起函数
- 紧接着会调用
KtCall
中的call()
方法,并传入一个我们定义的Callback
实例 - 当网络请求执行
成功/失败
以后,就会回调Callback
的onSuccess/onFail
方法 - 在
onSuccess/onFail
方法中,通过调用continuation.resumeWith()
返回结果
Continuation
前面讲过,Continuation
其实就是挂起函数的 Callback
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>) // 用于恢复
}
resumeWith()
是用于恢复
的- 参数类型
Result<T>
代表一个带有泛型的结果
,它的作用是承载协程执行的结果
suspendCoroutine
suspendCoroutine
是 Kotlin 官方提供的一个顶层函数
,他的作用其实就是,suspend_Coroutine 挂起协程,并在任务完成后恢复。
Obtains the current
Continuation
instance insidesuspend
functions and suspends the currently runningCoroutine
.
public suspend inline fun <T> suspendCoroutine(
crossinline block: (Continuation<T>) -> Unit
): T {...}
- 首先,它是一个挂起函数
- 参数类型
(Continuation) -> Unit
等价于挂起函数
类型(逆CPS
转换),所以也支持挂起和恢复(其回调方法resumeWith()
就是用于恢复的) suspendCoroutine{}
的作用,其实就是将挂起函数中的Continuation
暴露出来
简化回调
可以借助 Kotlin 官方提供的扩展函数,简化回调代码逻辑。
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
public inline fun <T> Continuation<T>.resume(value: T): Unit =
resumeWith(Result.success(value))
public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
resumeWith(Result.failure(exception))
suspend fun <T : Any> KtCall<T>.await(): T = // 扩展一个 带返回值的挂起函数
suspendCoroutine { c: Continuation<T> -> // 调用一个 高阶、挂起函数
call(object : Callback<T> { // 调用 call,传入一个 Callback
override fun onSuccess(data: T) = c.resume(data)
override fun onFail(t: Throwable) = c.resumeWithException(t)
})
}
使用
fun main() = runBlocking {
val api: ApiServiceV3 = create(ApiServiceV3::class.java) // 动态代理
val ktCall: KtCall<RepoList> = api.repos(lang = "Kotlin", since = "weekly")
val data: RepoList = ktCall.await() // 已经没有回调了
println(Gson().toJson(data))
}
支持取消
测试案例
fun main() = runBlocking {
val start = System.currentTimeMillis()
fun time() = System.currentTimeMillis() - start
val deferred: Deferred<String> = async {
val data: RepoList = create(ApiServiceV3::class.java).repos(lang = "Kotlin", since = "weekly").await()
println("返回时间:${time()} - ${data.msg}")
"这是 deferred.await() 的返回值"
}
deferred.invokeOnCompletion {
println("结束时间:${time()}")
}
delay(50L)
deferred.cancel()
println("取消时间:${time()}")
try {
val result: String = deferred.await()
println("await 结束时间:${time()} - $result")
} catch (e: Exception) {
println("发生异常:${time()} - ${e.message}")
}
}
取消时间:643
返回时间:3142 - suc
结束时间:3143
发生异常:3159 - DeferredCoroutine was cancelled
上面代码中,我们在 async 里调用了挂起函数,50ms 后我们尝试取消协程,结果发现:
- 即使调用了
cancel()
,内部的网络请求仍然会继续执行,且仍能正常返回结果 - 调用
cancel()
后再调用await()
会抛出异常 - 虽然调用
await()
后会抛出异常,但它并不是马上就抛,而是会等内部的网络请求执行结束以后才抛出异常,在此之前都会被挂起
所以,使用 suspendCoroutine{}
来实现的挂起函数,默是不支持取消的。
suspendCancellableCoroutine
使用 Kotlin 官方提供的 suspendCancellableCoroutine{}
,可以在 continuation 上设置一个监听 invokeOnCancellation{}
,在当前协程被取消时,我们只需要将 OkHttp 的 call 取消即可。
suspend fun <T : Any> KtCall<T>.await(): T = // 扩展一个 带返回值的挂起函数
suspendCancellableCoroutine { c: CancellableContinuation<T> -> // 支持取消
call(object : Callback<T> { // 调用 call,传入一个 Callback
override fun onSuccess(data: T) = c.resume(data)
override fun onFail(t: Throwable) = c.resumeWithException(t)
})
c.invokeOnCancellation {
println("协程被取消啦")
this.call.cancel() // 将 OkHttp 的 call 取消
}
}
协程被取消啦
取消时间:646
结束时间:661
发生异常:661 - DeferredCoroutine was cancelled
可以发现:
- 调用
cancel()
后,invokeOnCancellation
会立即响应协程取消事件 - 调用
cancel()
后再调用await()
依旧会抛出异常,并且是立即抛出异常,而不会挂起很长时间 - 通过在
invokeOnCancellation
中调用call.cancel()
,可以正常取消 OkHttp 的网络请求
结论:使用 suspendCancellableCoroutine
可以避免不必要的挂起,节省计算机资源,避免不必要的协程任务。
不监听 invokeOnCancellation
如果不监听 invokeOnCancellation
,或不在 invokeOnCancellation
中调用 call.cancel()
,那么,网络请求仍会成功发送且不会取消,只是没法再对网络请求结果监听和回调了,即:没有谁再去回调 Callback 了。
小结
上面这种方式并没有改动 KtHttp 的源代码,而是以扩展函数
来实现的。
解法二的代码,要等学完第 27 讲,深入理解了挂起函数的底层原理后,再来完成。
2017-11-07
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7798132.html