Retrofit第二章-kotlin协程的支持

2,776 阅读9分钟

在第一章中,我们分析了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代码,如下

image-1.png

public interface WanAndroidService {
   @GET("banner/json")
   @Nullable
   Object banner(@NotNull Continuation var1);
}

如果还不够直观的话,我们可以在Java代码中调用banner方法,看看Kotlin协程在Java中是怎么展现的,如下

image-2.png

这里就不多讲了。

既然是有最后一个参数的,并且代码5判断这个参数是Continuation.class,那么isKotlinSuspendFunction就为true了。

所以判断是否为suspend方法的条件有三个

  1. 这个参数没有注解
  2. 是最后一个参数
  3. 这个参数是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差不多,感兴趣可以自行查看

继续看第三章-搭配kotlin协程更方便的自定义CallAdapter