在第一章中,我们分析了Retrofit的常规用法,并分析了它的原理,点击这里查看第一章。
不过自从有了kotlin协程之后,以往的回调写法似乎显得有点笨重了。利用kotlin协程,我们可以很方便地完成子线程和主线程的切换,当我看到Retrofit也支持了kotlin协程的时候,我有点疑问,难道之前Retrofit不支持kotlin协程,无法使用kotlin协程吗?究竟是个啥意思,我们一探究竟。
一个例子了解kotlin协程
协程这个概念网络上可以搜到很多,不光是kotlin有协程,其他语言也有协程,不过我们Android开发需要了解的也就是kotlin协程,所以接下来如果说协程就是指的kotlin协程了,我们先来了解一下kotlin协程的写法。在Android平台,我们可以这样写
//Activity中
//开启协程
lifecycleScope.launch{
val result = withContext(Dispatchers.IO) {
//子线程运行
}
//主线程运行
tvResult.setText(result.....)
}
这里的lifecycleScope可以理解为Lifecycle对协程的延伸,用来绑定Activity或者Fragment的生命周期,销毁时自动取消,withContext(Dispatchers.IO)也在上方有注释,可以理解为切线程,后续神奇的就是tvResult.setText()自动切回到主线程执行,既然如此,我们可以与Retrofit结合使用了。
//这里定义的接口,我们很熟悉,感谢鸿洋大神提供的接口
interface WanAndroidService {
@GET("banner/json")
fun banner(): Call<Banner>
}
//进行Retrofit初始化
val retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service: WanAndroidService = retrofit.create(WanAndroidService::class.java)
//接下来,我们可以这样做
lifecycleScope.launch{
val result = withContext(Dispatchers.IO) {
//代码1
service.banner().execute()
}
//主线程运行
tvResult.setText(result.body().toString())
}
看代码1就完成了在子线程请求网络的需求,并且也很简洁,这Retrofit不就支持协程了吗?确实如此,但不够简洁。那Retrofit对协程的支持是什么样子的呢?再往下看
interface WanAndroidService {
@GET("banner/json")
//更改方法,加上suspend关键字,并且将Call<Banner>直接改为Banner
suspend fun banner(): Banner
}
//进行Retrofit初始化,代码不变.....
lifecycleScope.launch{
//这里直接调用banner方法,不用调用execute(),并且运行在子线程
val banner = service.banner()
//主线程运行
tvResult.setText(result.body().toString())
}
注意改动,suspend是协程的关键字,标记这个方法是个挂起方法,不用太在意,然后我们经常使用的Call也没有了,withContext(Dispatchers.IO)也不需要调用了,很神奇是吧。接下来就看看Retrofit是怎么做到的。
原理解析
第一章Retrofit原理分析的那篇文章讲过动态代理,以及Retrofit的create()方法,我们就接着之前的入口往下看。
步骤1:初始化
Retrofit#create() -> Retrofit#loadServiceMethod()都没有什么变化。
步骤2:ServiceMethod#parseAnnotations()
loadServiceMethod()中调用的是ServiceMethod#parseAnnotations()方法,我们来看下
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
//代码1
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
...校验逻辑
//代码2
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}
代码1,parseAnnotations()通过Builder模式构建了一个RequestFactory实例,RequestFactory封装了所有的请求参数,不过如果有协程的话,还包含了一个变量。我们来看下
步骤3:RequestFactory#build()
RequestFactory build() {
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
parameterHandlers[p] =
//代码1
parseParameter(p, parameterTypes[p], parameterAnnotationsArray[p], p == lastParameter);
}
...
return new RequestFactory(this);
}
其中标注的代码1,调用了parseParameter()方法,我们进入看一下
步骤4:RequestFactory.Builder#parseParameter()
private @Nullable ParameterHandler<?> parseParameter(
int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
ParameterHandler<?> result = null;
if (annotations != null) {
for (Annotation annotation : annotations) {
//代码1
ParameterHandler<?> annotationAction =
parseParameterAnnotation(p, parameterType, annotations, annotation);
if (annotationAction == null) {
continue;
}
...
//代码2
result = annotationAction;
}
}
//代码3
if (result == null) {
//代码4
if (allowContinuation) {
try {
//代码5
if (Utils.getRawType(parameterType) == Continuation.class) {
//代码6
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
}
return result;
}
这一步骤需要解释的代码有点多,因为涉及到代码6的isKotlinSuspendFunction的值,这个值决定了这个方法是否为suspend方法,涉及到对协程的支持。
从代码1来看,对annotations遍历,然后调用parseParameterAnnotation()进行解析,这里主要是解析@Query,@Path等注解,就不展开了。我们例子中是没有参数注解的,先不考虑,后续再看有注解的情况。
既然没有annotations,那么result结果是null,代码3的判断是true。
那么代码4,allowContinuation是什么呢?从步骤3可以看到这个allowContinuation就是判断是否为最后一个参数,这里按道理来说,我们示例中没有参数,那么这里还能为true吗?
其实kotlin的suspend函数看似是这样的
suspend fun banner(): Banner
而它实际上是这样的
fun banner(c: Continuation<? super Banner>): Banner
带有suspend关键字的方法会自动添加一个Continuation参数,这里涉及到kotlin协程的原理。不过我们可以简单验证一下。
来到WanAndroidService.kt文件,点击AndroidStudio的Tools工具栏,选择Kotlin,点击Show Kotlin Bytecode,再点击Decompile按钮就可以转换成Java代码,如下
public interface WanAndroidService {
@GET("banner/json")
@Nullable
Object banner(@NotNull Continuation var1);
}
如果还不够直观的话,我们可以在Java代码中调用banner方法,看看Kotlin协程在Java中是怎么展现的,如下
这里就不多讲了。
既然是有最后一个参数的,并且代码5判断这个参数是Continuation.class,那么isKotlinSuspendFunction就为true了。
所以判断是否为suspend方法的条件有三个
- 这个参数没有注解
- 是最后一个参数
- 这个参数是Continuation.class
那如果有其他参数注解会有影响吗,比如@Query?
自然是没有影响的,在Continuation之前的参数无法将isKotlinSuspendFunction置为true,但是最后一个参数一定能。
步骤5:步骤2的代码2 - HttpServiceMethod#parseAnnotations()
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
if (isKotlinSuspendFunction) {
...判断逻辑省略
//代码1
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
//代码2
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
}
//代码3
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
//代码4
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
//代码5
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
这一步骤是关键步骤,代码1做了一个转换,可以理解成将我们自定义的Banner封装成了Call,作为新的adapterType。
代码2也是一个转换,如果我们的annotations中没有SkipCallbackExecutorImpl注解,那就添加这个注解。
代码3,这是根据adapterType匹配CallAdapter的代码,Retrofit自带的是DefaultCallAdapterFactory,那这里有没有为协程再创建一个CallAdapterFactory呢?
答案是:没有。这里也是调用DefaultCallAdapterFactory#get()方法,返回匿名内部类CallAdapter。
代码4,更不用说了,这里还是使用GsonResponseBodyConverter。
代码5,跟之前不一样了,这里构建了SuspendForBody实例,并且将CallAdapter传入了。
最终的返回对象是SuspendForBody,那我们再回到步骤1得知,loadServiceMethod()方法最终返回结果就是这个SuspendForBody,然后再调用SuspendForBody#invoke()方法就大功告成了。同样,这里的invoke()方法是父类HttpServiceMethod的方法。
上一篇文章讲Retrofit原理的时候我们就已经了解到HttpServiceMethod#invoke()构建了一个OkHttpCall对象再调用子类的adapt()方法,那我们看下SuspendForBody
步骤6:SuspendForBody
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
private final boolean isNullable;
SuspendForBody(....);
this.callAdapter = callAdapter;
this.isNullable = isNullable;
}
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
//代码1
call = callAdapter.adapt(call);
//代码2
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
//代码3
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
构造方法不用说了,就是一些赋值操作。adapt()方法需要说明一下
代码1,调用callAdapter.adapt(call),这个callAdapter之前说过是DefaultCallAdapterFactory的get()方法中返回的CallAdapter对象,来看一下
DefaultCallAdapterFactory#get()
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
....
//标记1
final Executor executor =
Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
? null
: callbackExecutor;
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
//标记2
return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
}
};
}
其中标记2,我们需要看一下,在之前分析Retrofit支持非协程方法中我们知道executor是null,最终返回是ExecutorCallbackCall,那对协程支持后有没有变化呢?
给出答案:变化了。
在标记1中的代码Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class),这个结果返回的是true,也就是说明注解中包含了SkipCallbackExecutor,从而使executor为null,最终在标记2代码返回了call对象,也就是OkHttpCall。
那这个SkipCallbackExecutor是什么时候被添加进去的呢?
具体看步骤5的代码2,在annotations中添加了SkipCallbackExecutorImpl。
所以代码1拿到的call是OkHttpCall。
我们再来看代码2,从args中取出Continuation对象,这个args是从invoke()方法传入的,表示的是我们定义的
suspend fun banner(): Banner
后又被转换成
fun banner(c: Continuation<? super Banner>): Banner
的方法参数c,也就是Continuation<? super Banner>对象。
最终调用代码3,将call, continuation传给KotlinExtensions#await()方法,我们来看下KotlinExtensions#await()
步骤7:KotlinExtensions#await()
suspend fun <T : Any> Call<T>.await(): T {
//代码1
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
//代码2
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
....
//代码3
continuation.resumeWithException(e)
} else {
//代码4
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
//代码5
continuation.resumeWithException(t)
}
})
}
}
这个KotlinExtensions实际上不是类名,而是文件名,KotlinExtensions也是一个文件,这个await()方法也是直接写在文件中的,这是kotlin中的顶层函数的写法。
另外Call.await(),这个函数的定义也和Java不一样,这表示await()是Call的扩展函数,如果我们在kotlin中调用可以直接写成 call.await(),所谓扩展就是在Call.class的内部加上一个自定义的方法,而不改变Call.class本身的结构。
当然,这个顶层函数+扩展方法的写法在Java中是无法像在kotlin中使用那么方便的。所以在Java中调用这个方法时要写成这样
KotlinExtensions.await(call, continuation)
另外我们看看这个await()方法前面也加上了suspend修饰符,说明它是个挂起方法,而真正的挂起逻辑在代码1,suspendCancellableCoroutine,这是kotlin写成自带的开启协程的方法,由方法名可知,这是可以取消的。
接下来再看代码2,直接在协程中调用enqueue()方法,这个enqueue是传入的OkHttpCall#enqueue()。
最终,在onResponse()和onFailure()中,我们看到了代码3,4,5,它们是协程的恢复方法。协程有挂起,就会有恢复,由于我们定义的Banner是不可空的,那么这里会走代码4,如果产生了body为空或者异常,就会走代码3和5
不同的是虽然我们没有在方法上声明异常信息,但这里还是会抛出异常,异常机制在kotlin中也与Java不同,最终,网络请求结束。
注意点1:线程切换
什么时候切到子线程请求网络?
代码2,OkHttpCall#enqueue()方法调用时,内部调用okhttp中的Call切换到子线程运行,这是okHttp机制,自然而然onResponse和代码4 continuation.resume(body)都是在子线程。
又是什么时候切回主线程的呢?
continuation.resume(body)后子协程运行结束,切换回父协程所在的主线程运行。
协程会切线程吗?为什么?
协程并不会自动切换线程,因为它并不知道往哪切。suspendCancellableCoroutine启动的协程还是运行在主线程,因为我们没有指定它切换线程,所以它就不知道往哪切,自然运行在当前线程也就是主线程。我们一开始在例子中使用到的
val result = withContext(Dispatchers.IO) {
//子线程运行
}
withContext()如果不指定Dispatchers.IO,同样也不知道往哪里切,道理是一样的。可以简单理解为协程只知道恢复到哪里去,但并不知道要往哪里切线程。
注意点2:suspend方法
kotlin协程规定,suspend方法只能在协程或者另外一个suspend方法中调用,所以KotlinExtensions#await()必须是suspend修饰,因为suspendCancellableCoroutine也是一个suspend方法。
至此,Retrofit对协程的支持已经分析完毕了
另外提一嘴
在看源码时发现还有另外一种写法
@GET("banner/json")
suspend fun banner(): Response<Banner>
就是在Banner外再加上一个Response,用来接收全部的Response信息,此时Banner则作为body存在,对应的源码为SuspendForResponse,与SuspendForBody差不多,感兴趣可以自行查看