深入 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 问题,顺着这条链路定位最快:
- 参数或 URL 拼错,看
RequestFactory的解析结果,打断点在create方法上。 - 返回类型不对或异常类型怪异,看
CallAdapter分支,确认走的是协程await还是自定义适配器。 - 反序列化崩溃,看
Converter,确认返回类型与实际 JSON 结构是否匹配,别把@Body对象和响应对象搞混。
摸清这条链路,Retrofit 就不再是黑盒。声明式 HTTP 的本质,是用动态代理把接口声明变成请求模板,再用 CallAdapter 和 Converter 把「请求-响应」这条横切流程解耦成可替换的插件。