Android随笔-Retrofit

一、定位

Retrofit 是一个类型安全的 HTTP 客户端,本质是对 OkHttp 的"声明式上层封装 ":你用接口 + 注解描述请求,它在运行时用动态代理 生成接口实现,把方法调用翻译成 OkHttp 请求,再通过 CallAdapter 决定返回类型、Converter 负责序列化/反序列化。

复制代码
Retrofit = 动态代理(Proxy) + 注解解析(RequestFactory) + 调用适配(CallAdapter) + 数据转换(Converter) + 底层执行(OkHttp Call)

二、基本用法

kotlin 复制代码
// 1. 定义接口
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Int): User

    @POST("users")
    suspend fun createUser(@Body user: User): Response<User>
}

// 2. 构建 Retrofit
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")   // 必须以 / 结尾
    .client(okHttpClient)                   // 底层 OkHttp
    .addConverterFactory(GsonConverterFactory.create())
    .build()

// 3. 创建代理实例并调用
val api = retrofit.create(ApiService::class.java)
val user = api.getUser(1)  // 一行代码背后是下面整条链路

三、核心原理逐层拆解

3.1 动态代理:create() 发生了什么

java 复制代码
// Retrofit#create() 源码核心(简化)
public <T> T create(final Class<T> service) {
    validateServiceInterface(service);  // 校验必须是接口、不能继承其他接口
    return (T) Proxy.newProxyInstance(
        service.getClassLoader(),
        new Class<?>[] { service },
        new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) {
                if (method.getDeclaringClass() == Object.class) {
                    return method.invoke(this, args);  // toString/equals 等直接执行
                }
                // 核心:加载(或从缓存取)该方法的 ServiceMethod,执行
                return loadServiceMethod(method).invoke(args);
            }
        });
}

关键点:

  • 为什么用动态代理:接口没有实现类,JDK 动态代理在运行时生成实现,所有方法调用统一收编到 InvocationHandler.invoke()------这里就是把"方法调用"转成"HTTP 请求"的总入口。
  • ServiceMethod 缓存 :loadServiceMethod() 内部是一个 ConcurrentHashMap<Method, ServiceMethod>。反射解析注解的性能开销只发生在每个方法第一次调用时,之后命中缓存。所以 Retrofit 的反射不是性能问题。
  • create() 本身很便宜,可以全局单例 Retrofit、按需 create 多个 Service。

3.2 注解解析:ServiceMethod 与 RequestFactory

每个接口方法最终被解析成一个 ServiceMethod,它封装了一次请求的全部信息:

复制代码
ServiceMethod.parseAnnotations(retrofit, method)
    │
    ├── RequestFactory.parseAnnotations()   → 解析出"请求长什么样"
    │       ├── 方法注解:@GET/@POST/@HTTP → httpMethod + relativeUrl
    │       └── 参数注解:逐个解析成 ParameterHandler
    │           (@Path→替换URL占位符 / @Query→拼查询串 / @Body→RequestBody / @Header...)
    │
    ├── createCallAdapter()   → 决定"返回类型怎么适配"
    └── createResponseConverter() → 决定"响应体怎么反序列化"

RequestFactory.Builder 解析出的关键要素 :HTTP 方法、相对路径、Headers、ParameterHandler\[\] 数组(每个参数对应一个处理器,负责把自己写进 RequestBuilder)。这一步的产物是一份与执行无关的请求模板,线程安全、可复用。

3.3 返回类型分发:HttpServiceMethod 的三态

HttpServiceMethod.parseAnnotations() 会根据方法签名走三条分支:

方法签名 分支类 行为
fun get(): Call<User> CallAdapted 交给 CallAdapter 适配(默认返回 Call,或 RxJava 的 Observable 等)
suspend fun get(): Response<User> SuspendForResponse 挂起执行,恢复时给完整 Response
suspend fun get(): User SuspendForBody 挂起执行,恢复时只给 body,非 2xx 抛 HttpException

判断 suspend 的方式:method.getParameterTypes() 最后一个参数是 Continuation 类型(Kotlin suspend 函数编译后会多一个续体参数)------这是 Retrofit 支持协程的入口识别点

3.4 底层执行:OkHttpCall

retrofit2.Call 的默认实现是 OkHttpCall,它是 okhttp3.Call 的装饰器:

复制代码
OkHttpCall.enqueue(callback)
    └── callFactory.newCall(requestFactory.create(args))   // 用模板 + 实参构建真实 OkHttp 请求
        └── okhttp3.Call.enqueue(okhttp Callback)
            └── onResponse → parseResponse(rawResponse)
                    ├── code 2xx → Response.success(converter.convert(body))
                    └── 非 2xx → 缓冲 errorBody → Response.error(...)

