深入 Android Retrofit 网络层全链路:从 JDK 动态代理到 CallAdapter 协程桥接的声明式 HTTP 架构解析

在排查一次线上内存泄漏时,我盯着一个 ApiService 接口看了很久——明明只写了 @GET 注解和抽象方法,运行时却返回了真实的用户对象。接口没有实现类,Retrofit 到底在背后做了什么?

答案藏在一行 Proxy.newProxyInstance 里。Retrofit 的核心不是注解,而是把「声明」翻译成「可执行请求」的一条流水线。这条链路分四段:动态代理拦截、注解解析、OkHttp 桥接、协程适配。拆开看,所谓「网络框架」不过是把 JDK 反射、OkHttp 和 Kotlin 协程这三样东西缝在了一起。

动态代理:接口的隐形实现

Retrofit 的入口是 Retrofit.create(Class)。它没有为 ApiService 生成实现类,而是借「JDK 动态代理(Dynamic Proxy)」在运行时造出一个代理对象。核心逻辑如下:

public <T> T create(final Class<T> service) {
  return (T) Proxy.newProxyInstance(service.getClassLoader(),
      new Class<?>[] { service },
      new InvocationHandler() {
        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          // 方法来自 Object(equals/hashCode/toString)时,直接调用 method.invoke(this, args),
          // 让它在 InvocationHandler 实例自己上执行,而不是转发到代理对象
          if (method.getDeclaringClass() == Object.class) {
            return method.invoke(this, args);
          }
          return loadServiceMethod(service, method).invoke(proxy, args);
        }
      });
}

这里有个容易误解的细节:method.invoke(this, args) 的第一个参数是 this(当前 InvocationHandler 实例),不是 proxy。因为 equals/hashCode/toString 这三个方法在 InvocationHandler 自己的类上就有默认实现(继承自 匿名类的 Object),所以直接在 this 上执行就能得到合理结果,而不需要再转回到代理对象本身(那样反而会因为方法未在接口上声明而报错)。真正的接口方法调用才会进入 loadServiceMethod(service, method).invoke(proxy, args)。

代理对象接到任意方法调用,都会被 InvocationHandler 拦截,这里分两种情况:equals、hashCode、toString 这类 Object 方法在 handler 自己实例上执行,真正的接口方法交给 loadServiceMethod。

loadServiceMethod 内部用 ConcurrentHashMap 缓存结果。每个接口方法只解析一次,后续调用直接命中缓存。这步不能省:注解解析要遍历方法上的全部注解,属于耗时操作,缓存能避免每次请求重复解析。

注解解析:把方法翻译成请求模板

loadServiceMethod 返回 ServiceMethod,真正干活的是 HttpServiceMethod。它持有 RequestFactory、CallAdapter 和响应转换器,RequestFactory 负责把注解翻译成 HTTP 请求的元数据。

for (Annotation annotation : method.getAnnotations()) {
  parseMethodAnnotation(annotation); // 解析 @GET、@POST、@Headers
}
int parameterCount = parameterAnnotationsArray.length;
// 再逐个解析 @Path、@Query、@Body 等参数注解

RequestFactory 解析出的不是真实请求,而是一张「模板」:相对路径 users/{id}、HTTP 方法、请求头,以及每个参数的处理方式。比如 @Path("id") 记录的是「运行时从这里取值,替换 URL 占位符」。

有个细节容易踩:@Query 支持 Map 和 Iterable,@Body 支持任意对象。解析器按参数类型决定是拼接查询串还是走序列化。这套「按类型分派」的逻辑写死在 RequestFactory 里,所以参数类型报错时堆栈通常指向这一层。

模板解析完成后,HttpServiceMethod.invoke 才开始构造请求:

@Override
final ReturnT invoke(Object[] args) {
  Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
  return adapt(call, args);
}

OkHttpCall 把模板、实参、OkHttp 的 Call.Factory 和响应转换器打包在一起,但此时还没发出网络请求。决定最终返回类型的,是接下来的 adapt。

OkHttp 桥接:Call 的懒执行语义

OkHttpCall 是 Retrofit 与 OkHttp 之间的桥。它实现 Retrofit 自己的 Call<T> 接口,内部持有一个懒加载的 okhttp3.Call。

@Override
public void enqueue(Callback<T> callback) {
  okhttp3.Call call;
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already executed.");
    executed = true;
    call = getRawCall();
  }
  call.enqueue(new okhttp3.Callback() {
    @Override public void onResponse(okhttp3.Call call, okhttp3.Response raw) {
      callback.onResponse(OkHttpCall.this, parseResponse(raw));
    }
    @Override public void onFailure(okhttp3.Call call, IOException e) {
      callback.onFailure(OkHttpCall.this, e);
    }
  });
}

