Android随笔-Retrofit
一、定位
Retrofit 是一个类型安全的 HTTP 客户端,本质是对 OkHttp 的"声明式上层封装":你用接口 + 注解描述请求,它在运行时用动态代理生成接口实现,把方法调用翻译成 OkHttp 请求,再通过CallAdapter决定返回类型、Converter负责序列化/反序列化。
Retrofit = 动态代理(Proxy) + 注解解析(RequestFactory) + 调用适配(CallAdapter) + 数据转换(Converter) + 底层执行(OkHttp Call)二、基本用法
// 1. 定义接口interfaceApiService{@GET("users/{id}")suspendfungetUser(@Path("id")id:Int):User@POST("users")suspendfuncreateUser(@Bodyuser:User):Response<User>}// 2. 构建 Retrofitvalretrofit=Retrofit.Builder().baseUrl("https://api.example.com/")// 必须以 / 结尾.client(okHttpClient)// 底层 OkHttp.addConverterFactory(GsonConverterFactory.create()).build()// 3. 创建代理实例并调用valapi=retrofit.create(ApiService::class.java)valuser=api.getUser(1)// 一行代码背后是下面整条链路三、核心原理逐层拆解
3.1 动态代理:create() 发生了什么
// Retrofit#create() 源码核心(简化)public<T>Tcreate(finalClass<T>service){validateServiceInterface(service);// 校验必须是接口、不能继承其他接口return(T)Proxy.newProxyInstance(service.getClassLoader(),newClass<?>[]{service},newInvocationHandler(){@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args){if(method.getDeclaringClass()==Object.class){returnmethod.invoke(this,args);// toString/equals 等直接执行}// 核心:加载(或从缓存取)该方法的 ServiceMethod,执行returnloadServiceMethod(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(),核心代码:
suspendfun<T>Call<T>.await():T{returnsuspendCancellableCoroutine{continuation->// 协程取消 → 取消 OkHttp 请求continuation.invokeOnCancellation{cancel()}enqueue(object:Callback<T>{overridefunonResponse(call:Call<T>,response:Response<T>){if(response.isSuccessful){continuation.resume(response.body()!!)// 成功:恢复协程}else{continuation.resumeWithException(HttpException(response))}}overridefunonFailure(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 拼接。
五、实战标准配置
valokHttpClient=OkHttpClient.Builder().connectTimeout(15,TimeUnit.SECONDS).readTimeout(15,TimeUnit.SECONDS)// 日志拦截器(release 包记得关掉或降级为 BASIC/NONE).addInterceptor(HttpLoggingInterceptor().apply{level=if(BuildConfig.DEBUG)BODYelseNONE})// Token 注入拦截器(应用拦截器,能看到最终请求).addInterceptor{chain->valrequest=chain.request().newBuilder().addHeader("Authorization","Bearer${TokenManager.get()}").build()chain.proceed(request)}// Token 过期自动刷新重试(Authenticator,只在 401 时触发).authenticator{route,response->valnewToken=runBlocking{TokenManager.refresh()}response.request.newBuilder().header("Authorization","Bearer$newToken").build()}.build()统一错误封装(现代写法):
sealedinterfaceApiResult<outT>{dataclassSuccess<T>(valdata:T):ApiResult<T>dataclassError(valcode:Int,valmessage:String?):ApiResult<Nothing>dataclassException(valthrowable:Throwable):ApiResult<Nothing>}suspendfun<T>apiCall(block:suspend()->T):ApiResult<T>=try{ApiResult.Success(block())}catch(e:HttpException){// 非 2xxApiResult.Error(e.code(),e.message())}catch(e:IOException){// 网络异常ApiResult.Exception(e)}六、工作流程
以这行代码为起点,拆解它背后发生的全部事情:
valuser=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.Builder、Request.Builder(链式配置复杂对象) |
| 适配器模式 | CallAdapter(把 OkHttp Call 适配成 Call/RxJava/suspend 各种返回类型) |
| 策略模式 | Converter(序列化策略可插拔:Gson/Moshi/Protobuf) |
| 工厂模式 | CallAdapter.Factory、Converter.Factory(按类型遍历匹配) |
| 装饰器模式 | OkHttpCall 装饰 okhttp3.Call;ExecutorCallbackCall 装饰回调切线程 |
| 外观模式 | Retrofit 本身是 OkHttp 复杂能力的简化门面 |
九、常见问题
- Retrofit 的原理?
—— 用第一节那个公式回答,然后逐层展开动态代理 → 注解解析 → CallAdapter/Converter → OkHttp 执行。 - 反射性能差,Retrofit 为什么敢用?
—— 注解解析只在方法首次调用时发生,ServiceMethod 存 ConcurrentHashMap 缓存;代理创建本身只生成字节码,热点在 OkHttp。 - suspend 函数是怎么支持的?
—— 编译后方法多一个 Continuation 参数被识别 → SuspendForBody/SuspendForResponse 分支 → suspendCancellableCoroutine 桥接 enqueue 回调,取消联动 call.cancel()。 - 回调在哪个线程?
—— enqueue 回调经 callbackExecutor 切主线程(Android 平台注入MainThreadExecutor);suspend 版本由协程调度器决定,恢复在调用方上下文。 - CallAdapter 和 Converter 的区别?
—— CallAdapter 管"返回类型"(Call/Observable/suspend body),Converter 管"数据怎么转"(JSON↔对象)。匹配都是工厂顺序遍历、先到先得。 - 如何上传文件 / 下载大文件?
—— @Multipart + @Part(MultipartBody.Part);@Streaming + ResponseBody.byteStream() 分块写盘(注意别开 Gson converter 转它)。 - 如何做 Token 过期自动刷新?
—— OkHttp Authenticator(401 触发,同步刷新并重放请求),与应用拦截器的 Token 注入配合。 - Retrofit 与 OkHttp 分工?
—— Retrofit 管"接口抽象、注解解析、类型适配、数据转换";OkHttp 管"连接池、拦截器链、缓存、HTTP/2、实际收发"。Retrofit 自己不做任何网络 IO。
