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
Continuationinstance insidesuspendfunctions 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

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号