关键在 getRawCall:它调用 RequestFactory.create(args),把模板和真实参数拼成 okhttp3.Request,再交给 Call.Factory.newCall。所以 OkHttp 的 Call 在真正发起请求那一刻才创建,这就是「懒执行」。

executed 标志保证 Call 只能执行一次。我踩过这个坑:同一个 Call 对象在两个协程里各 await 一次,第二次直接抛 IllegalStateException。Retrofit 的 Call 是单次消费的,要复用只能重新调接口方法拿新 Call。

CallAdapter:从 Call 到协程的桥

adapt 是 Retrofit 扩展性最强的设计。CallAdapter<ResponseT, ReturnT> 只做一件事:把 Call<ResponseT> 转成任意返回类型 ReturnT,这是一个用户可扩展的接口——RxJava、Guava 的适配都是靠实现 CallAdapter.Factory 接入的。默认情况下(没有额外适配器),方法直接返回 Call<T> 本身。

Kotlin suspend 函数的支持走的是另一条路,不是通过用户可扩展的 CallAdapter 完成,而是 Retrofit 内置在 HttpServiceMethod 里的特殊处理路径。解析注解时,RequestFactory 会检测方法的最后一个参数类型是不是 kotlin.coroutines.Continuation(这就是 isKotlinSuspendFunction 标志的来源),一旦命中,HttpServiceMethod.parseAnnotations 就不再走常规的 CallAdapted 分支,而是构造 SuspendForBody 或 SuspendForResponse(按返回值是不是 Response<T> 区分)这两个内部类。它们仍然会向 Retrofit.callAdapter 要一个 CallAdapter<ResponseT, Call<ResponseT>>——默认情况下这个适配器只是把 Call 原样传回——但真正的挂起语义是在拿到这个 Call 之后,由 KotlinExtensions.await/awaitResponse 完成的,和用户自定义 CallAdapter.Factory 的扩展机制是两条不同的路径:CallAdapter 面向「Call 转其他返回类型」这种通用扩展点,suspend 支持是编译期就能识别、写死在库内部的特判逻辑。

KotlinExtensions.await 的核心逻辑等价于:

suspend fun <T : Any> Call<T>.await(): T = suspendCancellableCoroutine { continuation ->
  continuation.invokeOnCancellation { cancel() }
  enqueue(object : Callback<T> {
    override fun onResponse(call: Call<T>, response: Response<T>) {
      if (response.isSuccessful) {
        continuation.resume(response.body()!!)
      } else {
        continuation.resumeWithException(HttpException(response))
      }
    }
    override fun onFailure(call: Call<T>, t: Throwable) {
      continuation.resumeWithException(t)
    }
  })
}

这段代码解释了两个行为。取消协程会联动取消底层 OkHttp 请求,靠的是 invokeOnCancellation { cancel() };非 2xx 响应抛 HttpException 而不是返回 null,这一点新手在错误处理时经常漏掉。

CallAdapter 让 Retrofit 在不改核心链路的前提下接上了 RxJava、Guava、Kotlin 协程。我更倾向协程版本:取消语义清晰,也不用为每个接口手写 disposable 管理。

Converter:序列化与反序列化的边界

链路最后一段是 Converter,分两个方向:请求方向把 @Body 对象序列化成 RequestBody,响应方向把 ResponseBody 反序列化成对象。

public interface Converter<F, T> {
  T convert(F value) throws IOException;
}

Gson 的响应转换器很直白:拿到 ResponseBody 的字符流,交给 Gson.fromJson。请求方向同理,用 toJson 生成 RequestBody。

Retrofit 会为每个 Call 预先绑定好 responseConverter,转换发生在 OkHttpCall.parseResponse 里,不在协程层。所以后端返回不符合预期结构的 JSON 时,异常在 IO 线程抛出,协程里用 try/catch 能正常捕获。

返回类型写成 Call<ResponseBody> 时,Retrofit 跳过反序列化,把原始流直接给你。这种写法适合下载文件或流式响应,忘了 close() 就会泄漏连接。

实践建议

排查 Retrofit 问题,顺着这条链路定位最快:

  1. 参数或 URL 拼错,看 RequestFactory 的解析结果,打断点在 create 方法上。
  2. 返回类型不对或异常类型怪异,看 CallAdapter 分支,确认走的是协程 await 还是自定义适配器。
  3. 反序列化崩溃,看 Converter,确认返回类型与实际 JSON 结构是否匹配,别把 @Body 对象和响应对象搞混。

摸清这条链路,Retrofit 就不再是黑盒。声明式 HTTP 的本质,是用动态代理把接口声明变成请求模板,再用 CallAdapter 和 Converter 把「请求-响应」这条横切流程解耦成可替换的插件。