前言

不知道你有没有发现,在之前文章中,咱们虽然界说了带suspend关键字的挂起函数,可是里边的完成咱们一般都是调用其他挂起函数,是协程库提供的,或者第三方库完成的,比如咱们熟知的Retrofit库,就能够直接在ApiService中界说挂起函数。

那咱们能够自己完成一个挂起函数吗?其实在# 协程(09) | 完成一个简易Retrofit这篇文章中,咱们就把Callback转化为了挂起函数,当时是说这种适合没有权限修正第三方库的情况,那现在咱们有权限修正源码了,怎么优雅地完成挂起函数呢?

本篇文章就来扩展之前做的Retrofit,咱们来看看从界说一个挂起函数,到完成,到调用这一整个流程怎么完成。

正文

因为代码是在前面项目上进行修正和扩展的,所以强烈建议查看文章:# 协程(09) | 完成一个简易Retrofit和# 协程(15) | 挂起函数原理解析,这俩篇文章一个是代码的之前进度,好做比较,另一个是挂起函数的原理,方便本篇文章的开端。

Continuation理解

在前面挂起函数原理解析中,咱们剖析过,挂起函数经过CPS转化后,它便是一个状态机模型。在一个挂起函数里,调用N个挂起函数,会发生N+1个分支,经过屡次调用自己,把每个挂起函数的值都保存到一个仅有的Continuation目标中,然后节约内存。

经过原理后,咱们再来看看这个Continuation接口的界说:

/**
 * Interface representing a continuation after a suspension point 
 * that returns a value of type `T`.
 */
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}

它是一个接口,表明挂起点后的连续,回来类型为T的值。

什么是接口,接口能够看成是一种通用的笼统和封装,而这儿的Continuaiton接口则是对挂起函数的一种行为封装。即一切挂起函数,它都会有一个挂起点,然后该挂起点后边的协程代码便是连续(continuation),该挂起函数康复时会带着T类型的值,这么一看Continuaiton的泛型参数也就很好理解了。

再接着看,因为挂起函数需求康复的行为,所以接口中界说和笼统了resumeWith办法,并且还需求当前协程的上下文context

这儿说一个特别的完成,咱们在平时运用挂起函数时,好像从来没有运用过这个continuation,可是能够在挂起函数中访问协程上下文,这是咋做到的呢?

比如下面代码:

suspend fun testCoroutine() = coroutineContext

在该挂起函数中,咱们能够访问协程上下文,能够发现界说如下:

public suspend inline val coroutineContext: CoroutineContext
    get() {
        throw NotImplementedError("Implemented as intrinsic")
    }

这居然是一个suspend inline类型的变量,不用惊奇,这种写法是Kotlin编译器帮咱们做的,咱们自己无法完成,经过这种办法,咱们再反编译一下上面代码:

public static final Object testCoroutine(@NotNull Continuation $completion) {
   return $completion.getContext();
}

就能够明晰地发现,咱们调用的其实仍是Continuation目标的协程上下文,常识协程框架帮咱们做了省略。

完成挂起函数

在更深入理解了Continuation接口后,咱们来完成一个挂起函数。

首先是直接在ApiService中界说挂起函数:

/**
 * [reposSuspend]是挂起函数,这儿运用直接界说
 * 和完成挂起函数的方式
 * */
@GET("/repo")
suspend fun reposSuspend(
    @Field("lang") language: String,
    @Field("since") since: String
): RepoList

然后在invoke中,和处理Flow分支一样,咱们再加个分支来处理suspend函数:

private fun <T : Any> invoke(path: String, method: Method, args: Array<Any>): Any? {
        if (method.parameterAnnotations.size != args.size) return null
         ...
        //类型判别
        return when {
            isSuspend(method) -> {
               ...
            }
            isKtCallReturn(method) -> {
                ...
            }
            isFlowReturn(method) -> {
               ...
            }
            else -> {
                ...
            }
        }
    }
/**
 * 判别办法是否是[suspend]办法
 * */
private fun isSuspend(method: Method) =
    method.kotlinFunction?.isSuspend ?: false

这儿判别办法是否是挂起函数,需求用到Kotlin的反射相关API。

这儿咱们就能够处理中心逻辑了,当是挂起函数时怎么完成,首先咱们界说一个realCall办法,在该办法中咱们完成挂起函数:

/**
 * 该办法是[suspend]办法,用于完成挂起函数,其实在内部也调用了一个挂起函数,
 * 这儿的重点是[Continuation]参数,运用该参数,回来挂起函数康复的值。
 *
 * @param call [OkHttp]的[call]目标,用于网络恳求。
 * @param gson [Gson]的目标,用于反序列化实例
 * @param type [Type]的目标,它是差异与[Class],是真实表明一个类的类型
 * */
suspend fun <T: Any> realCall(call: Call,gson: Gson,type: Type): T =
    suspendCancellableCoroutine { continuation ->
        call.enqueue(object : Callback{
            override fun onFailure(call: Call, e: IOException) {
                continuation.resumeWithException(e)
            }
            override fun onResponse(call: Call, response: Response) {
                try {
                    val t = gson.fromJson<T>(response.body?.string(), type)
                    continuation.resume(t)
                } catch (e: java.lang.Exception){
                    continuation.resumeWithException(e)
                }
            }
        })
        continuation.invokeOnCancellation {
            call.cancel()
        }
}