线程模型:

  • execute():同步,在调用线程执行,Android 主线程调用会崩(NetworkOnMainThreadException)
  • enqueue():异步,网络请求跑在 OkHttp Dispatcher 的线程池 ;回调默认经过 callbackExecutor 切回主线程 ------Android 上 Retrofit 通过 Platform 检测到 Android 环境,注入 MainThreadExecutor(内部 Handler.post)。所以 Retrofit 的 onResponse 默认在主线程,这就是为什么老代码可以直接在回调里更新 UI

3.5 suspend 支持的本质

suspend fun getUser(): User 最终走到 KotlinExtensions.await(),核心代码:

kotlin 复制代码
suspend fun <T> Call<T>.await(): T {
    return suspendCancellableCoroutine { continuation ->
        // 协程取消 → 取消 OkHttp 请求
        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)          // 失败:带异常恢复
            }
        })
    }
}

一句话答案:Retrofit 对 suspend 的支持 = suspendCancellableCoroutine 把回调式 enqueue 桥接成挂起函数 ,请求仍在 OkHttp 的 IO 线程池执行,协程挂起不阻塞线程,取消协程会联动 call.cancel() 断掉 HTTP 连接。

3.6 两大扩展点(策略模式)

CallAdapter.Factory ------ 决定方法返回类型:

复制代码
工厂按添加顺序遍历,get(returnType, ...) 返回第一个非 null 的适配器
├── DefaultCallAdapterFactory(内置兜底):支持 Call<T>,包一层 ExecutorCallbackCall 切主线程
├── RxJava2CallAdapterFactory:Observable/Single/Completable
└── 自定义:比如返回 LiveData<T>、Result<T>

Converter.Factory ------ 负责数据转换,三个层级:

方法 用途
responseBodyConverter ResponseBody → Java/Kotlin 对象(Gson/Moshi/kotlinx.serialization)
requestBodyConverter 对象 → RequestBody(@Body 参数序列化)
stringConverter 对象 → String(@Path/@Query/@Header 参数转字符串,内置 EnumConverter 等)

同样是工厂按注册顺序遍历,先到先得------所以内置的 BuiltInConverters 在最后,你要自定义解析(比如加密响应)就把自己的 Factory 加在 Gson 前面。

四、注解速查表

分类 注解 说明
HTTP 方法 @GET @POST @PUT @DELETE @PATCH @HEAD @OPTIONS 括号内是相对路径
自定义方法 @HTTP(method="...", path="...", hasBody=...) 少见方法用
标记 @FormUrlEncoded 表单提交,配合 @Field/@FieldMap
标记 @Multipart 文件/多部分上传,配合 @Part/@PartMap
标记 @Streaming 大文件下载,不一次性读入内存,流式写盘
参数 @Path("id") 替换 URL 中 {id} 占位符
参数 @Query("page") / @QueryMap 拼接查询参数
参数 @Url 动态完整 URL,会覆盖 baseUrl 拼接
参数 @Body 对象序列化为请求体
参数 @Header("Authorization") / @Headers 动态/静态请求头
参数 @Tag 给请求打标,拦截器里 request.tag() 取出做差异化处理

baseUrl 拼接规则:baseUrl 必须以 / 结尾;接口路径以 / 开头表示域名根路径绝对定位,不以 / 开头则相对 baseUrl 拼接。

五、实战标准配置

kotlin 复制代码
val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    // 日志拦截器(release 包记得关掉或降级为 BASIC/NONE)
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = if (BuildConfig.DEBUG) BODY else NONE
    })
    // Token 注入拦截器(应用拦截器,能看到最终请求)
    .addInterceptor { chain ->
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${TokenManager.get()}")
            .build()
        chain.proceed(request)
    }
    // Token 过期自动刷新重试(Authenticator,只在 401 时触发)
    .authenticator { route, response ->
        val newToken = runBlocking { TokenManager.refresh() }
        response.request.newBuilder()
            .header("Authorization", "Bearer $newToken")
            .build()
    }
    .build()

统一错误封装(现代写法):

kotlin 复制代码
sealed interface ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>
    data class Error(val code: Int, val message: String?) : ApiResult<Nothing>
    data class Exception(val throwable: Throwable) : ApiResult<Nothing>
}

suspend fun <T> apiCall(block: suspend () -> T): ApiResult<T> = try {
    ApiResult.Success(block())
} catch (e: HttpException) {          // 非 2xx
    ApiResult.Error(e.code(), e.message())
} catch (e: IOException) {            // 网络异常
    ApiResult.Exception(e)
}

六、工作流程

以这行代码为起点,拆解它背后发生的全部事情:

kotlin 复制代码
val user = api.getUser(1)   // suspend fun getUser(@Path("id") id: Int): User

阶段 0:初始化(App 启动时,只做一次)

