第一部分:OkHttp 基础
第一章:OkHttp 概述
1.1 OkHttp 是什么?作用是什么?
标准回答:
OkHttp 是 Square 开源的 HTTP/HTTP2 客户端库,用于 Android 和 Java 中发送 HTTP 请求、接收响应。它不仅是“发请求的工具”,而是自带连接池、拦截器链、缓存、自动 GZIP 等能力的完整网络层方案,Retrofit、Picasso 等很多库都基于 OkHttp。
作用分点说明:
| 维度 | 说明 | 面试可展开 |
|---|---|---|
| 请求能力 | 支持 GET/POST/PUT/DELETE/PATCH 等,HTTP/1.1、HTTP/2、OkHttp5 支持 HTTP/3(QUIC) | 能说清 HTTP/1.1 与 HTTP/2 区别(多路复用、头部压缩)会加分 |
| 网络优化 | 连接池复用 TCP、自动 GZIP、可选缓存、可配置超时与重试 | 连接池“按什么复用”要能答:Route(host+port+协议+代理) |
| 安全 | HTTPS、证书校验、Certificate Pinning、自定义 TLS | 证书锁定是防中间人、抓包 |
| 扩展性 | 拦截器链(应用拦截器 + 网络拦截器),可插拔日志、鉴权、加解密、监控 | 必须能说清“应用”和“网络”拦截器执行时机差异 |
底层实现:基于 Socket,通过责任链模式的拦截器依次处理请求与响应,连接由 ConnectionPool 统一复用。
常见追问
- “和直接用 Socket 有什么区别?”→ 封装了 HTTP 协议、连接复用、线程模型、缓存与重试。
- “为什么说它是门面?”→ 对外是 Call/Request/Response 等简单 API,内部是拦截器链、连接池、协议处理等复杂逻辑。
1.2 OkHttp 的特点和优势
特点与优势总表(按维度):
| 维度 | 特点/优势 | 说明 |
|---|---|---|
| 性能 | 连接池复用 | 同 host 多请求复用 TCP 连接,减少握手/挥手,降低延迟 |
| 自动 GZIP | 请求头自动加 Accept-Encoding: gzip,响应自动解压,省流量 | |
| HTTP/2 多路复用 | 单连接多请求并行,配合头部压缩,进一步提升吞吐与延迟 | |
| 功能 | 双层拦截器 | 应用拦截器(含缓存也走)、网络拦截器(仅真实请求),便于鉴权、日志、重试 |
| 同步/异步 | execute() 同步、enqueue(Callback) 异步,Dispatcher 管理线程池 | |
| 超时细分 | connectTimeout、readTimeout、writeTimeout 可分别设置 | |
| 内置缓存 | Cache + CacheInterceptor,支持强制缓存与协商缓存(304) | |
| 重试与重定向 | RetryAndFollowUpInterceptor 处理 3xx、部分连接失败重试 | |
| 易用 | Builder 链式 API | Request、OkHttpClient 等均 Builder 构建,链式调用、可读性好 |
| 不可变对象 | Request/Response 不可变,线程安全、便于测试与复用 | |
| 依赖简单 | 主要依赖 Okio,与 Android SDK 解耦,单元测试可用 MockWebServer | |
| 生态 | 上层库广泛采用 | Retrofit、Picasso、Glide 等以 OkHttp 为底层,生态统一 |
| 文档与维护 | Square 维护,文档全、Issue 响应快,社区方案多 |
面试可总结:性能上连接池 + GZIP + HTTP/2;功能上拦截器 + 缓存 + 超时/重试;易用上 Builder + 不可变;生态上被 Retrofit 等广泛使用。与 HttpURLConnection 的对比见 1.3。
1.3 OkHttp 和 HttpURLConnection 的区别(优势一览)
| 对比项 | OkHttp | HttpURLConnection |
|---|---|---|
| 连接管理 | 连接池复用,按 Route 匹配 | 无连接池,每次可能新建连接 |
| 协议 | HTTP/1.1、HTTP/2、HTTP/3(5.x) | 仅 HTTP/1.1 |
| 拦截器 | 支持,可统一加头、日志、重试 | 无,需每个请求手写 |
| 重试/缓存 | 可配置重试;内置 Cache + CacheInterceptor | 需自己实现 |
| GZIP | 自动加 Accept-Encoding、自动解压 | 需自己解压 |
| Cookie | CookieJar 接口,可持久化 | 需自己管理 Cookie 头 |
| API 风格 | Builder、不可变、链式 | 多步 set/get,易忘步骤 |
| 同步/异步 | execute / enqueue + Dispatcher | 只有同步,异步要自己开线程 |
| 取消 | Call.cancel(),可配合 Tag 批量取消 | 无统一取消 API |
| 依赖 | 需引入 okhttp | JDK 自带 |
代码对比(GET):
OkHttp:Request r = new Request.Builder().url(url).build(); try (Response resp = client.newCall(r).execute()) { resp.body().string(); }
HttpURLConnection:要 setRequestMethod、setConnectTimeout、getInputStream,还要自己处理重定向、编码、关流。
追问:什么时候还用 HttpURLConnection?→ 对包体积敏感、不能加第三方库,或只做极简单请求时。
1.4 OkHttp 版本与发展
| 版本 | 主要变化 / 特性 | 说明 |
|---|---|---|
| 2.x | 早期版本 | 基础 HTTP 能力,API 较原始 |
| 3.x | API 重构、拦截器、HTTP/2 | 架构与 API 转折点;拦截器链、连接池、更好错误处理 |
| 4.x | Kotlin、协程友好 | 与 Kotlin 生态更好配合,持续维护 |
| 5.x | HTTP/3(QUIC) | 支持 QUIC/UDP,0-RTT/1-RTT,持续性能与安全更新 |
面试要点:3.x 是 API 与架构的转折点;5.x 支持 HTTP/3(QUIC)。
1.5 如何添加 OkHttp 依赖?
Gradle(Module 级 build.gradle):
dependencies {
// OkHttp 核心库(必选)
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
// 可选:日志拦截器,便于调试
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
// 可选:单元测试时模拟 HTTP 服务
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
}
依赖说明:
| 依赖 | 说明 |
|---|---|
| okhttp | 核心库,包含连接池、拦截器、缓存、HTTP/2 等全部功能 |
| logging-interceptor | 打印请求/响应行、头、体,可设 Level(NONE/BASIC/HEADERS/BODY) |
| mockwebserver | 测试用,本地起 HTTP 服务,enqueue MockResponse 模拟接口 |
依赖冲突处理:若与其它库拉起的 OkHttp 版本不一致,可在工程级 build.gradle 中强制统一:
configurations.all {
resolutionStrategy {
force 'com.squareup.okhttp3:okhttp:4.12.0'
}
}
1.6 OkHttp 支持的 HTTP 协议
支持情况一览:OkHttp 支持 HTTP/1.0、HTTP/1.1、HTTP/2;从 OkHttp 5.x 起支持 HTTP/3(基于 QUIC)。与服务器协商时,若双方都支持会优先选更高版本,否则逐级降级。
协议对比表(面试可照表说清差异):
| 对比项 | HTTP/1.0 | HTTP/1.1 | HTTP/2 | HTTP/3 (QUIC) |
|---|---|---|---|---|
| 传输层 | TCP | TCP | TCP | UDP(QUIC) |
| 连接模型 | 每请求一连接 | 持久连接 Keep-Alive | 单连接多流 | 单连接多流 |
| 多路复用 | ✗ | ✗(请求串行) | ✓ 单连接多请求并行 | ✓ 单连接多流 |
| 头部压缩 | ✗ | ✗ | ✓ HPACK | ✓ QPACK |
| 数据格式 | 文本 | 文本 | 二进制分帧 | 二进制帧 |
| 服务器推送 | ✗ | ✗ | ✓ 可选 | ✓ 可选 |
| 队头阻塞 | 有 | 有(应用层) | 有(TCP 层) | 无(流独立) |
| 建连成本 | 每次 TCP 握手 | 复用连接 | 复用 + TLS 协商 | 0-RTT/1-RTT 建连更快 |
| OkHttp 支持 | ✓ 兼容 | ✓ 默认、最常用 | ✓ 与服务器协商可用 | ✓ 仅 5.x+ |
| 典型场景 | 老服务兼容 | 通用、代理友好 | 多请求同域、低延迟 | 弱网、移动端、低延迟 |
要点说明:
-
HTTP/1.1
当前默认;同一 TCP 连接可发多个请求,但请求-响应是串行的,一个没返回下一个要等(应用层队头阻塞);不支持头部压缩。 -
HTTP/2
多路复用:一条连接上多个请求/响应通过 Stream ID 并行,不必排队。
二进制分帧:不再用纯文本,便于解析与压缩。
HPACK 压缩头部,减少重复字段。
服务器推送:服务端可主动向客户端推送资源(如 CSS/JS),无需客户端先请求,减少 RTT;实际中 OkHttp 对 Server Push 支持有限,多数场景仍以客户端请求为主。
仍基于 TCP,TCP 丢包时整条连接会阻塞(TCP 层队头阻塞)。 -
HTTP/3
基于 QUIC(UDP),每个流独立,丢包只影响该流,无 TCP 层队头阻塞。
建连可 0-RTT,适合弱网与移动端;需 OkHttp 5.x 及以上。
OkHttp 如何选协议(用 HTTP/1.1 还是 HTTP/2)
协议选择机制:OkHttp 会自动选择双方都支持的最高可用协议版本(如都支持则优先 HTTP/2,否则降级到 HTTP/1.1),一般无需手写。
- 平时不用管:走 HTTPS 时,建立 TLS 连接那一步,客户端和服务器会通过 ALPN 自动“商量”用哪个协议(h2 或 http/1.1)。OkHttp 默认参与这次商量,最后用商量出来的结果发请求,所以一般不用写任何代码。
- 想自己定协议时:比如只想用 HTTP/2 和 1.1、或想优先试 HTTP/2,再在 Builder 里设
protocols,列表顺序即优先级(前面的先试):
OkHttpClient client = new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build();
面试追问:HTTP/2 的“多路复用”具体指什么?→ 一条 TCP 连接上可以同时发多个请求、收多个响应,用 Stream ID 区分,不必等前一个请求结束再发下一个,从而减少延迟、提高吞吐。
1.7 OkHttp 的架构设计
1. 整体架构分层
OkHttp 采用分层架构 + 拦截器链(责任链),从顶到底分为四层:
┌─────────────────────────────────────────┐
│ 应用层 (Application) │
│ OkHttpClient、Request、Response、Call │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 拦截器链 (Interceptor Chain) │
│ 应用拦截器 + 内置拦截器 + 网络拦截器 │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 连接层 (Connection) │
│ ConnectionPool、Route、RealConnection │
└─────────────────────────────────────────┘
│
┌─────────────────────────────────────────┐
│ 网络层 (Network) │
│ Socket、TLS、HTTP/2 编解码 │
└─────────────────────────────────────────┘
2. 核心组件与职责(表格)
| 组件 | 作用 | 职责简述 |
|---|---|---|
| OkHttpClient | 配置入口、全局单例 | 持有 ConnectionPool、Dispatcher、拦截器列表、超时/代理/Cache/CookieJar 等;线程安全,建议全局单例共享 |
| Request | 请求封装 | URL、Method、Headers、Body;不可变,Builder 构建 |
| Response | 响应封装 | code、message、headers、body;可含 cacheResponse、networkResponse,区分来源 |
| Call(RealCall) | 一次请求-响应对 | execute() 同步、enqueue() 异步;内部统一走 getResponseWithInterceptorChain() 进入拦截器链 |
| Interceptor | 请求/响应处理 | intercept(Chain):可改 request → chain.proceed() → 可改 response 后返回;应用拦截器与网络拦截器两种 |
| ConnectionPool | 连接复用与清理 | 按 Route 存空闲 RealConnection;取连接时匹配 Route + 健康检查;定时清理超时或超额连接 |
| Dispatcher | 异步请求调度 | 维护“正在执行”与“等待”的 Call 队列;用线程池执行异步请求;默认最大并发 64、每 host 5 |
| Route | 路由信息 | 封装 address、proxy、协议等,用于连接匹配与复用 |
| RealConnection | 真实连接 | 封装 Socket、TLS、HTTP/2;连接池中复用的单位 |
3. 请求执行流程(步骤拆解)
| 步骤 | 说明 |
|---|---|
| 1 | 构建 Request,通过 client.newCall(request) 得到 RealCall |
| 2 | 调用 execute() 或 enqueue():内部进入 getResponseWithInterceptorChain() |
| 3 | 构建 RealInterceptorChain,按顺序依次执行各拦截器 |
| 4 | 请求方向:每个拦截器可修改 request,调用 chain.proceed(request) 进入下一层 |
| 5 | ConnectInterceptor 从 ConnectionPool 获取或新建 RealConnection,建立 TCP/TLS |
| 6 | CallServerInterceptor 通过连接写请求、读响应,完成真实 IO |
| 7 | 响应方向:Response 沿链反向返回,每层拦截器可修改 response 再返回 |
| 8 | 同步则直接返回 Response;异步则在 Dispatcher 线程池中回调 Callback |
4. 拦截器链顺序(请求 → 响应)
请求方向(从上到下):
| 顺序 | 类型 | 拦截器 | 作用 |
|---|---|---|---|
| 1 | 应用 | 用户 addInterceptor 添加的拦截器 | 鉴权、公共头、日志等;即使命中缓存也会执行 |
| 2 | 内置 | RetryAndFollowUpInterceptor | 重试、处理 3xx 重定向 |
| 3 | 内置 | BridgeInterceptor | 补全 Content-Type、Content-Length、Accept-Encoding 等,解压 GZIP |
| 4 | 内置 | CacheInterceptor | 读缓存、写缓存、发条件请求(304) |
| 5 | 内置 | ConnectInterceptor | 从连接池取或新建连接,得到 Exchange |
| 6 | 网络 | 用户 addNetworkInterceptor 添加的拦截器 | 网络层日志、监控、重试;仅真实发请求时执行 |
| 7 | 内置 | CallServerInterceptor | 写请求行/头/体,读响应行/头/体 |
响应方向:按上述顺序反向从 CallServerInterceptor 一路返回到用户应用拦截器。
5. 设计模式
| 设计模式 | 应用位置 | 说明 |
|---|---|---|
| 责任链 | 拦截器链 Interceptor + Chain | 每个 Interceptor 处理完通过 chain.proceed() 交给下一个,请求与响应沿同一条链传递,功能解耦、易扩展。 |
| 建造者 | Request.Builder、OkHttpClient.Builder、Response.Builder | 链式配置 URL、header、body、超时、拦截器等,避免构造参数过多。 |
| 单例 | OkHttpClient | 通常全局单例,共享连接池、Dispatcher、配置,避免重复建连、线程池。 |
| 策略 | 各 Interceptor 实现 | 不同拦截器承担不同策略(重试、桥接、缓存、连接、写请求),可插拔组合。 |
| 工厂 | Call.Factory、EventListener.Factory | newCall(request) 由工厂创建 RealCall;EventListener 由 Factory 创建,便于按请求定制监听。 |
| 门面 | OkHttpClient | 对外只暴露 newCall、newBuilder 等简单 API,内部封装 Dispatcher、ConnectionPool、拦截器链、连接建立等复杂逻辑。 |
6. 架构优势
| 优势 | 说明 |
|---|---|
| 模块化 | 各层职责清晰,拦截器、连接池、缓存独立,便于理解和维护 |
| 可扩展 | 拦截器机制无需改核心代码即可加鉴权、日志、加解密、监控 |
| 高性能 | 连接池复用、HTTP/2 多路复用、缓存与 GZIP,减少延迟与流量 |
| 灵活 | 超时、代理、Cookie、缓存、协议等均可配置,适配多种场景 |
7. 关键设计决策
- 拦截器链:统一用一条链处理请求与响应,功能解耦、易扩展,而不是在 Call 里堆 if-else。
- 连接池:复用 TCP 连接,减少握手开销,配合 HTTP/2 进一步提升性能。
- 异步:通过 Dispatcher 管理线程池与队列,避免阻塞调用方,支持取消与并发控制。
- 缓存:遵循 HTTP 缓存标准,由 CacheInterceptor 统一处理,对业务透明。
面试追问:为什么需要两层拦截器(应用 vs 网络)?→ 应用拦截器即使从缓存返回也会执行,适合做鉴权、统一加头、业务日志;网络拦截器只在真正发网络请求时执行,适合看真实请求/响应、做网络监控或仅对 IO 失败重试。
第二章:OkHttp 基本使用
2.1 如何发送 GET、POST、PUT、DELETE?
通用步骤:建 OkHttpClient(建议单例)→ 用 Request.Builder 建 Request(设 URL、方法、可选 body/headers)→ client.newCall(request).execute()(同步)或 enqueue(callback)(异步)。
注意:同步不能在 Android 主线程调用,否则会抛 NetworkOnMainThreadException;GET 的 URL 可带查询参数,见 2.3。
各方法要点:
| 方法 | Builder 用法 | 请求体 | 说明 |
|---|---|---|---|
| GET | .url(url).get() 或省略 .get() | 无 | 获取资源,无 body |
| POST | .post(requestBody) | 必须有 | 创建/提交资源,需 RequestBody |
| PUT | .put(requestBody) | 必须有 | 全量更新资源,需 RequestBody |
| PATCH | .patch(requestBody) | 通常有 | 部分更新,需 RequestBody |
| DELETE | .delete() | 通常无 | 删除资源;若要 body 用 .delete(requestBody) |
| HEAD | .head() | 无 | 只取响应头,无 body |
完整示例(POST JSON):
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
String json = "{\"name\":\"张三\",\"age\":25}";
RequestBody body = RequestBody.create(
json, MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url("https://api.example.com/users")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String result = response.body().string();
} else {
int code = response.code();
}
} catch (IOException e) {
// 网络异常、超时等
}
请求体常见形式:POST/PUT 等需 RequestBody,常见三种——JSON(RequestBody.create(json, application/json))、表单(FormBody.Builder)、文件/多部分(MultipartBody.Builder)。JSON 与表单的完整写法见 2.4,multipart 见第五章文件上传。
2.2 如何设置请求头?
在 Request.Builder 上链式调用即可;常用如 Authorization、Content-Type、User-Agent。
(1)单值覆盖 — header()
同名 header 只保留一个,后设置的会覆盖先设置的。
Request request = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.header("User-Agent", "MyApp/1.0")
.build();
(2)多值同键 — addHeader()
同一 key 可添加多个值,不会覆盖。
Request request = new Request.Builder()
.url(url)
.addHeader("Accept", "application/json")
.addHeader("Accept", "application/xml")
.build();
(3)批量设置 — headers()
用 Headers.Builder 构建后一次性传入,会替换掉之前通过 header/addHeader 设置的 headers。
Headers headers = new Headers.Builder()
.add("Authorization", "Bearer " + token)
.add("Content-Type", "application/json")
.add("Accept", "application/json")
.build();
Request request = new Request.Builder()
.url(url)
.headers(headers)
.build();
注意:header() 会覆盖同名;敏感信息放 Header,不要放 URL。
2.3 如何设置查询参数?
GET 等请求的 URL 可带查询参数:用下文的 HttpUrl 拼好完整 URL,再 .url(httpUrl).get() 即可。
(1)直接在 URL 里拼(不推荐,易漏编码)
String url = "https://api.example.com/users?page=1&limit=10&sort=name";
Request request = new Request.Builder().url(url).build();
(2)使用 HttpUrl.Builder(推荐,自动编码)
HttpUrl httpUrl = HttpUrl.parse("https://api.example.com/users").newBuilder()
.addQueryParameter("page", "1")
.addQueryParameter("limit", "10")
.addQueryParameter("sort", "name")
.build();
Request request = new Request.Builder().url(httpUrl).build();
(3)动态参数(Map)
Map<String, String> params = new HashMap<>();
params.put("page", "1");
params.put("limit", "10");
HttpUrl.Builder builder = HttpUrl.parse(baseUrl).newBuilder();
for (Map.Entry<String, String> e : params.entrySet()) {
builder.addQueryParameter(e.getKey(), e.getValue());
}
Request request = new Request.Builder().url(builder.build()).build();
(4)特殊字符与数组
addQueryParameter 会对值做 URL 编码(如空格→%20、中文按 UTF-8 编码)。同一 key 多个值可多次 addQueryParameter("ids","1"); addQueryParameter("ids","2"),得到 ?ids=1&ids=2。
注意:空字符串和 null 编码结果不同;参数顺序一般不影响结果。
2.4 如何发送 JSON 与表单数据?
对比一览:
| 维度 | 表单(Form Data) | JSON |
|---|---|---|
| 写法 | FormBody.Builder().add(k,v)...build(),再 post(formBody) | RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")),再 post(body) |
| Content-Type | OkHttp 自动设为 application/x-www-form-urlencoded,自动 URL 编码 | 固定 application/json,charset 建议 utf-8 |
| 数据来源/格式 | 手写多个 add 或 Map 遍历;格式 key=value&key=value | 手写字符串或 Gson:gson.toJson(obj);格式 {"key":"value"} |
| 适用场景 | 表单提交、登录 | 常见 REST API |
| 嵌套结构 | 不支持 | 支持 |
示例代码:
// JSON
String json = "{\"name\":\"张三\",\"age\":25}"; // 或 gson.toJson(user)
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder().url("https://api.example.com/users").post(body).build();
// 表单
RequestBody formBody = new FormBody.Builder()
.add("username", "zhangsan").add("password", "123456").add("email", "zhangsan@example.com")
.build();
Request request = new Request.Builder().url("https://api.example.com/login").post(formBody).build();
注意:日期等类型需先转成字符串或约定格式;Form 中空字符串会正常发送。
2.5 如何处理响应与响应头?
(1)基本响应、状态码与响应头
同步时用 try (Response response = client.newCall(request).execute()) 获取响应。先判断 response.isSuccessful() 再读 body;非 2xx 时用 response.code()、response.message() 处理。响应头:response.header("Content-Type") 取单值,response.headers() 取 Headers 对象,可遍历或 headers.values("Set-Cookie") 取多值(响应头名大小写不敏感)。
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String body = response.body().string();
} else {
int code = response.code();
String message = response.message();
}
// 响应头示例(在 try 内使用 response)
Headers headers = response.headers();
String contentType = response.header("Content-Type");
}
(2)响应体与按类型处理
response.body() 得到 ResponseBody。小文本用 body.string(),小二进制用 body.bytes(),大文件用 body.byteStream() 流式读写避免 OOM。body 只能读一次,读后流关闭。
| 响应类型 | 写法 |
|---|---|
| JSON | String json = response.body().string(); 再 gson.fromJson(json, User.class) |
| 纯文本 | response.body().string() |
| 二进制/文件 | response.body().bytes() 或 byteStream() 写文件 |
(3)错误处理与响应来源
4xx/5xx 仍进 onResponse(或同步的 execute() 返回的 Response),需用 response.isSuccessful() 或 response.code() 分支:401/403/404/5xx 等按业务处理。网络异常、超时、取消等抛 IOException(同步需 catch;异步进 onFailure)。响应来源:response.cacheResponse() 非 null 表示来自缓存,response.networkResponse() 非 null 表示来自网络(304 时可能两者都有)。
重要注意:
- ResponseBody 只能读一次,再次读取会抛错或得到空;需多次使用可先
bytes()存起来。 - 必须关闭 Response(try-with-resources 或
response.close()),否则连接无法回收到连接池。 - 大响应用
byteStream()流式读写,避免string()/bytes()导致 OOM。
面试追问:为什么 body 只能读一次?→ 底层是流,读完后流已消费无法回放;需多次使用可先 bytes() 再解析。
第三章:OkHttp 异步请求
3.1 同步与异步的区别
| 特性 | 同步 execute() | 异步 enqueue() |
|---|---|---|
| 线程 | 调用线程阻塞执行 | 调用立即返回,IO 在 Dispatcher 线程池 |
| 返回值 | 直接得到 Response | 通过 Callback 的 onResponse/onFailure |
| 适用 | 后台线程、单元测试 | 主线程发起、不阻塞 UI |
| 取消 | 需自己管理线程中断 | call.cancel() 即可 |
重要:
- 同步不能在 Android 主线程调用,否则抛 NetworkOnMainThreadException。
- 异步的 onResponse / onFailure 在 Dispatcher 的线程池执行,不是主线程,所以回调里更新 UI 必须切回主线程(runOnUiThread、Handler、LiveData.postValue 等)。
线程模型简述:
- 同步:谁调 execute() 谁被阻塞,直到拿到 Response。
- 异步:主线程调 enqueue() 立即返回;RealCall 被放进 Dispatcher 的队列,由线程池里的线程执行 IO,执行完后在同一线程调 onResponse/onFailure,所以回调里不能直接操作 UI。
队列、并发与优先级:
| 维度 | 同步 execute() | 异步 enqueue() |
|---|---|---|
| 是否有队列 | 无;每次 execute() 占用调用线程直接执行 | 有;进入 Dispatcher 的 就绪队列,满足条件后进入 运行队列 |
| 队列/并发上限 | 无上限(由调用方线程数决定) | 最大并发 默认 64(maxRequests),每主机最大并发 默认 5(maxRequestsPerHost);超出部分在就绪队列等待 |
| 执行顺序 | 谁先调 execute() 谁先执行(多线程下取决于线程调度) | FIFO:就绪队列先进先出,无内置优先级 |
| 优先级 | 无 API | 无内置优先级;若要“高优先级”需业务层控制(如先 enqueue 重要请求、或自建队列再按优先级 enqueue) |
同步请求也会被 Dispatcher 记录(runningSyncCalls),便于 cancelAll 等,但不占用异步的 maxRequests / maxRequestsPerHost 配额。
3.2 发异步请求与 Callback 要点
发异步:client.newCall(request).enqueue(callback),调用后立即返回,结果在 Callback 里拿到。
Callback 接口:onResponse(Call, Response) 请求成功时调,可取 response.body();onFailure(Call, IOException) 网络异常、超时、取消时调。两方法都在 Dispatcher 线程池执行,不是主线程。
Android 中正确用法(回调里更新 UI 必须切主线程):
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful() && response.body() != null) {
String body = response.body().string();
runOnUiThread(() -> textView.setText(body));
}
}
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(() -> Toast.makeText(MainActivity.this, "网络错误", Toast.LENGTH_SHORT).show());
}
});
注意:先判断 response.isSuccessful() 再取 body;onResponse 可能抛 IOException;在 onFailure 里可先 if (call.isCanceled()) return; 再提示,避免对已取消请求更新 UI。补充:一次请求只会回调 onResponse 或 onFailure 其一;onResponse 在收到响应头时就会调用,body 需在本方法内读(response.body().string() 只能读一次,读后流关闭)。
3.3 异步错误处理
| 情况 | 处理方式 |
|---|---|
| onFailure | 网络异常、超时、取消;可用 instanceof SocketTimeoutException / ConnectException / UnknownHostException 区分提示;call.isCanceled() 为 true 时可提示“已取消”或不提示。 |
| onResponse 且 !response.isSuccessful() | 4xx/5xx,用 response.code() 处理:401 跳登录、403 无权限、404 不存在、5xx 提示重试。 |
区分:网络异常、超时、取消等走 onFailure;服务器返回 4xx/5xx 仍走 onResponse,需用 response.isSuccessful() 或 response.code() 再分支处理。
3.4 取消与多请求管理
单次取消:call.cancel();call.isCanceled() 判断是否已取消。Activity 内:在 onDestroy 里 if (currentCall != null) currentCall.cancel(); 并置空,防泄漏。
按 Tag 批量取消:Request 设 .tag("xxx"),取消时 client.dispatcher().cancel("xxx") 只取消带该 tag 的请求;client.dispatcher().cancelAll() 取消所有请求(含同步与异步)。
用 List 管理多 Call:enqueue 前把 Call 加入 List,在 onResponse/onFailure 里移除;需要时遍历 List 对未执行的 Call 调 cancel()。集合建议线程安全(如 CopyOnWriteArrayList);页面销毁时统一 cancel 或按 tag 取消。
// 单次取消 + Activity 生命周期
Call currentCall = client.newCall(request);
currentCall.enqueue(callback);
// onDestroy: if (currentCall != null) currentCall.cancel();
// 按 Tag 取消
Request request = new Request.Builder().url(url).tag("user").build();
client.dispatcher().cancel("user");
多 Call 用 List 管理示例(需在 onResponse/onFailure 里从列表移除,页面销毁时遍历 cancel):
List<Call> calls = new CopyOnWriteArrayList<>();
Call c = client.newCall(request);
calls.add(c);
c.enqueue(new Callback() {
@Override public void onResponse(Call call, Response response) throws IOException {
calls.remove(call);
// 处理响应
}
@Override public void onFailure(Call call, IOException e) {
calls.remove(call);
}
});
// 取消全部:for (Call call : calls) if (!call.isExecuted()) call.cancel(); calls.clear();
注意:已执行完的请求无法取消;回调里先判断 call.isCanceled() 再更新 UI;取消后清空对 Call 的引用。
3.5 异步线程模型
Dispatcher 作用:管理异步请求的执行与等待队列,内部用线程池执行 IO;控制最大并发数,避免过多连接。
就绪队列与运行队列:
-
就绪队列(readyAsyncCalls)
- 含义:已调用
enqueue()但还没开始执行网络 IO 的异步 Call 的等待队列。 - 大小:无固定上限,enqueue 多少就排多少;只有“运行队列”有空位时才会从就绪队列里取 Call 去执行。
- 谁有:只有异步有就绪队列;同步没有(
execute()一调就占当前线程执行,不排队)。
- 含义:已调用
-
运行队列(runningAsyncCalls / runningSyncCalls)
- 含义:正在执行(或正在跑网络 IO)的 Call 的集合。
- 大小:
- 异步:同时运行的异步 Call 数 ≤ maxRequests(默认 64),且同一 host ≤ maxRequestsPerHost(默认 5);超出时新 enqueue 的 Call 进就绪队列等待。
- 同步:无数量上限,由当前有多少线程在调
execute()决定;同步 Call 会被记在runningSyncCalls里,便于cancelAll()等,但不占异步的 64/5 配额。
- 谁有:同步和异步都有“运行中”的集合;同步只有这一种(无就绪队列),异步有就绪 + 运行两种。
| 队列 | 含义 | 大小/上限 | 同步是否有 | 异步是否有 |
|---|---|---|---|---|
| 就绪队列 | 等待执行的 Call | 无上限,按 enqueue 增长 | 否 | 是 |
| 运行队列 | 正在执行的 Call | 异步:≤64 且每 host≤5;同步:无上限 | 是(runningSyncCalls) | 是(runningAsyncCalls) |
查看队列:client.dispatcher().queuedCalls() 返回就绪队列(仅异步、等待执行的 Call),client.dispatcher().runningCalls() 返回正在执行的 Call(含异步与同步)。
默认配置与队列大小:
| 配置项 | 默认值 | 说明 |
|---|---|---|
| maxRequests | 64 | 全局同时执行的异步 Call 上限;超出部分留在就绪队列等待 |
| maxRequestsPerHost | 5 | 同一 host 同时执行的请求上限,防止单域名占用过多连接 |
| 线程池 | 无上限(按需创建) | 执行请求与回调的线程 |
执行顺序与优先级:就绪队列按 FIFO 被取出执行;OkHttp 没有请求优先级 API,若要优先执行某类请求,需在业务层控制 enqueue 顺序或自建优先队列再调用 enqueue。说明:同一 OkHttpClient 上同步与异步可混用,Dispatcher 统一记录;WebSocket 连接不计入 maxRequestsPerHost。
自定义 Dispatcher:
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(100);
dispatcher.setMaxRequestsPerHost(10);
OkHttpClient client = new OkHttpClient.Builder()
.dispatcher(dispatcher)
.build();
线程模型小结:
enqueue()在调用线程(如主线程)执行,立即返回。- 实际网络 IO 在 Dispatcher 的线程池中执行。
onResponse/onFailure也在该线程池执行,故更新 UI 必须切到主线程。
3.6 避免异步导致的内存泄漏
典型泄漏场景:
- Activity/Fragment 持有 Call + Callback 持有 Activity/Fragment:在 Activity 里 enqueue,Callback 用匿名内部类或非静态内部类,会隐式持有 Activity;请求未完成时 Activity 已销毁,导致 Activity 无法被 GC。
- 未在生命周期里取消:没有在 onDestroy(或 Fragment 的 onDestroyView/onDestroy)里 cancel 该 Call,连接和回调会一直挂着,Callback 继续持有外部类引用。
- 静态或单例持有 Call/Callback:若 Manager、单例等长期存活对象持有 Call 或注册了持有 Activity 的 Callback,且未在页面销毁时取消或移除,会长期持有 Activity。
- 多次请求只取消部分:同一页面多次 enqueue,只对“最后一次”的 Call 做 cancel 或只存最后一个引用,之前的 Call 未取消且仍被某处引用,导致页面泄漏。
正确做法:
- 在 onDestroy 里取消:若 Activity 持有
Call currentCall,在 onDestroy 中if (currentCall != null) currentCall.cancel();并置空。 - Callback 不持强引用:用静态内部类 + WeakReference<Activity>,在 onResponse/onFailure 里先
Activity a = ref.get(); if (a == null) return;再更新 UI。 - 用 ViewModel/Repository 持有 Call:在 ViewModel.onCleared() 或 Repository 的 clear 中 cancel,避免 Activity 直接持有 Call。
追问:cancel 之后回调还会执行吗?→ 会,但 call.isCanceled() 为 true,应在回调里先判断,若已取消就不再更新 UI 或做后续逻辑。
WeakReference 写法示例(避免 Callback 持强引用 Activity):
static class MyCallback implements Callback {
final WeakReference<MainActivity> ref;
MyCallback(MainActivity a) { ref = new WeakReference<>(a); }
@Override
public void onResponse(Call call, Response response) throws IOException {
MainActivity a = ref.get();
if (a == null) return;
String body = response.body() != null ? response.body().string() : "";
a.runOnUiThread(() -> a.textView.setText(body));
}
@Override
public void onFailure(Call call, IOException e) {
if (ref.get() != null) ref.get().runOnUiThread(() -> { /* 提示错误 */ });
}
}
第四章:OkHttp 请求配置
4.1 超时配置(连接 / 读取 / 写入)
三种超时均通过 OkHttpClient.Builder 设置,超时抛 SocketTimeoutException。
| 类型 | 含义 | 常用设置 | 场景建议 |
|---|---|---|---|
| connectTimeout | 从发起到建立 TCP(含 TLS)的最大等待时间 | 10s | WiFi/内网可 5s,弱网/海外 20~30s |
| readTimeout | 连接建立后,读响应体的最大等待时间 | 30s | 普通 API 10~30s,大文件下载 60s+,流式可设 0(慎用) |
| writeTimeout | 发送请求体的最大等待时间 | 30s | 普通 POST 10~30s,大文件上传 60~120s+ |
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
注意:readTimeout 建议 ≥ connectTimeout;大文件上传/下载需相应增大 writeTimeout/readTimeout;流式设 0 时需业务层控制结束,避免占用连接。
4.2 重试机制
OkHttp 默认不会对失败请求自动重试(只会对 307/308 等做重定向、对某些连接失败做一次连接重试)。若要对超时、连接失败等做业务层重试,需要自定义拦截器。
思路:在 intercept 里 for 循环,try-catch 调用 chain.proceed(request);若抛 SocketTimeoutException、ConnectException、UnknownHostException 等可重试异常,且当前重试次数 < 最大次数,则 sleep 一段时间(建议递增退避,如 1s、2s、3s)后再次 proceed;否则 throw 出去。注意:只对幂等请求(GET、PUT、DELETE)或你确认可重试的 POST 使用,避免重复提交。
示例骨架:
public class RetryInterceptor implements Interceptor {
private final int maxRetry;
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException last = null;
for (int i = 0; i <= maxRetry; i++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) return response;
} catch (IOException e) {
last = e;
if (!isRetryable(e) || i >= maxRetry) throw e;
try { Thread.sleep(1000L * (i + 1)); } catch (InterruptedException ie) { throw new IOException(ie); }
}
}
if (response != null) return response;
throw last;
}
private boolean isRetryable(IOException e) {
return e instanceof SocketTimeoutException || e instanceof ConnectException;
}
}
4.3 重定向
默认行为:OkHttp 默认 followRedirects(true),自动跟随 301、302、303、307、308,最多约 20 次,由 RetryAndFollowUpInterceptor 处理。
禁用:OkHttpClient.Builder().followRedirects(false).build()。
自定义重定向:需在拦截器里实现。判断 response.code() 为 3xx 时,取 response.header("Location"),用 request.url().resolve(location) 得新 URL,再 chain.proceed(request.newBuilder().url(newUrl).build()) 重新发请求;必须限制重定向次数或记录已访问 URL 避免死循环。
4.4 代理
(1)HTTP 代理
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.example.com", 8080));
OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).build();
(2)SOCKS 代理
Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("socks.example.com", 1080));
OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).build();
(3)代理需要认证时
Authenticator proxyAuthenticator = (route, response) -> {
String credential = Credentials.basic("username", "password");
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
};
OkHttpClient client = new OkHttpClient.Builder()
.proxy(proxy)
.proxyAuthenticator(proxyAuthenticator)
.build();
服务器返回 407 Proxy Authentication Required 时,OkHttp 会调用 proxyAuthenticator,用返回的带 Proxy-Authorization 的 Request 重试。
4.5 OkHttpClient 配置汇总
常用配置项:超时(4.1)、连接池、拦截器、缓存、CookieJar、代理、Authenticator、重定向、协议列表等;按需组合。
完整配置示例:
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES))
.addInterceptor(new LoggingInterceptor())
.addNetworkInterceptor(new HeaderInterceptor())
.cache(new Cache(new File(context.getCacheDir(), "http-cache"), 10 * 1024 * 1024))
.cookieJar(cookieJar)
.proxy(proxy)
.authenticator(authenticator)
.followRedirects(true)
.followSslRedirects(true)
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.build();
单例写法(推荐):
public class OkHttpManager {
private static volatile OkHttpClient instance;
public static OkHttpClient getInstance() {
if (instance == null) {
synchronized (OkHttpManager.class) {
if (instance == null) {
instance = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(new LoggingInterceptor())
.build();
}
}
}
return instance;
}
}
按环境区分:
| 环境 | 建议 |
|---|---|
| Debug | 加 HttpLoggingInterceptor,Level 为 BODY 或 HEADERS,便于抓包排查 |
| Release | 不加或 Level 为 NONE/BASIC;加统一鉴权、公共头等拦截器,避免日志泄露 |
第五章:OkHttp 文件操作
5.1 上传文件(单文件 / 多文件 / 大文件)
使用 MultipartBody(multipart/form-data),可同时传文件和普通字段。
单文件:
RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpeg"));
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", "file.jpg", fileBody)
.addFormDataPart("description", "图片描述")
.build();
Request request = new Request.Builder().url(uploadUrl).post(requestBody).build();
多文件:多次 addFormDataPart,或循环 List<File> 动态添加;注意总大小与 writeTimeout。
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
builder.addFormDataPart("avatar", "avatar.jpg", RequestBody.create(avatarFile, MediaType.parse("image/jpeg")));
builder.addFormDataPart("cover", "cover.jpg", RequestBody.create(coverFile, MediaType.parse("image/jpeg")));
Request request = new Request.Builder().url(uploadUrl).post(builder.build()).build();
大文件:用流式 RequestBody(见 5.3 上传进度)避免整文件进内存 OOM;适当增大 writeTimeout。
注意:按文件类型设 MediaType(image/jpeg、application/pdf 等);大文件可配合 5.3 做上传进度。
5.2 下载文件
(1)流式下载(推荐,适合大文件)
Request request = new Request.Builder().url("https://example.com/file.pdf").build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
InputStream inputStream = response.body().byteStream();
FileOutputStream outputStream = new FileOutputStream("file.pdf");
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.close();
}
}
(2)一次性读取(仅小文件)
if (response.isSuccessful() && response.body() != null) {
byte[] bytes = response.body().bytes();
FileOutputStream fos = new FileOutputStream("file.pdf");
fos.write(bytes);
fos.close();
}
注意:大文件必须用流式,避免 OOM;先判断 isSuccessful() 和 body() != null;用 try-with-resources 或 finally 关闭流;保存路径建议用应用内部/外部存储并处理权限。
5.3 上传进度与下载进度
上传进度:自定义 RequestBody,重写 writeTo(BufferedSink sink),用 Okio.source(file) 分段读、写入 sink,每写一段根据已写字节与 contentLength() 回调进度。回调在 IO 线程,更新 UI 需切主线程。
// 思路:继承 RequestBody,writeTo 里用 Okio 分段读文件、写 sink,并回调进度
@Override
public void writeTo(BufferedSink sink) throws IOException {
long total = contentLength();
long written = 0;
Source source = Okio.source(file);
Buffer buffer = new Buffer();
long read;
while ((read = source.read(buffer, 8192)) != -1) {
sink.write(buffer, read);
written += read;
if (listener != null) listener.onProgress(written, total);
}
}
下载进度:用自定义 ResponseBody 包装原始 body,重写 source(),用 ForwardingSource 包装原始 source,在 read 里累计已读字节并回调进度;再通过拦截器将 response.newBuilder().body(progressBody).build() 返回。contentLength() 可能为 -1(无 Content-Length 时只能显示已下载量)。
5.4 断点续传
原理:通过请求头 Range: bytes=已下载长度- 告诉服务器从该字节之后开始返回;服务器支持则返回 206 Partial Content 和剩余内容,否则可能返回 200 全量。
基本实现:
File targetFile = new File("/path/to/file.pdf");
long downloadedBytes = targetFile.exists() ? targetFile.length() : 0;
Request request = new Request.Builder()
.url(url)
.header("Range", "bytes=" + downloadedBytes + "-")
.build();
try (Response response = client.newCall(request).execute()) {
if (response.code() == 206 && response.body() != null) {
FileOutputStream fos = new FileOutputStream(targetFile, true); // 追加
InputStream in = response.body().byteStream();
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
} else if (response.code() == 200) {
// 服务器不支持 Range,需删掉旧文件重新全量下载
targetFile.delete();
// 再走一次普通下载
}
}
总大小:可从响应头 Content-Range 解析,格式如 bytes start-end/total,其中 total 即文件总字节数,用于计算进度。
注意:需服务器支持 Range 请求;本地文件必须以追加模式打开;实际项目可把已下载长度持久化,避免每次用文件长度(断点可能不在文件末尾)。
第二部分:OkHttp 高级特性
第六章:OkHttp 拦截器(Interceptor)
6.1 拦截器是什么?作用?
定义:实现 Interceptor 接口,在请求发出前和响应返回后做统一处理。
作用:统一加头、鉴权、日志、重试、加解密、监控等;通过责任链串联,每个拦截器可修改 Request/Response 或决定是否继续 chain.proceed()。
6.2 拦截器执行顺序
OkHttp 用责任链处理请求:请求从链顶依次往下传,到最底层真正发网络 IO;响应再从最底层沿链反向一路返回到调用方。每一层都可以在请求前改 Request、在响应后改 Response。
(1)请求方向(从上到下)
| 顺序 | 拦截器 | 类型 | 作用简述 |
|---|---|---|---|
| 1 | 应用拦截器 | 用户添加 | addInterceptor() 添加;鉴权、公共头、全局日志等;即使命中缓存也会执行 |
| 2 | RetryAndFollowUpInterceptor | 内置 | 重试(如连接失败)、处理 3xx 重定向、407 代理认证等 |
| 3 | BridgeInterceptor | 内置 | 补全 Content-Type、Content-Length、Accept-Encoding 等;响应时解压 GZIP |
| 4 | CacheInterceptor | 内置 | 读缓存、写缓存、发条件请求(304);命中缓存时可能不再往下走 |
| 5 | ConnectInterceptor | 内置 | 从连接池取或新建 RealConnection,建立 TCP/TLS,得到 Exchange |
| 6 | 网络拦截器 | 用户添加 | addNetworkInterceptor() 添加;仅真正发网络请求时执行,可见重定向等中间请求 |
| 7 | CallServerInterceptor | 内置 | 通过连接写请求行/头/体,读响应行/头/体,完成真实 IO |
记忆:应用 → 重试 → 桥接 → 缓存 → 连接 → 网络 → 发请求。
(2)响应方向(从下到上)
响应从 CallServerInterceptor 一路反向返回:先经过网络拦截器,再依次经过 Connect、Cache、Bridge、RetryAndFollowUp、应用拦截器。每一层拿到的都是下一层返回的 Response,可修改后再 return 给上一层。
(3)关键点
- 必须调用一次
chain.proceed(request):才会把请求交给下一层并拿到 Response;不调用则链中断,请求不会发出。 - 通常只调用一次:多次 proceed 会多次发请求(除非刻意做重试、重放等)。
- 应用 vs 网络:应用拦截器包在最外层,能看到“最终”的一次请求/响应;网络拦截器在 Connect 与 CallServer 之间,仅真实走网络时执行,且能看到重定向、中间请求(见 6.4)。
6.3 如何自定义拦截器
实现 Interceptor 接口,在 intercept(Chain chain) 中:
- 拿到
Request request = chain.request(),用request.newBuilder()...build()得到新 Request(如加 Header)。 - 调用
Response response = chain.proceed(newRequest)把请求交给下一层(必须调用,否则请求不会发出)。 - 可选:用
response.newBuilder()...build()包装响应后再 return。
示例:统一加 Token
public class AuthInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
Request request = original.newBuilder()
.header("Authorization", "Bearer " + getToken())
.build();
return chain.proceed(request);
}
}
6.4 应用拦截器 vs 网络拦截器
概念:OkHttp 有两类用户可添加的拦截器——应用拦截器(addInterceptor)在链的最外层,网络拦截器(addNetworkInterceptor)在 ConnectInterceptor 和 CallServerInterceptor 之间,仅在实际建连、发请求时才会走到。二者添加方式不同,执行时机和看到的请求/响应范围也不同。
对比表
| 维度 | addInterceptor(应用拦截器) | addNetworkInterceptor(网络拦截器) |
|---|---|---|
| 添加方式 | client.newBuilder().addInterceptor(interceptor).build() | client.newBuilder().addNetworkInterceptor(interceptor).build() |
| 在链中的位置 | 最外层,第一个执行 | 在 Connect 与 CallServer 之间,仅走网络时执行 |
| 执行时机 | 每次 Call 都会执行;即使命中缓存、未发真实请求也会执行 | 仅真正建立连接、发网络请求时执行;命中缓存或重定向前的请求不会经过 |
| 看到的请求 | 业务发起的“最终”请求(可能已被重试/重定向后的那一笔) | 每次真实发往网络的请求,包括重定向产生的中间请求,都可看到 |
| 看到的响应 | 最终返回给调用方的那一次响应 | 每次网络往返的响应(如 302 的响应、最终 200 的响应) |
| 能否拿到 Connection | 否,Chain 里拿不到底层连接 | 可以,chain.connection() 可拿到当前 RealConnection |
| 典型用途 | 统一加 Header/Token、全局日志、加解密、统计“一次业务请求” | 网络层日志(看真实请求数)、连接级重试、网络监控、抓包调试 |
执行时机举例
- 若服务端返回 304 Not Modified:CacheInterceptor 命中缓存,不会往下走到 Connect/CallServer。此时应用拦截器会执行(看到一次请求、一次 304 或缓存响应),网络拦截器不会执行。
- 若发生 302 重定向:会发两次真实请求(第一次 302,第二次 GET 新 URL)。应用拦截器通常只看到“最终”的那次请求与响应;网络拦截器会看到两次请求、两次响应。
选型建议
- 需要每次 Call 都做的逻辑(鉴权、公共头、全链路日志、按“请求次数”统计)→ 用 应用拦截器。
- 需要只对真实网络往返做逻辑(看真实建连、真实请求条数、连接级重试、网络监控)→ 用 网络拦截器。
说明:自定义拦截器唯一必须做的是实现 Interceptor 并调用一次 chain.proceed(request)。下面为常见用法概览,按需选用。
6.5 常见用法概览(按需选用)
| 用途 | 做法简述 |
|---|---|
| 加公共头 / Token | 应用拦截器里 request.newBuilder().header(...).build() 后 chain.proceed(newRequest);401 刷新用 Authenticator |
| 请求日志 | HttpLoggingInterceptor,Level 选 NONE/BASIC/HEADERS/BODY,生产建议 NONE 或 BASIC |
| 重试 | 拦截器内 try-catch proceed(),对可重试异常退避重试,非幂等慎用 |
| 缓存 | Builder.cache(new Cache(dir, size)),由 CacheInterceptor 自动处理;单请求可用 CacheControl.FORCE_NETWORK / FORCE_CACHE |
| 加解密 | 替换 request.body() / response.body() 为加密/解密后的 RequestBody / ResponseBody,body 只能消费一次 |
6.6 拦截器性能
核心结论:每条请求都会同步走过整条拦截器链,拦截器里的耗时直接叠加在请求延迟上,高 QPS 下影响更明显,因此拦截器内逻辑要尽量轻。
(1)执行模型
chain.proceed()是同步调用,请求从上往下走完链、响应从下往上返回,每一层在请求前和响应后各执行一次。- 某一层做重 CPU/IO 会阻塞该次 Call 的线程;应用拦截器连缓存命中也会执行,更需控制耗时;网络拦截器仅在真实发请求时执行,但若在回调里做重活(如主线程写库)同样会拖慢整体。
(2)应避免的操作
| 类型 | 不推荐 | 建议 |
|---|---|---|
| 重 CPU | 大 JSON 解析、复杂加解密、大文本正则 | 放到业务层或子线程,必要时再做 |
| 重 IO | 同步写文件、读大文件、拦截器内再发请求 | 日志异步/缓冲,避免每条请求刷盘 |
| 大对象 | response.body().string() 后再复制、拼接 | 流式处理或采样,避免整 body 进内存 |
| 主线程 | Callback 里做 DB/文件/网络 | 切到子线程或协程,避免 ANR |
(3)日志与耗时统计
- 日志:生产关掉或
Level.NONE/BASIC,仅 DEBUG/灰度开 BODY;避免每条请求打大 Body/大 Header。 - 耗时:打点用
nanoTime()差值即可,上报异步(单独线程/队列),不在请求路径上写库或发网络。
(4)最佳实践
- 拦截器内只做:改 Header、读少量信息、轻量判断;重逻辑优先放业务层或子线程。
- 必须做加解密等重逻辑时:用异步或流式,避免一次性把 body 全读进内存。
- 应用拦截器因缓存命中也会走,要更轻;网络拦截器可做网络统计,仍须控制耗时与内存。
面试要点:链同步执行、拦截器要轻;避免重 CPU/IO/大对象;日志生产关或降级、耗时异步上报。
第七章:OkHttp 连接池(ConnectionPool)
7.1 作用
核心:复用 TCP 连接,减少握手/挥手次数,降低延迟、提高吞吐。
为什么需要:每次新建连接都要 TCP 三次握手(HTTPS 还有 TLS 握手),开销大。把用过的连接保留下来给同目标请求复用,可少建连、少挥手。
做什么:ConnectionPool 维护一批空闲的 RealConnection(即 TCP 连接)。新请求来时先查池里是否有同一目标且可用的连接,有则直接用,无则新建;请求结束后连接放回池而非立即关闭,供后续复用。配合 HTTP/2 时同一条连接还可多路复用多个请求,建连次数更少。
7.2 工作原理
(1)Route 与“同一目标”
连接池按 Route 判断两条请求能不能共用一条连接。Route 可以理解为「访问目标」:host + port + 协议(如 HTTP/1.1 或 HTTP/2)+ 代理。只有这四者都相同,才可能复用同一条连接。
- 例:请求 A 访问
https://api.example.com:443、直连;请求 B 也访问https://api.example.com:443、直连 → Route 相同,可复用。 - 例:请求 A 走代理、请求 B 直连,或 host/port 不同 → Route 不同,不能复用,需要不同连接。
(2)取连接(ConnectInterceptor 要连接时)
- ConnectInterceptor 根据当前请求得到对应的 Route,向 ConnectionPool 要连接。
- 连接池在空闲连接里查找:是否有同 Route 且健康的连接(未关闭、可写)。
- 若有:取出该连接交给本次请求使用,不再新建连接。
- 若无:新建 RealConnection(TCP 建连,HTTPS 还要 TLS 握手),建好后可放入池,再交给本次请求使用。
(3)用完后放回与清理
- 请求结束后,若该连接可复用(如 HTTP/1.1 的 Keep-Alive 或 HTTP/2 多路复用),不会立刻关闭,而是放回连接池,并记录空闲开始时间。
- 池内有清理逻辑(定时或触发时执行):
- 某条连接空闲超过 keepAlive 时间(如 5 分钟)→ 从池中移除并关闭。
- 池里空闲连接数超过 maxIdle(如 5 个)→ 多出来的空闲连接被移除并关闭。
这样既避免连接长期占用不释放,又能在高并发下控制池大小。
7.3 配置
ConnectionPool pool = new ConnectionPool(10, 5, TimeUnit.MINUTES);
OkHttpClient client = new OkHttpClient.Builder().connectionPool(pool).build();
参数:
- 第一个参数:最大空闲连接数(默认 5);
- 第二个参数:空闲连接保持时间(默认 5 分钟),超时会被清理。
不同场景建议:
| 场景 | 建议 |
|---|---|
| 小应用、请求少 | (5, 5, TimeUnit.MINUTES) 默认即可 |
| 中等并发、多 host | (10, 5, TimeUnit.MINUTES) |
| 高并发、长连接多 | (20, 5, TimeUnit.MINUTES) 或适当增大 |
注意:过大占文件描述符和内存;过小可能频繁建连;建议用单例 OkHttpClient 共享同一连接池。
7.4 连接生命周期
一条连接从创建到关闭,大致经历以下阶段:
| 阶段 | 说明 |
|---|---|
| 创建 | 没有可复用的连接时,新建 RealConnection,完成 TCP 建连(HTTPS 还有 TLS 握手)。 |
| 使用 | 连接被某次请求占用,通过该连接发送请求、接收响应。 |
| 放回池 | 请求结束后,若支持复用(如 Keep-Alive、HTTP/2),连接不关闭,而是放入连接池并标记为空闲,同时记录空闲开始时间(内部如 idleAtNanos)。 |
| 再次复用 | 后续有同 Route 的请求时,从池中取出该连接直接使用,无需再建连。 |
| 被清理关闭 | 空闲时间超过 keepAlive,或池内空闲连接数超过 maxIdle 时,清理任务会从池中移除该连接并关闭 Socket,释放资源。 |
整体流程:创建 → 使用 → 放回池 →(再次复用 或 超时/超数被清理关闭)。清理逻辑由 ConnectionPool 内部的定时/触发任务执行。
7.5 连接复用条件
一条连接能被本次请求复用,需要同时满足:
- Route 一致:即目标相同——host、port、协议(如都 HTTP/2)、代理 都与当前请求一致(见 7.2 的 Route 说明)。
- 连接健康:连接未被关闭,且底层 Socket 仍可写(未被对端关闭或异常)。
HTTP/1.1:同一连接在某一时刻只能承载一个请求-响应对,用完放回池后,下一个同 Route 的请求可以再复用这条连接。
HTTP/2:同一连接上可以多路复用——多个请求同时在这条连接上收发,用 Stream ID 区分,因此一条连接可同时服务多个请求,复用率更高。
7.6 清理机制
连接池通过后台清理任务(定时或在一定时机触发)维持池大小和连接有效性,主要两条规则:
| 规则 | 含义 | 目的 |
|---|---|---|
| 按空闲时间清理 | 某条连接空闲超过 keepAlive 时间(如 5 分钟)→ 从池中移除并关闭。 | 避免长时间不用的连接一直占用文件描述符和内存。 |
| 按空闲数量清理 | 池中空闲连接数超过 maxIdle(如 5 个)→ 将空闲最久的连接移除并关闭,直到数量不超过 maxIdle。 | 高并发后连接变多时,限制池大小,防止资源占用过多。 |
“空闲”指连接已放回池、当前没有被任何请求使用。清理只针对空闲连接,正在使用的连接不会被强制关闭。
7.7 监控
需要观察连接池状态时,可以用以下方式:
- 连接池 API:
client.connectionPool().idleConnectionCount()可获取当前空闲连接数,便于排查“连接是否被复用、池里有多少空闲连接”。 - EventListener:在构建 OkHttpClient 时设置
EventListener,在 connectionAcquired(从池取出或新建连接时)、connectionReleased(请求结束、连接放回池时)等回调里做简单统计或日志,即可观察连接的获取与释放情况。
适合做简单监控和问题排查,不必深入源码。
7.8 性能优化建议
| 建议 | 说明 |
|---|---|
| 单例 OkHttpClient | 整个 App 或同一网络层共用同一个 OkHttpClient,从而共用同一个连接池,连接复用率更高。 |
| 启用 HTTP/2 | 与服务器协商使用 HTTP/2 后,同一条连接可多路复用多个请求,进一步减少建连次数。 |
| 按场景调大连接池 | 请求多、同一 host 并发高时,可适当增大 maxIdle 和 keepAlive(见 7.3),减少频繁建连与清理;不宜过大,避免占用过多文件描述符和内存。 |
| 配合缓存 | 合理使用 OkHttp 缓存(CacheInterceptor),减少重复请求,间接降低建连与请求次数。 |
第八章:OkHttp 缓存机制
8.1 机制概览
OkHttp 的缓存遵循 HTTP 缓存标准(RFC 7234):根据服务端返回的响应头决定是否缓存、何时过期,下次请求时先看缓存再决定是否发网络请求,由拦截器链里的 CacheInterceptor 统一完成读缓存、写缓存、发条件请求,对业务透明。
两种缓存方式:
| 类型 | 依据 | 行为简述 |
|---|---|---|
| 强制缓存 | 服务端通过 Cache-Control(如 max-age=3600)或 Expires 告诉客户端“多久内可直接用缓存” | 在有效期内再次请求同一 URL 时,不发请求,直接返回本地缓存;过期后才可能向服务器验证。 |
| 协商缓存 | 服务端返回 Last-Modified 或 ETag,再次请求时客户端带 If-Modified-Since / If-None-Match | 服务器比较后若资源未变则返回 304 Not Modified,客户端用缓存;若已变则返回 200 和新内容,并更新缓存。 |
CacheInterceptor 在链中的角色:位于 Bridge 与 Connect 之间;请求来时先查缓存(有且未过期则直接返回);未命中或需验证时往下走发请求;收到响应后按响应头决定是否写入缓存,并在需要时发带条件的请求(如 If-None-Match)做协商。具体写入/读取/304 逻辑见 8.3~8.5。
缓存的优势:
| 优势 | 说明 |
|---|---|
| 减少请求、省流量 | 命中强制缓存时不发请求;协商缓存 304 时只传少量头、不传 body,节省带宽。 |
| 加快响应 | 本地读缓存比网络快,弱网或高延迟时体验更好。 |
| 减轻服务端压力 | 重复请求由客户端缓存承担,降低 QPS。 |
| 离线可用 | 已缓存资源在无网或超时时仍可返回(需业务处理 504 等)。 |
一级缓存(OkHttp 磁盘缓存)的大小与格式:
| 项目 | 说明 |
|---|---|
| 大小 | 由 new Cache(File dir, long maxSize) 的 maxSize 指定,单位字节(如 10 * 1024 * 1024 即 10MB)。超出时按 LRU 淘汰旧条目。 |
| 存储实现 | 底层为 DiskLruCache,与 OkHttp 同源的磁盘 LRU 缓存实现。 |
| 目录与文件 | 指定目录下会有 journal 文件(记录 key、状态)和若干数据文件;每条缓存对应 key(通常与请求 URL 等有关),响应头与体分开存储。 |
| key 规则 | 默认由 Request 的 URL 等生成唯一 key,同一 URL 对应同一缓存条目。 |
8.2 配置
基本写法:
// dir:缓存根目录;maxSize:最大占用字节数,超出后 LRU 淘汰
File cacheDir = new File(context.getCacheDir(), "http-cache");
long maxSize = 10 * 1024 * 1024; // 10MB
Cache cache = new Cache(cacheDir, maxSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
参数说明:
| 参数 | 含义 | 建议 |
|---|---|---|
| dir | 缓存文件所在目录,需可写,建议用应用私有目录 | Android 用 context.getCacheDir() 或其子目录(如 "http-cache"),避免用外部存储以免被用户清理影响 |
| maxSize | 缓存总大小上限(字节),超出时按 LRU 淘汰 | 按业务量设定,常见 10MB~50MB;过小易淘汰频繁,过大占磁盘 |
注意:同一目录只应被一个 Cache 实例使用,多实例共用同一 dir 可能损坏缓存;建议单例 OkHttpClient 时只建一个 Cache。
8.3 工作原理
总览:请求进入 CacheInterceptor 后,先查本地缓存;根据是否有缓存、是否过期、是否支持协商,分三种情况处理;需要发网络请求时,收到响应后再根据响应头决定是否写回缓存。整体是「先读、再决定是否请求、最后写」的流程。
(1)请求到达后:先查缓存,再分支
请求进入 CacheInterceptor
↓
用 Request 的 key(通常 URL)查本地缓存
↓
┌─────┴─────┬─────────────────┬──────────────────────┐
↓ ↓ ↓ ↓
有缓存 有缓存 无缓存 请求带了
且未过期 但已过期 或要求 FORCE_NETWORK
(强制缓存) 且支持协商 强制走网络
↓ ↓ ↓ ↓
直接返回 发条件请求 发普通请求 发普通请求
缓存 Response (If-Modified-Since 收到响应后 收到响应后
不发请求 /If-None-Match) 按头决定是否写缓存 按头决定是否写缓存
↓
304 → 用缓存+更新元数据
200 → 用新响应+写回缓存
(2)三种分支说明
| 分支 | 条件 | CacheInterceptor 行为 |
|---|---|---|
| 直接命中 | 有缓存,且未过期(max-age/Expires 内) | 直接返回缓存的 Response,不发网络请求,后续拦截器不再执行。 |
| 协商验证 | 有缓存但已过期,且缓存中有 Last-Modified 或 ETag | 发带 If-Modified-Since / If-None-Match 的请求;304 则用本地缓存内容并更新元数据;200 则用新响应并写回缓存。 |
| 走网络 | 无缓存、或请求带 FORCE_NETWORK、或不可缓存 | 正常发请求,收到响应后根据响应头决定是否写入缓存(见下文「写回缓存」)。 |
(3)写回缓存(收到网络响应后)
- 只有从网络拿到响应时才会执行写缓存(直接命中时没有网络响应,不会写)。
- 判断逻辑:若响应头没有 no-store,且存在 Cache-Control: max-age / Expires 等可缓存指示,则用 Request 的 key 将响应头与响应体写入 DiskLruCache。
- no-store:不写入,且已有同 key 缓存也可能被忽略或不再使用。
- no-cache:可以写入,但下次请求该 URL 时会先发条件请求验证(协商缓存),不会直接命中。
(4)注意
- no-store:响应不缓存,请求也不读已有缓存(除非请求方强制 FORCE_CACHE)。
- no-cache:响应可缓存,但每次使用前都要向服务器验证(走协商缓存)。
8.4 强制缓存
含义:服务端通过响应头告诉客户端“在某一时间段或某一时间点前,可以直接使用本地缓存,不用发请求”。OkHttp 的 CacheInterceptor 会解析这些头并自动决定是否直接返回缓存。
常见响应头:
| 响应头 | 含义 | 示例 |
|---|---|---|
| Cache-Control: max-age=N | 缓存 N 秒内有效,N 秒内再次请求同一 URL 可直接用缓存 | max-age=3600 表示 1 小时内可用缓存 |
| Cache-Control: s-maxage | 同上,针对共享缓存(如 CDN),OkHttp 作为私有缓存一般也参考 | - |
| Expires | 在该日期时间点前可直接用缓存(HTTP 日期格式) | 若时钟不准可能影响判断,优先看 Cache-Control |
客户端强制走缓存:若希望某次请求只读缓存、不发网络(如离线场景),可对该 Request 设置 CacheControl.FORCE_CACHE;若没有可用缓存则会得到 504 Unsatisfiable Request,需业务处理。
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_CACHE)
.build();
8.5 协商缓存
含义:首次请求时服务端返回 Last-Modified 或 ETag,客户端缓存;再次请求时客户端带上 If-Modified-Since 或 If-None-Match,服务端比较后若资源未变则返回 304,客户端用本地缓存,从而省去传输 body。
流程简述:
- 首次请求:服务端返回 200 和 Last-Modified / ETag,OkHttp 将响应与这两个头一起写入缓存。
- 再次请求:CacheInterceptor 从缓存中取出上次的 Last-Modified/ETag,在请求头中带上 If-Modified-Since / If-None-Match 发往服务端。
- 服务端未修改:返回 304 Not Modified(无 body 或 body 为空),OkHttp 用本地缓存内容拼出 Response 并更新缓存元数据。
- 服务端已修改:返回 200 和新 body,OkHttp 用新响应更新缓存。
ETag 与 Last-Modified 对比:
| 类型 | 说明 | 特点 |
|---|---|---|
| ETag | 资源内容的哈希或版本标识 | 更精确,内容一变 ETag 就变;可避免时间戳精度问题 |
| Last-Modified | 资源最后修改时间(GMT) | 实现简单;秒级精度,1 秒内多次修改可能区分不了 |
两者可同时存在,由服务端决定用哪一个做 304 判断;OkHttp 会按规范带上对应请求头。
8.6 清除缓存
(1)清空 OkHttp 缓存(所有条目)
if (client.cache() != null) {
client.cache().evictAll();
}
- 会删除该 Cache 目录下所有已缓存条目,下次请求将重新走网络并可能再次写入。
- 适用场景:用户主动“清除缓存”、版本升级后希望强制刷新等;不宜在每次请求前调用,否则缓存失去意义。
(2)单次请求强制走网络(跳过读缓存)
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
- 本次请求不使用本地缓存,直接发网络;收到响应后仍可能写入缓存(由响应头决定)。
- 适用场景:下拉刷新、必须拿最新数据时。
(3)直接删除缓存目录
File cacheDir = new File(context.getCacheDir(), "http-cache");
if (cacheDir.exists() && cacheDir.isDirectory()) {
for (File f : cacheDir.listFiles()) f.delete(); // 简单示例,实际可递归删除
}
- 直接删文件后,OkHttp 的 Cache 实例若仍在使用该目录,可能出现不一致;建议先不再使用该 Cache(如重建 OkHttpClient),再删目录,或直接使用 evictAll() 更安全。
注意:evictAll 会清空所有已缓存响应;按需在“清除缓存”或缓存过大时使用,避免频繁清空影响性能。
8.7 存储位置
指定方式:new Cache(File dir, long maxSize) 的第一个参数 dir 即为缓存根目录,所有缓存文件(含 DiskLruCache 的 journal 与数据文件)都在该目录下。
常见写法:
| 场景 | 示例 |
|---|---|
| Android 应用私有目录 | new File(context.getCacheDir(), "http-cache"),无需权限、卸载时随应用删除 |
| 子目录隔离 | 用 "okhttp-cache" 等子目录名,避免与其它缓存混在一起 |
注意:目录需存在且可写;通常由应用在首次使用前创建,或使用 getCacheDir() 已存在的目录。不要与其它库或多个 Cache 实例共用同一 dir。
8.8 自定义策略
“自定义策略”指什么:默认情况下,OkHttp 完全按服务端响应头(如 Cache-Control、Expires)决定要不要用缓存、要不要发请求。自定义策略就是:对某次或某类请求,由你来指定“必须走网络”或“只用缓存”等,而不是完全听服务端的。
什么时候用:例如——下拉刷新时要强制拿最新数据(不想用旧缓存);离线时某次请求只想读缓存(不想发网络);或希望对某几个接口统一“总是走网络”。这时就需要在请求上或拦截器里加上对应的策略。
四种做法(按需求选一种用,不是步骤):
| 你想达到的效果 | 用哪种做法 | 怎么做 |
|---|---|---|
| 某一次请求:别用缓存,直接发网络(如下拉刷新) | 方式一 | 该请求加 cacheControl(CacheControl.FORCE_NETWORK) |
| 某一次请求:只用缓存,不发网络(没缓存就 504) | 方式一 | 该请求加 cacheControl(CacheControl.FORCE_CACHE) |
| 某一次请求:每次都先问服务端(协商缓存,不用过期缓存) | 方式二 | 该请求加自定义 CacheControl,如 noCache() 或 maxAge(0, ...) |
| 某几类请求(如某域名/某 URL):统一强制网络或只用缓存 | 方式三 | 在拦截器里根据 URL 给 request 加 cacheControl(...) |
| 想改“下次这个 URL 的缓存怎么用”(改响应头再缓存) | 方式四 | 在拦截器里改 Response 的 Cache-Control 再返回(慎用) |
方式一:单次请求“强制网络”或“只用缓存”
在建这个 Request 的时候加上 cacheControl(...),只影响这一次请求:
// 这次请求:不用缓存,直接发网络(适合下拉刷新)
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
// 这次请求:只用缓存,不发网络;没有缓存会 504,要自己处理
Request request = new Request.Builder()
.url(url)
.cacheControl(CacheControl.FORCE_CACHE)
.build();
方式二:单次请求“每次都要问服务端”
例如希望这次请求即使用到缓存,也要先向服务端发一次验证(协商),不直接用过期缓存:
CacheControl custom = new CacheControl.Builder()
.maxAge(0, TimeUnit.SECONDS)
.noCache()
.build();
Request request = new Request.Builder()
.url(url)
.cacheControl(custom)
.build();
方式三:对某类请求统一加策略(用拦截器)
不想每个 Request 都手写,可以在拦截器里根据 URL 或其它条件,统一给某类请求加上“强制网络”或“只用缓存”:
// 例如:某几个接口统一强制走网络
if (url.contains("/api/live/")) {
request = request.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build();
}
return chain.proceed(request);
方式四:改响应头再缓存(少用)
在拦截器里拿到 Response 后,改掉其中的 Cache-Control 再 return,会影响被缓存下来的内容,从而影响之后同一 URL 的缓存行为。一般不建议改服务端头,除非有明确需求。
第九章:OkHttp Cookie 管理
9.0 Cookie 的作用(为什么需要管理)
Cookie 是什么
服务端用响应头 Set-Cookie 把一小段数据(如 sessionId、token)发给客户端;客户端在之后的请求里用请求头 Cookie 再带回去。这样服务端就能在无状态的 HTTP 上认出“同一个用户/同一场会话”。
常见作用:
| 作用 | 说明 |
|---|---|
| 会话保持 | 服务端把 sessionId 放进 Cookie,后续请求带上,就能保持登录、购物车等状态。 |
| 登录态 | 登录成功后 Set-Cookie 下发 token/sessionId,之后请求自动带上,不用每次手写。 |
| 跨请求带状态 | 同域名下多个请求都带同一份 Cookie,服务端可做权限、风控等。 |
OkHttp 里为什么要“管理”
HTTP 不会自动帮你存 Cookie。第一次拿到 Set-Cookie 后,你不存、下次也不带,服务端就当你是个新用户。所以需要:响应时把 Cookie 存起来,发请求时按 URL 把该带的 Cookie 带上。OkHttp 用 CookieJar 把“存”和“取”交给你实现;你实现并设置后,OkHttp 会在发请求前调你取、收响应后调你存,实现自动携带。
9.1 如何管理 Cookie(CookieJar 接口与流程)
一句话:实现 CookieJar 两个方法——响应时存(saveFromResponse),请求前取(loadForRequest);把实例传给 OkHttpClient.Builder().cookieJar(...)。默认是 NO_COOKIES(不存也不带)。
接口:
public interface CookieJar {
void saveFromResponse(HttpUrl url, List<Cookie> cookies); // 响应里有 Set-Cookie 时,OkHttp 调这个把 Cookie 交给你存
List<Cookie> loadForRequest(HttpUrl url); // 发请求前 OkHttp 调这个,你返回该 URL 该带的 Cookie 列表
}
谁在什么时候调用:
| 时机 | 谁调用 | 你做什么 |
|---|---|---|
| 发请求前 | OkHttp 调 loadForRequest(url) | 按 url(一般按 host)从你的存储里取出该带的 Cookie 列表返回;OkHttp 会自己拼成 Cookie 头加到请求上。 |
| 收到响应后 | OkHttp 解析 Set-Cookie,调 saveFromResponse(url, cookies) | 把这份 Cookie 列表存进你的存储(内存 Map 或持久化),下次 loadForRequest 能按 url 取到。 |
一个最简单的实现(内存、按 host 存):
public class MyCookieJar implements CookieJar {
private final Map<String, List<Cookie>> store = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookies != null && !cookies.isEmpty())
store.put(url.host(), cookies);
}
@Override
public List<Cookie> loadForRequest(HttpUrl url) {
return store.getOrDefault(url.host(), Collections.emptyList());
}
}
OkHttpClient client = new OkHttpClient.Builder()
.cookieJar(new MyCookieJar())
.build();
注意:示例按 host 存,同一 host 会覆盖;要做 path/域名细分需自己设计 key。持久化时在 save/load 里用 SharedPreferences 或数据库,并过滤过期(cookie.expiresAt())。
9.2 设置 Cookie(怎么把 Cookie 带到请求里)
两种方式:
| 方式 | 做法 | 什么时候用 |
|---|---|---|
| 手动 | 建 Request 时加 header("Cookie", "name=value; name2=value2") | 只这一次请求要带、临时几个固定值、不打算存 |
| CookieJar | 实现 CookieJar(见 9.1),在 loadForRequest 里按 url 返回 Cookie 列表 | 要自动带、会话/登录态、多次请求都带 |
手动示例:Request.Builder().url(url).header("Cookie", "sessionId=abc123").build()。
用 CookieJar 时:你不用在 Request 上写 Cookie;OkHttp 发请求前会调你的 loadForRequest,把你返回的 List 拼成 Cookie 头并加到请求上。
9.3 获取 Cookie(怎么拿到 Cookie)
- 从本次响应拿:
response.headers("Set-Cookie")拿 Set-Cookie 原始字符串;用Cookie.parse(url, setCookieHeader)可转成Cookie对象,取 name、value、expiresAt 等。 - 从你存的 CookieJar 拿:Cookie 在你自己的 Map/DB 里,按 host 查即可;或在 CookieJar 实现里对外提供方法(如
getCookiesForHost(host))给业务用。
9.4 清除 Cookie
Cookie 存在你的 CookieJar 实现里(Map 或 SharedPreferences/数据库),清除 = 在你自己的存储里删数据。
| 想清什么 | 怎么做 |
|---|---|
| 某域名 | Map:remove(host);DB:delete where host = ? |
| 全部 | Map:clear();DB/SP:删掉所有 Cookie 数据 |
| 某域名下某个 name | 读出该 host 的 List,去掉对应 name 再写回;或 DB:delete where host = ? and name = ? |
清掉后,下次 loadForRequest 就不会再返回这些 Cookie,请求里自然就不带了。
9.5 CookieJar 和“手动加 Cookie”的区别
| 手动加 Cookie 头 | CookieJar | |
|---|---|---|
| 管多少 | 只影响你手写的那一次 Request | 所有通过该 OkHttpClient 的请求都可自动带 |
| 存不存 | 不存,每次自己写 | 可存(内存或持久化),按 URL 自动取 |
| 适用 | 单次、临时 | 登录态、会话、同域名多请求 |
9.6 自定义 CookieJar(内存 vs 持久化)
你要做的:实现 saveFromResponse 和 loadForRequest,存到哪里自己定。
| 存法 | 做法 | 特点 |
|---|---|---|
| 内存 | 用 Map<String, List<Cookie>>,key 用 host,见 9.1 示例 | 简单,进程内有效;进程结束就没了 |
| 持久化 | save 时把 List 按 host 存进 SharedPreferences(JSON)或数据库(host/name/value/expiresAt 等);load 时按 url 读出并过滤过期再返回 | App 重启后还在,适合“登录一次,下次打开仍登录” |
注意:同一 host 可能多条 Cookie,用 List;要通用一点要考虑 domain、path、过期;多线程用 ConcurrentHashMap 或加锁。
9.7 持久化存储(简要)
目的:Cookie 写磁盘/数据库,App 重启后还能用,用户不用重复登录。
- SharedPreferences:按 host 存,把
List<Cookie>转 JSON 写入;读时反序列化并过滤过期。数据多时建议用数据库。 - 数据库:表里存 host、name、value、expiresAt、path、domain 等;save 时 insert/update,load 时按 host 查并去掉过期。方便按条件删、查。
加载时一定要过滤过期(expiresAt < now);Session Cookie 无过期时要自己定策略。
第三部分:Retrofit 基础
第十章:Retrofit 概述
10.1 Retrofit 是什么?作用是什么?
定义
Retrofit 是 Square 出的、基于 OkHttp 的 HTTP 客户端库。它用 Java/Kotlin 接口 + 注解 来描述“请求哪个 URL、什么方法、带什么参数”,调用接口方法时由 Retrofit 自动拼出 Request、交给 OkHttp 发请求,并把响应体通过 Converter 转成你声明的类型(如 User、List<User>),实现类型安全的 API 调用。
和“手写 OkHttp”的区别
手写 OkHttp 时你要自己 Request.Builder().url(...).post(body).build()、自己解析 response.body().string() 成对象。Retrofit 让你只写接口(方法返回值、参数、注解),拼 Request 和解析 Response 由库完成,代码更短、出错更少。
作用概括
| 作用 | 说明 |
|---|---|
| 简化调用 | 接口即文档,调用方直接 api.getUser(1),不用关心 URL 拼接、请求体格式 |
| 类型安全 | 返回 Call<User>、Observable<User> 等,编译期能检查类型,运行时少出错 |
| 可插拔 | 通过 Converter 支持 JSON/XML 等,通过 CallAdapter 支持 Call/RxJava/协程,按项目选 |
| 复用 OkHttp 能力 | 底层 OkHttp,连接池、缓存、拦截器、超时等都可沿用 |
10.2 特点与优势
| 维度 | 说明 |
|---|---|
| 类型安全 | 接口方法有明确返回类型和参数类型,编译期检查,减少运行时错误;手写 URL/解析易拼错、类型不匹配。 |
| 注解驱动 | 用 @GET、@POST、@Path、@Query、@Body 等描述 API,代码简洁、易维护、接口即文档。 |
| 自动转换 | Converter 自动把请求/响应体与对象互转(如 JSON↔Bean),无需手写 Gson.parse。 |
| 多种异步方式 | 支持 Call 回调、RxJava Observable、Kotlin suspend,按项目选型,不用自己包线程。 |
| 与 OkHttp 集成 | 底层用 OkHttp,共享连接池、缓存、拦截器,可传入自定义 OkHttpClient,统一网络配置。 |
| 易测试 | 接口可 mock,单元测试直接打桩;配合 MockWebServer 可测真实 HTTP 行为。 |
10.3 Retrofit 和 OkHttp 的关系
一句话:Retrofit 基于 OkHttp,是 OkHttp 的上层封装;Retrofit 负责“把接口+注解变成一次 HTTP 调用”,真正发请求、建连接、走拦截器的是 OkHttp。二者是配合关系,不是二选一。
分工:
| 层级 | 谁在做 | 做什么 |
|---|---|---|
| Retrofit | 接口解析、注解处理 | 根据你的接口方法上的 @GET/@POST 等和 @Path/@Query 等,拼出 Request(URL、方法、请求体等);选 Converter 把请求/响应体转成 JSON 等;选 CallAdapter 把 Call 转成 RxJava/协程等。最后把拼好的 Request 交给 OkHttp。 |
| OkHttp | 实际网络层 | 拿 Retrofit 传下来的 Request,发真实请求:走拦截器链、连接池、缓存、超时、Cookie 等,得到 Response 再往回传。 |
如何结合:
构建 Retrofit 时可以传入 OkHttpClient(Retrofit.Builder().client(okHttpClient).build())。所以你在 OkHttp 里配的拦截器、连接池、缓存、超时、CookieJar、证书等都会在 Retrofit 发请求时生效;Retrofit 只负责“把接口调用变成 Request”,不替换 OkHttp 的能力。
总结:
- Retrofit = 面向接口的编排层(怎么把一次 API 调用变成 Request + 怎么把 Response 转成你要的类型)。
- OkHttp = 执行层(怎么发请求、怎么建连、怎么缓存、怎么带 Cookie)。
- 用 Retrofit 时底层仍然是 OkHttp,所以 OkHttp 的面试点(拦截器、连接池、缓存等)在 Retrofit 场景下一样重要。
10.4 与其他网络框架对比
对比表:
| 特性 | Retrofit | 仅 OkHttp | HttpURLConnection |
|---|---|---|---|
| 抽象层次 | 接口 + 注解,面向 API | Request/Response 构建 | 流式 API,InputStream 等 |
| 类型安全 | ✓ 返回类型、参数类型明确 | ✗ 手写 URL、手写 body | ✗ 同上 |
| 数据转换 | 自动(Converter) | 需手写 Gson 等解析 | 需手写解析 |
| 异步方式 | Call / RxJava / 协程 可插拔 | 自己 enqueue 或包线程 | 需自己开线程 |
| 底层 | OkHttp | 自身 | JDK 内置 |
| 典型用法 | 定义接口,调用方法即发请求 | 每个请求 new Request、newCall | 每请求 openConnection、getInputStream |
小结:Retrofit 在 OkHttp 之上加了“接口抽象 + 类型 + 自动转换”,适合做 App 的 API 层;只用 OkHttp 更灵活但要自己拼请求和解析;HttpURLConnection 是 JDK 自带,无依赖但写法繁琐,一般只在不能引入第三方库时考虑。
10.5 如何添加 Retrofit 依赖
Gradle(Module 级 build.gradle):
dependencies {
// 核心库(必选)
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
// 常用:JSON 转换(选一个即可,Gson 最常见)
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 可选:RxJava 支持
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
// 可选:Kotlin 协程(Kotlin 项目可用 2.6+ 或社区 adapter)
}
依赖说明:
| 依赖 | 作用 |
|---|---|
| retrofit | 核心库,接口解析、Request 拼装、与 OkHttp 对接;会传递依赖 OkHttp。 |
| converter-gson | 用 Gson 把请求体/响应体与对象互转(JSON↔Bean);需同时加 Gson 依赖。 |
| converter-jackson / converter-moshi | 同上,换用 Jackson 或 Moshi 做 JSON。 |
| adapter-rxjava3 | 接口方法可返回 Observable<T> 等,与 RxJava 配合。 |
| adapter-rxjava2 | 对应 RxJava 2,与 RxJava 3 二选一。 |
注意:Retrofit 与 OkHttp 版本无强绑定,但建议都用较新稳定版;若工程里已有 OkHttp,Retrofit 会使用其传递依赖,注意版本统一避免冲突。
10.6 架构设计
总览:你写的是接口,调用的是 api.getUser(1) 这样的方法;Retrofit 在背后把这次调用变成一次 OkHttp 请求,再把响应转成你声明的类型返回。整条链路可以分成“请求前(拼 Request)”和“请求后(转 Response、适配返回类型)”两段,中间是 OkHttp 真正发请求。
请求前(从方法调用到 OkHttp Request):
api.getUser(1) 调用
↓
① 动态代理:InvocationHandler 接住,得到方法 getUser、参数 [1]
↓
② ServiceMethod:根据方法上的 @GET("users/{id}")、@Path("id") 等解析
→ 得到 URL、HTTP 方法、请求体类型、响应体类型
→ 用参数 1 填进 path,拼出 OkHttp 的 Request
↓
③ Converter(请求体):若方法有 @Body User user,用 Gson 等把 user 转成 RequestBody,塞进 Request
↓
④ CallAdapter:根据方法返回类型(Call / Observable / suspend)
→ 创建 OkHttpCall(包装这次 Request),再适配成 Call<T> 或 Observable<T> 等返回给你
↓
⑤ OkHttpCall 内部:把 Request 交给 OkHttp 发出去(进入 OkHttp 拦截器链、连接池等)
请求后(从网络响应到方法返回值):
OkHttp 拿到 Response(ResponseBody)
↓
① Converter(响应体):把 ResponseBody 转成你声明的类型 T(如 User),例如 Gson 解析 JSON → User
↓
② CallAdapter:把 T 包装进 Call<T> / Observable<T> / suspend 的返回值,交回给调用方
分阶段表(面试可按表说):
| 阶段 | 组件 | 做的事 |
|---|---|---|
| 接住调用 | 动态代理(InvocationHandler) | 你调用接口方法时,实际进代理;拿到方法 + 参数,交给 Retrofit。 |
| 解析注解 | ServiceMethod | 根据 @GET/@POST、@Path/@Query/@Body 等,拼出 URL、HTTP 方法、Request;参数值填进 path/query/body。 |
| 请求体转换 | Converter | 把 @Body 参数对象 转成 RequestBody(如对象→JSON 字符串→RequestBody)。 |
| 返回类型适配 | CallAdapter | 把内部的 OkHttpCall 适配成你写的 Call<T> / Observable<T> / suspend 返回值。 |
| 发请求 | OkHttp | 拿 Request 真正发 HTTP 请求,走拦截器、连接池;得到 Response。 |
| 响应体转换 | Converter | 把 ResponseBody 转成 T(如 JSON 字符串→User)。 |
一句话总结:动态代理接住方法调用 → ServiceMethod 解析注解拼 Request → Converter 转请求体 → CallAdapter 包装成 Call/Observable 等 → OkHttp 发请求 → 响应再经 Converter 转成 T → CallAdapter 交回给你。
设计模式
Retrofit 通过动态代理和与 OkHttp 配合的责任链(拦截器链)等设计,实现“接口即 API”的简洁用法。常见设计模式如下:
| 设计模式 | 应用位置 | 说明 |
|---|---|---|
| 动态代理 | 创建 API 接口实现 | 你只定义接口,Retrofit 用 Proxy.newProxyInstance 生成实现类;调用 api.getUser(1) 时进入 InvocationHandler,拿到方法名和参数,再交给 ServiceMethod 拼 Request,从而无需手写实现类。 |
| 建造者模式 | Retrofit.Builder | 通过 new Retrofit.Builder().baseUrl(...).addConverterFactory(...).client(...).build() 链式配置 baseUrl、Converter、CallAdapter、OkHttpClient 等,最后统一 build 出 Retrofit 实例,避免构造参数过多。 |
| 适配器模式 | CallAdapter | 内部真实执行的是 OkHttpCall,但接口方法返回的是 Call<T>、Observable<T> 或 Kotlin suspend 等不同形式;CallAdapter 把 OkHttpCall 适配成调用方期望的返回类型,便于与 RxJava、协程等结合。 |
| 策略模式 | Converter 设置 | 请求/响应体的转换策略可插拔:Gson、Jackson、Moshi 等通过 addConverterFactory 注册,Retrofit 按需选用能处理当前类型的 Converter,不同数据格式对应不同转换策略,无需改核心代码。 |
补充:底层发请求时 OkHttp 的责任链(拦截器链)仍在工作,Retrofit 只负责拼好 Request 交给 OkHttp,所以“责任链”更多体现在 OkHttp 侧;Retrofit 侧突出的是动态代理 + 建造者 + 适配器 + 策略。
10.7 支持的数据格式
机制:Retrofit 不写死数据格式,通过 Converter 把请求体/响应体与 Java 对象互转。你通过 addConverterFactory(...) 注册一种或多种 ConverterFactory,Retrofit 在需要转换时按顺序选第一个能处理该类型的 Converter。
常见格式与依赖:
| 格式 | 常用 ConverterFactory | 依赖(示例) |
|---|---|---|
| JSON | GsonConverterFactory、JacksonConverterFactory、MoshiConverterFactory | converter-gson、converter-jackson、converter-moshi |
| XML | SimpleXmlConverterFactory 等 | converter-simplexml |
| Protobuf | 自定义或社区 Converter | 自己实现 Converter.Factory |
| 字符串 / 其它 | 自定义 Converter.Factory | 自己实现 |
使用示例:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create()) // 先加 JSON,多数接口用这个
// .addConverterFactory(SomeXmlConverterFactory.create()) // 需要时再加 XML
.build();
多种 Factory 时,Retrofit 按添加顺序选择第一个能处理当前类型的 Converter;一般把最常用的(如 Gson)放前面。
10.8 版本历史
| 阶段 | 说明 |
|---|---|
| 2.x | 当前主流,接口+注解、Converter、CallAdapter 可插拔,基于 OkHttp。 |
| 2.6+ | 对 Kotlin 更友好,支持 suspend 等;可配合协程 CallAdapter。 |
| 维护 | Square 持续维护,修 bug、兼容新版本 OkHttp/Java。 |
面试可答:主流用 2.x;特点是类型安全、注解驱动、可插拔 Converter(如 Gson)和 CallAdapter(Call/RxJava/协程);底层是 OkHttp。
第十一章:Retrofit 基本使用
11.1 创建 Retrofit 实例
用 Retrofit.Builder 链式配置 baseUrl、Converter、可选 client,最后 build()。建议单例;baseUrl 末尾带 /,与相对路径拼接一致。
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient) // 可选
.build();
11.2 定义 API 接口
用 接口 + 方法注解 描述 HTTP 方法、路径、参数;返回类型为 Call<T>、Observable<T> 或 Kotlin suspend。接口无方法体,参数必须带 @Path/@Query/@Body 等注解。
// 示例 1:GET + @Path
@GET("users/{id}")
Call<User> getUser(@Path("id") long id);
// 示例 2:POST + @Body
@POST("users")
Call<User> create(@Body User user);
// 示例 3:表单 @FormUrlEncoded + @Field
@POST("login")
@FormUrlEncoded
Call<LoginResult> login(@Field("username") String user, @Field("password") String pwd);
// 示例 4:上传 @Multipart + @Part
@Multipart
@POST("upload")
Call<UploadResult> upload(@Part MultipartBody.Part file, @Part("desc") RequestBody desc);
11.3 各 HTTP 方法注解
| 注解 | 含义 | 典型用法 |
|---|---|---|
| @GET | GET | 路径可含 {id} 配 @Path;查询用 @Query/@QueryMap |
| @POST | POST | @Body 发 JSON;@FormUrlEncoded+@Field 表单;@Multipart+@Part 上传 |
| @PUT | PUT | 全量更新,用法同 POST |
| @DELETE | DELETE | 多无 body;要 body 用 @Body |
| @PATCH | PATCH | 部分更新,用法同 POST |
| @HEAD | HEAD | 只拿响应头,无 body |
| @OPTIONS | OPTIONS | 常用于 CORS 预检 |
11.4 如何调用 API
| 方式 | 说明 |
|---|---|
| 同步 | 调用 call.execute() 得 Response<T>,注意不能主线程。 |
| 异步 | 调用 call.enqueue(Callback),在 onResponse/onFailure 里处理。 |
| RxJava | 接口返回 Observable<T>,需 addCallAdapterFactory(RxJava3CallAdapterFactory.create()),再 subscribeOn/observeOn/subscribe。 |
| 协程 | 接口声明 suspend fun xxx(): T,在协程里直接 api.xxx()。 |
示例 1:同步
Call<User> call = api.getUser(123);
Response<User> resp = call.execute();
if (resp.isSuccessful()) User user = resp.body();
示例 2:异步
call.enqueue(new Callback<User>() {
@Override public void onResponse(Call<User> c, Response<User> r) {
if (r.isSuccessful()) User user = r.body();
}
@Override public void onFailure(Call<User> c, Throwable t) { }
});
示例 3:RxJava(接口返回 Observable<User>)
api.getUser(123)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> {}, error -> {});
示例 4:协程(接口 suspend fun getUser(id: Int): User)
lifecycleScope.launch {
val user = api.getUser(123)
}
第十二章:Retrofit 请求参数注解
12.1 查询参数(@Query、@QueryMap)
@Query:单个/多个拼成 query;可选参数用 Integer 等可空类型,null 不拼进 URL;同名多值用 List<>。
@QueryMap:动态多参数,key 为参数名,value 为值。
// 示例 1:多个 @Query → ?page=1&limit=10
@GET("users")
Call<List<User>> getUsers(@Query("page") int page, @Query("limit") int limit);
// 示例 2:可选参数(null 不拼)
@GET("users")
Call<List<User>> getUsers(@Query("page") Integer page);
// 示例 3:同名多值 → ?ids=1&ids=2
@GET("users")
Call<List<User>> getUsers(@Query("ids") List<Integer> ids);
// 示例 4:@QueryMap 动态参数
@GET("users")
Call<List<User>> getUsers(@QueryMap Map<String, String> params);
12.2 @Path
路径占位:URL 里写 {name},参数用 @Path("name"),名字一致。值已编码时用 @Path(value = "id", encoded = true) 避免二次编码。
@GET("users/{id}")
Call<User> getUser(@Path("id") long id);
// getUser(123) → .../users/123
@GET("users/{userId}/posts/{postId}")
Call<Post> getPost(@Path("userId") long userId, @Path("postId") long postId);
12.3 @Body
对象作请求体,由 Converter 序列化(如 JSON)。
@POST("users")
Call<User> create(@Body User user);
12.4 表单与文件(@Field/@FieldMap、@Part/@PartMap)
@Field、@FieldMap:与 @FormUrlEncoded 同用,拼表单。
@Part、@PartMap:与 @Multipart 同用,上传文件或混合表单。
// 示例 1:@Field 表单
@POST("login")
@FormUrlEncoded
Call<LoginResult> login(@Field("username") String user, @Field("password") String pwd);
// 示例 2:@FieldMap
@POST("login")
@FormUrlEncoded
Call<LoginResult> login(@FieldMap Map<String, String> fields);
// 示例 3:@Part 文件 + 字段
@Multipart
@POST("upload")
Call<UploadResult> upload(@Part MultipartBody.Part file, @Part("desc") RequestBody desc);
// 示例 4:文件 Part 构造
MultipartBody.Part part = MultipartBody.Part.createFormData("file", fileName,
RequestBody.create(file, MediaType.parse("image/jpeg")));
12.5 请求头(@Header、@HeaderMap)
单头用 @Header,多头用 @HeaderMap;同名多值可用多次 @Header 或 @Headers。
@GET("user")
Call<User> getUser(@Header("Authorization") String token);
@GET("user")
Call<User> getUser(@HeaderMap Map<String, String> headers);
12.6 @Url
动态完整 URL,覆盖 baseUrl;调用时传入完整 URL。
@GET
Call<ResponseBody> get(@Url String url);
// 调用:api.get("https://other.com/path")
12.7 注解优先级与顺序
含义:“优先级”指谁覆盖谁(如 @Url 覆盖 baseUrl);“顺序”指 Retrofit 在拼 Request 时先处理什么、后处理什么(先 URL,再 query,再 header,再 body)。理解这两点可以避免“写了多个注解却结果不对”的问题。
(1)URL 的确定顺序与优先级
| 步骤 | 说明 |
|---|---|
| 1 | 默认用 baseUrl(Builder 里设的) + 方法上的相对路径(如 @GET("users/{id}") 的 users/{id})。 |
| 2 | 若方法参数带了 @Url,则整段 URL 由 @Url 决定,不再用 baseUrl + 相对路径(@Url 优先级最高)。 |
| 3 | 在最终路径字符串里,@Path 按名字替换占位符 {xxx}:如 @Path("id") 替换 {id}。 |
所以优先级是:@Url > baseUrl + 相对路径;路径内的 @Path 只做占位符替换,不改变“用 baseUrl 还是用 @Url”的决策。
(2)Request 构建顺序(拼装的先后)
Retrofit 拼一次请求时,大致按以下顺序处理,便于记忆:
- 先确定 URL:如上,baseUrl + 相对路径(或 @Url)+ @Path 替换。
- 再拼查询参数:@Query、@QueryMap 按顺序或合并进 URL 的 query 部分。
- 再拼请求头:@Header、@HeaderMap 加到 Request 的 headers。
- 最后处理请求体:根据方法上的注解决定 body——@Body 直接做对象→RequestBody;@FormUrlEncoded + @Field / @FieldMap 拼表单;@Multipart + @Part / @PartMap 拼 multipart。
同一类注解之间(如多个 @Query)一般按方法参数顺序或 Map 顺序合并,不存在“后面的覆盖前面的”这种优先级,而是都生效。
(3)互斥关系(不能同时用的组合)
| 互斥 | 说明 |
|---|---|
| @Body 与 @Field / @Part | 同一方法不能既有 @Body 又有 @Field 或 @Part;请求体要么是一个对象(@Body),要么是表单(@Field)或 multipart(@Part),只能选一种。 |
| @FormUrlEncoded 与 @Multipart | @FormUrlEncoded(表单编码)和 @Multipart(文件上传等)二选一;不能同一个方法上两个都写。 |
(4)@Path 的注意点
- 占位符名字要和 @Path("xxx") 的 "xxx" 一致,和参数名无关(参数名可以随便,如 @Path("id") long userId)。
- 若 path 值已经编码过,需要设
@Path(value = "id", encoded = true),否则 Retrofit 会再编一次导致双重编码。
(5)面试追问
- @Query 传 null 会怎样?
多数 Converter / 默认行为会忽略该参数,不拼进 URL;具体以 Retrofit 与所用 Converter 实现为准,建议避免传 null,需要时可传空字符串或不传该参数。