在该办法中,咱们经过调用suspendCancellableCoroutine办法来完成一个挂起函数,其间就运用了对外露出的continuation目标,来回来该挂起函数康复时所回来的值。

那么现在就剩最终一步了,凑齐该办法所需求的参数,然后调用它,咱们在invoke办法中如下写:

isSuspend(method) -> {
    //反射获取类型信息
    val genericReturnType = method.kotlinFunction?.returnType?.javaType
        ?: throw java.lang.IllegalStateException()
    //调用realCall办法
    //该办法会报错
    realCall<T>(call, gson,genericReturnType)
}

这儿会发现咱们在一般的invoke办法中根本无法调用挂起函数realCall,这儿要怎么做呢?

经过上一篇挂起函数原理的剖析,咱们知道编译器会解析suspend关键字,经过CPS转化后,函数类型会变化,那咱们运用函数引证,来获取realCall的非suspend函数类型,然后再调用,代码如下:

//反射获取类型信息
val genericReturnType = method.kotlinFunction?.returnType?.javaType
    ?: throw java.lang.IllegalStateException()
val continuation = args.last() as? Continuation<T>
Log.i(KtHttp.javaClass.simpleName, "invoke: continuation : $continuation")
//将挂起函数类型转化成,带Continuation的类型
val func = ::realCall as (Call,Gson,Type,Continuation<T>?) -> Any?
//这儿依旧无法调用
func.invoke(call, gson,genericReturnType,continuation)

这儿无法运用的原因是func的函数类型,它是带泛型T的,目前Kotlin还不支持带泛型的函数类型,那只能想办法把T给消除掉了:

//界说一个暂时函数,调用规定了泛型类型
suspend fun temp(call: Call, gson: Gson, type: Type) = realCall<RepoList>(call, gson, type)
//反射获取类型信息
val genericReturnType = method.kotlinFunction?.returnType?.javaType
    ?: throw java.lang.IllegalStateException()
val continuation = args.last() as? Continuation<T>
Log.i(KtHttp.javaClass.simpleName, "invoke: continuation : $continuation")
//这样func能够彻底获取temp函数的引证
val func = ::temp as (Call,Gson,Type,Continuation<T>?) -> Any?
func.invoke(call, gson,genericReturnType,continuation)

上面咱们经过界说temp函数来消除了T,可是却有问题,代码不具有普遍性了,当函数回来值为其他类型,则无法执行。

正确的做法是经过反射,能够拿到realCall函数的函数引证,代码如下:

isSuspend(method) -> {
    //反射获取类型信息
    val genericReturnType = method.kotlinFunction?.returnType?.javaType
        ?: throw java.lang.IllegalStateException()
    //打印看看一切的参数
    Log.i(KtHttp.javaClass.simpleName, "invoke: args : ${args.toList()}")
    //创建continuation实例
    val continuation = args.last() as? Continuation<T>
    Log.i(KtHttp.javaClass.simpleName, "invoke: continuation : $continuation")
    //经过反射拿得realCall办法
    val func = KtHttp::class.getGenericFunction("realCall")
    func.call(this, call, gson, genericReturnType, continuation)
}
/**
 * 获取办法的反射目标
 * */
private fun KClass<*>.getGenericFunction(name: String): KFunction<*> {
    return members.single { it.name == name } as KFunction<*>
}

经过反射,咱们能够拿到realCall的办法,然后经过反射调用,咱们就能够在非挂起函数中调用挂起函数了。

留意上面的打印,咱们打印了continuation以及一切参数,咱们先来执行一下上面代码,如下:

findViewById<TextView>(R.id.suspendCall).setOnClickListener {
    lifecycleScope.launch {
        val data = KtHttp.create(ApiService::class.java).reposSuspend(language = "Kotlin", since = "weekly")
        findViewById<TextView>(R.id.result).text = data.toString()
    }
}

这儿咱们就能够运用咱们完成的挂起函数来以同步的方式写出异步的代码了。

这儿你或许会有疑问,咱们传递了2个参数,可是上面打印究竟是什么呢?打印如下:

invoke: args : [Kotlin, weekly,
Continuation at com.example.wan.MainActivity$onCreate$6$1.invokeSuspend(MainActivity.kt:70)]

会发现在实际运行时,因为该办法是挂起函数,依据上一篇文章咱们说的内容,会在办法后边增加一个Continuation类型的额定参数。运用这个额定的参数,咱们就能够在非挂起函数中,调用挂起函数了。

总结

本篇文章,和咱们日常开发关系非常大,做个简略总结:

  • 先是Continuation接口的笼统,它表明挂起函数在协程某个挂起点的挂起和康复,是一种行为的笼统。
  • 随后咱们自己完成了挂起函数,经过高阶函数suspendCancellableCoroutine露出的continuation实例,咱们能够设置康复时的数据。
  • 最终便是怎么在非挂起函数调用咱们的挂起函数呢?办法便是依据前一节说的挂起函数CPS后的本质函数类型来调用,具体办法必须得是经过反射。
  • 关于continuation这个目标,会在调用挂起函数时主动增加,它的值打印的话,会如上面所示,显现在哪里挂起和连续。

本篇文章所触及的代码:github.com/horizon1234…**