复制代码
Retrofit.Builder()
    .baseUrl(...)                    → 记录基础 URL(必须 / 结尾)
    .client(okHttpClient)            → 记录 callFactory(真实执行者)
    .addConverterFactory(...)        → 装入 Converter 工厂列表
    .addCallAdapterFactory(...)      → 装入 CallAdapter 工厂列表
    .build()                         → 检测平台(Android → 注入 MainThreadExecutor)
                                     → 生成 Retrofit 实例(全局单例)

此阶段只存配置,不解析任何接口、不做任何网络操作

阶段 1:创建代理(retrofit.create())

复制代码
retrofit.create(ApiService::class.java)
    │
    ├─ 校验:必须是接口、不能继承其他接口、不能有类型参数
    │
    └─ Proxy.newProxyInstance(classLoader, [ApiService], invocationHandler)
         → 运行时动态生成 ApiService 的实现类(代理对象)
         → 该接口的所有方法调用,都会被收编到
           InvocationHandler.invoke(proxy, method, args)

此阶段依然没有任何注解解析和网络操作,代理创建非常便宜

阶段 2:首次调用------方法解析(每个方法只做一次)

复制代码
api.getUser(1)
    │
    └─ InvocationHandler.invoke(method=getUser, args=[1])
         │
         ├─ method 属于 Object?(toString 等)→ 直接执行返回
         │
         └─ loadServiceMethod(method)
              │
              ├─ 查缓存 ConcurrentHashMap<Method, ServiceMethod>
              │    ├─ 命中 → 直接返回(以后每次调用都走这里,零反射)
              │    └─ 未命中 → 解析(仅此一次)↓
              │
              └─ ServiceMethod.parseAnnotations(retrofit, method)
                   │
                   ├─ ① RequestFactory.parseAnnotations()
                   │     解析方法注解:@GET → httpMethod="GET", relativeUrl="users/{id}"
                   │     解析参数注解:@Path("id") → ParameterHandler.Path
                   │     产物:与实参无关的"请求模板"(线程安全、可复用)
                   │
                   ├─ ② 识别方法签名 → 确定执行分支
                   │     最后一个参数是 Continuation?→ suspend 分支
                   │     (SuspendForResponse / SuspendForBody)
                   │     否则 → CallAdapted 分支
                   │
                   ├─ ③ createCallAdapter()
                   │     遍历 CallAdapter.Factory 列表,第一个匹配的胜出
                   │
                   └─ ④ createResponseConverter()
                         遍历 Converter.Factory 列表,找到 User 类型的反序列化器

反射开销全部集中在这里,且每个方法只发生一次------这是"Retrofit 用反射为什么不怕性能问题"的标准答案。

阶段 3:构建真实请求(每次调用都发生)

复制代码
ServiceMethod.invoke(args=[1])
    │
    └─ RequestFactory.create(args)
         │
         ├─ new RequestBuilder(以解析好的模板为底)
         ├─ 遍历 ParameterHandler[] 数组,把实参"写"进请求:
         │     @Path  → "users/{id}" 中的 {id} 替换为 "1" → "users/1"
         │     @Query → 拼查询串   @Body → Converter 序列化为 RequestBody
         │     @Header→ 加请求头
         ├─ baseUrl + relativeUrl 拼接出完整 URL
         └─ 生成 okhttp3.Request

阶段 4:交给 OkHttp 执行

复制代码
OkHttpCall(装饰 okhttp3.Call)
    │
    ├─ suspend/异步路线:call.enqueue(okhttp Callback)
    │      → OkHttp Dispatcher 线程池调度
    │      → 拦截器链:应用拦截器 → RetryAndFollowUp → Bridge
    │        → Cache → Connect(连接池复用/TLS)→ CallServer
    │      → 真正发出 HTTP 请求,等待响应
    │
    └─ 同步路线:call.execute()(调用线程直接执行,主线程禁用)

Retrofit 自己到此为止------网络 IO 全是 OkHttp 的事。

阶段 5:响应处理与交付(分两条支线)

复制代码
onResponse(rawResponse)
    │
    └─ parseResponse(rawResponse)
         ├─ code 2xx → Response.success(converter.convert(body))
         │              (Gson/Moshi:ResponseBody → User 对象)
         └─ 非 2xx  → 缓冲 errorBody → Response.error(...)
支线 A:Call
复制代码
ExecutorCallbackCall(装饰器)
    → callbackExecutor.execute { callback.onResponse(...) }
    → Handler.post 切回主线程 → 你在回调里直接更新 UI
支线 B:suspend 路线(现代主流)
复制代码
suspendCancellableCoroutine
    ├─ 成功 → continuation.resume(user)        协程在调用处恢复,拿到 User
    ├─ 失败 → resumeWithException(...)          走 catch
    └─ 协程被取消 → call.cancel()               联动断开 HTTP 连接

