End

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 实例
  • 当网络请求执行 成功/失败 以后,就会回调 CallbackonSuccess/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 inside suspend functions and suspends the currently running Coroutine.

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

posted @ 2017-11-07 11:17  白乾涛  阅读(4498)  评论(0编辑  收藏  举报