携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情
前言
本文主要通过阅读Retrofit的源码,探究Retrofit是如何实现兼容协程(挂起)的。需要读者有一定的Retrofit源码基础以及Kotlin语法基础。话不多说,马上开始!
PS:本文源码基于2.8.0版本
正文
协程调用
使用过Retrofit的朋友都能够熟练地写出如下的网络请求接口:
interface TestApi{
@GET("../..")
fun getData():Call<MyDataBean>
}
调用该方法并没有实际进行网络请求,而是将网络请求封装成Call。实际进行请求的时候还需要手动调用Call.excute(同步请求)或者Call.enqueue(异步请求)。
这样用也没什么不妥的,但是Retrofit既然支持Kotlin的协程调用,那说明还是很有意义的。在此不讨论用什么方式调用才是最优方案,只讨论实现原理。
如果使用协程调用的话,上面的例子就需要进行一些改动:
interface TestApi{
@GET("../..")
suspend fun getData():MyDataBean
}
主要改动是两个地方,加了一个suspend关键字以及返回类型不再是Call,而直接就是MyDataBean(即业务Bean类)。
了解过协程的朋友自然知道suspend关键字,其作用是标记一个方法为挂起方法,需要在另一个挂起方法内或者在协程内才能被调用。不了解的朋友自行学习一下,这里也不展开了,免得跑题了。
直接返回MyDataBean,说明该方法是“所调即所得”。直接调用该方法就能得到返回结果。看起来这是个同步执行的过程,其实不然,协程的特点就是能用同步的方式写出异步代码。同样也不继续展开了。
改造成协程调用的最大区别在于,调用该方法的时候,网络请求就已经进入了执行的步骤,无需再调用类似Call.enqueue的方法。
原理
Retrofit的核心在于动态代理,通过建立一个代理类,动态地将方法调用转发到InvocationHandler的invoke方法内,再通过创建ServiceMethod接着调用invoke构建Call。
public <T> T create(final Class<T> service) {
validateServiceInterface(service);
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];
@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
//实际调用的方法:ServiceMethod.invoke
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}
协程调用的区别在于ServiceMethod的invoke方法并不是返回Call,而是网络请求的结果。
首先来看看创建ServiceMethod的代码:
ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = ServiceMethod.parseAnnotations(this, method); //重点
serviceMethodCache.put(method, result);
}
}
return result;
}
serviceMethodCache毫无疑问是一个Map,存放着每一个已经调用过的ServiceMethod,这样的好处是再次调用的时候无需重新创建ServiceMethod,典型的空间换时间的策略。
这里的代码很简单,就是检索一下调用的方法是否已经缓存过,有就拿出来直接调用,没有则构建一个并存入缓存中。重点是构建ServiceMethod,继续追踪代码,实际上调用的是HttpServiceMethod的parseAnnotations方法:
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;
//获取注解
Annotation[] annotations = method.getAnnotations();
//获取方法的直接返回类型(Call,Observable等)
Type adapterType;
if (isKotlinSuspendFunction) {
//是挂起方法,需要特殊处理
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType = Utils.getParameterLowerBound(0,
(ParameterizedType) parameterTypes[parameterTypes.length - 1]);
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
//表明需要返回Response的标志位
continuationWantsResponse = true;
}
//统一以Call为直接返回类型
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);//①
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
//非协程,普通用法则直接取出方法的直接返回类型
adapterType = method.getGenericReturnType();
}
//构建CallAdapter、Converter,省略
okhttp3.Call.Factory callFactory = retrofit.callFactory;
//开始返回
if (!isKotlinSuspendFunction) {
//非协程,返回正常的CallAdapted
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//协程调用,并且需要Response,返回SuspendForResponse
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//协程调用,返回SuspendForBody
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
}
PS:以上代码经过了一些删除,去掉了与协程调用实现无关的代码。
首先说一下adapterType,这玩意表明的是被调用的方法的直接返回类型,普通使用则一般为Call,使用Rxjava则为Observable,当然还有自定义的类型(自定义CallAdapter)。而协程调用的返回类型比较特殊,并不返回Call而是直接返回网络请求获得的数据。从源码可以看出,如果是协程调用,会在返回类型上再包装一层Call(代码①处),统一使用Call。因为实际上进行网络请求的时候,最终还是调用了Call.enqueue方法的,后面会说到。
最后是返回HttpServiceMethod。可以看到分了三种情况返回
- 非协程,返回CallAdapted
- 协程,并且返回类型为Response,返回SuspendForResponse
- 协程,普通返回类型,返回SuspendForBody
因为本文主题为协程调用,所以第一种忽略。
第二种为相对特殊的情况(比如下载文件的网络请求,需要拿到Response),本文也暂时不展开。
重点看第三种情况,返回的是SuspendForBody,到底是什么东东呢?既然它能作为HttpServiceMethod返回,那必然是HttpServiceMethod的子类。这时候先放一放,只需知道它是ServiceMethod的子类(HttpServiceMethod是ServiceMethod子类)就行了。我们回过头来看看InvocationHandler的invoke方法,返回的是ServiceMethod的invoke,而我们已经知道了,实际上返回的是SuspendForBody,那么就可以直接看SuspendForBody的invoke方法,好吧~SuspendForBody并没有invoke方法。不要紧!它没有,它爸爸有。我们来看看HttpServiceMethod的invoke方法:
//class HttpServiceMethod
@Override final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}
很简单,创建了一个OkhttpCall(Call的一个实现类),并返回adapt方法的结果,adapt方法则是一个抽象方法,子类负责具体实现,那么这时候就是SuspendForBody的舞台了:
//class SuspendForBody
@Override protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
//以下是重点
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
着重看重点部分,只需看await方法,awaitNullable只是适配Kotlin的可空类型。
KotlinExtensions.await(call,continuation)这其实是Java调用Kotlin的扩展方法的方式,同时也是调用Kotlin挂起方法的方式,第一个参数是Receiver,对应扩展方法的Receiver;第二个参数则是挂起方法需要的Continuation。以下是await的代码:
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation -> //①
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> { //②
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name +
'.' +
method.name +
" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
果不其然,await既是一个挂起方法,同时也是Call的扩展方法。可以看到,实际上协程调用还是调用了enqueue这个方法(代码②处)。
再看看代码①处,这里是协程的一种用法:异步转协程。协程会被一直阻塞,直到resume。当网络请求的结果返回时(即onResponse),根据不同的情况resume来返回结果(或异常)。协程带着网络请求结果resume时,就会层层上送直到InvocationHandler的invoke方法返回,同时经过动态代理返回到调用处。
至此,整个流程已经解析完毕,以下是流程图:
sequenceDiagram
InvocationHandler.invoke->>ServiceMethod:loadServiceMethod
ServiceMethod->>HttpServiceMethod:parseAnnotations
HttpServiceMethod->>SuspendForBody:parseAnnotations
SuspendForBody->>HttpServiceMethod:invoke
HttpServiceMethod->>SuspendForBody:adapt
SuspendForBody->>Call:await
Call->>Continuation:suspendCancellableCoroutine
Continuation->>Call:enqueque
Call->>Continuation:onResponse
Continuation->>Call:resume
Call->>SuspendForBody:return
SuspendForBody->>InvocationHandler.invoke:return
思考
- 通过2.5.0版本以及2.8.0版本的源码对比(2.6.0以后的版本支持协程调用),发现主要的改动是将HTTPServiceMethod从final类变成抽象类,并把旧版本的实现放到了CallAdapted中。CallAdapted、SuspendForBody、SuspendForResponse三者都继承于HttpServiceMethod,保证了对于上层的一致性,并具有不错的拓展性(有新的实现则新添加一个类继承于HttpServiceMethod就可以了,体现了开闭原则)。
- 既然在SuspendForBody中调用Kotlin的扩展方法这么别扭,为什么还要这样做呢?那是因为除了直接将接口中的方法声明为挂起这种方式实现协程调用,Retrofit还提供另一种方式实现:直接调用Call的挂起扩展方法await。这个await也就是在SuspendForBody中调用的那个await。提供这样的实现方法有什么好处呢?好处就是让已经在使用中的项目更好地迁移到协程调用。直接使用Call.await可以在无需更改接口的方法声明的前提下也能够使用协程。试想下,假如一个大型项目, 用上百个网络请求接口,光是将接口改成挂起调用就是一个费事费力的事情了。不得不说,Retrofit还是很周到的。
最后
本文大概摸清了Retrofit是如何支持协程调用的,并且在阅读源码的过程中,加深了对Retrofit整体流程的印象。但还有一些东西是没有弄清楚了,比如为什么需要区分Response和非Response。这些疑问都需要慢慢去细读。通过与旧源码对比,能够更好地猜想作者为什么要这么改,好处是什么,间接培养自己的设计意识。如果有不对的地方希望朋友们不吝赐教。