七、 整体流程概览

复制代码
你的代码 api.getUser(1)
   │
   ▼
动态代理 InvocationHandler.invoke          ← create() 时生成
   │
   ▼
loadServiceMethod(ConcurrentHashMap 缓存)  ← 首次反射解析注解,之后零反射
   │
   ▼
RequestFactory + 实参 → okhttp3.Request     ← ParameterHandler 逐个写入
   │
   ▼
OkHttpCall → OkHttp 拦截器链 → 服务器       ← 真正的网络 IO(IO 线程池)
   │
   ▼
parseResponse → Converter.convert           ← JSON → 对象
   │
   ▼
交付:Call 回调切主线程 / suspend 恢复协程    ← CallAdapter 决定的形态

Retrofit 用动态代理把接口方法调用收编到 invoke();首次调用时反射解析注解生成 ServiceMethod 并缓存,其中 RequestFactory 负责拼请求、CallAdapter 决定返回类型、Converter 负责数据转换;之后每次调用用模板+实参构建 OkHttp Request,交给 OkHttp 执行;响应回来后 Converter 反序列化,回调路线经 Executor 切回主线程,suspend 路线通过 suspendCancellableCoroutine 恢复协程。Retrofit 全程不做网络 IO,它是 OkHttp 的声明式封装层。

八、设计模式总结

模式 体现
动态代理 create() 生成接口实现,统一收编方法调用
建造者模式 Retrofit.BuilderRequest.Builder(链式配置复杂对象)
适配器模式 CallAdapter(把 OkHttp Call 适配成 Call/RxJava/suspend 各种返回类型)
策略模式 Converter(序列化策略可插拔:Gson/Moshi/Protobuf)
工厂模式 CallAdapter.Factory、Converter.Factory(按类型遍历匹配)
装饰器模式 OkHttpCall 装饰 okhttp3.Call;ExecutorCallbackCall 装饰回调切线程
外观模式 Retrofit 本身是 OkHttp 复杂能力的简化门面

九、常见问题

  1. Retrofit 的原理?
    ------ 用第一节那个公式回答,然后逐层展开动态代理 → 注解解析 → CallAdapter/Converter → OkHttp 执行。
  2. 反射性能差,Retrofit 为什么敢用?
    ------ 注解解析只在方法首次调用时发生,ServiceMethod 存 ConcurrentHashMap 缓存;代理创建本身只生成字节码,热点在 OkHttp。
  3. suspend 函数是怎么支持的?
    ------ 编译后方法多一个 Continuation 参数被识别 → SuspendForBody/SuspendForResponse 分支 → suspendCancellableCoroutine 桥接 enqueue 回调,取消联动 call.cancel()。
  4. 回调在哪个线程?
    ------ enqueue 回调经 callbackExecutor 切主线程(Android 平台注入MainThreadExecutor);suspend 版本由协程调度器决定,恢复在调用方上下文。
  5. CallAdapter 和 Converter 的区别?
    ------ CallAdapter 管"返回类型"(Call/Observable/suspend body),Converter 管"数据怎么转"(JSON↔对象)。匹配都是工厂顺序遍历、先到先得。
  6. 如何上传文件 / 下载大文件?
    ------ @Multipart + @Part(MultipartBody.Part);@Streaming + ResponseBody.byteStream() 分块写盘(注意别开 Gson converter 转它)。
  7. 如何做 Token 过期自动刷新?
    ------ OkHttp Authenticator(401 触发,同步刷新并重放请求),与应用拦截器的 Token 注入配合。
  8. Retrofit 与 OkHttp 分工?
    ------ Retrofit 管"接口抽象、注解解析、类型适配、数据转换";OkHttp 管"连接池、拦截器链、缓存、HTTP/2、实际收发"。Retrofit 自己不做任何网络 IO。
相关推荐
想取一个与众不同的名字好难1 小时前
安卓自定义颜色选择器
android
Coffeeee2 小时前
ios零基础的Android开发能否靠AI让老板省一笔人工费呢
android·ios·ai编程
Joey_friends2 小时前
指纹authenticate流程图
android·java·c++
郑州光合科技余经理2 小时前
家政O2O平台解析:从0搭建上门预约小程序解决方案
android·java·开发语言·前端·小程序·架构·php
yueqc14 小时前
Android 渲染(三):掉帧监控
android·渲染·apm·掉帧
Mico184 小时前
MySQL 8.0.35 基于GTID 主从复制安装增强半同步复制
android·mysql·adb
Kapaseker4 小时前
你是不是还没用过 select?实战 Kotlin select
android·kotlin
__Witheart__5 小时前
适用于Android内核boot.img生成流程
android·rockchip
木易 士心5 小时前
Jetpack Compose 深度解析:初始组合与重组的底层奥秘
android