OkHttp & Retrofit 完全指南——上

53 阅读1小时+

第一部分: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 链式 APIRequest、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 的区别(优势一览)

对比项OkHttpHttpURLConnection
连接管理连接池复用,按 Route 匹配无连接池,每次可能新建连接
协议HTTP/1.1、HTTP/2、HTTP/3(5.x)仅 HTTP/1.1
拦截器支持,可统一加头、日志、重试无,需每个请求手写
重试/缓存可配置重试;内置 Cache + CacheInterceptor需自己实现
GZIP自动加 Accept-Encoding、自动解压需自己解压
CookieCookieJar 接口,可持久化需自己管理 Cookie 头
API 风格Builder、不可变、链式多步 set/get,易忘步骤
同步/异步execute / enqueue + Dispatcher只有同步,异步要自己开线程
取消Call.cancel(),可配合 Tag 批量取消无统一取消 API
依赖需引入 okhttpJDK 自带

代码对比(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.xAPI 重构、拦截器、HTTP/2架构与 API 转折点;拦截器链、连接池、更好错误处理
4.xKotlin、协程友好与 Kotlin 生态更好配合,持续维护
5.xHTTP/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.0HTTP/1.1HTTP/2HTTP/3 (QUIC)
传输层TCPTCPTCPUDP(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) 进入下一层
5ConnectInterceptor 从 ConnectionPool 获取或新建 RealConnection,建立 TCP/TLS
6CallServerInterceptor 通过连接写请求、读响应,完成真实 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.FactorynewCall(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,常见三种——JSONRequestBody.create(json, application/json))、表单FormBody.Builder)、文件/多部分MultipartBody.Builder)。JSON 与表单的完整写法见 2.4,multipart 见第五章文件上传。


2.2 如何设置请求头?

在 Request.Builder 上链式调用即可;常用如 AuthorizationContent-TypeUser-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-TypeOkHttp 自动设为 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 只能读一次,读后流关闭。

响应类型写法
JSONString 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。补充:一次请求只会回调 onResponseonFailure 其一;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(含异步与同步)。

默认配置与队列大小

配置项默认值说明
maxRequests64全局同时执行的异步 Call 上限;超出部分留在就绪队列等待
maxRequestsPerHost5同一 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 未取消且仍被某处引用,导致页面泄漏。

正确做法

  1. 在 onDestroy 里取消:若 Activity 持有 Call currentCall,在 onDestroy 中 if (currentCall != null) currentCall.cancel(); 并置空。
  2. Callback 不持强引用:用静态内部类 + WeakReference<Activity>,在 onResponse/onFailure 里先 Activity a = ref.get(); if (a == null) return; 再更新 UI。
  3. 用 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&lt;MainActivity&gt; ref;
    MyCallback(MainActivity a) { ref = new WeakReference&lt;&gt;(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)的最大等待时间10sWiFi/内网可 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);若抛 SocketTimeoutExceptionConnectExceptionUnknownHostException 等可重试异常,且当前重试次数 < 最大次数,则 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;
    }
}

按环境区分

环境建议
DebugHttpLoggingInterceptor,Level 为 BODY 或 HEADERS,便于抓包排查
Release不加或 Level 为 NONE/BASIC;加统一鉴权、公共头等拦截器,避免日志泄露

第五章:OkHttp 文件操作

5.1 上传文件(单文件 / 多文件 / 大文件)

使用 MultipartBodymultipart/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&lt;File&gt; 动态添加;注意总大小与 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() 添加;鉴权、公共头、全局日志等;即使命中缓存也会执行
2RetryAndFollowUpInterceptor内置重试(如连接失败)、处理 3xx 重定向、407 代理认证等
3BridgeInterceptor内置补全 Content-Type、Content-Length、Accept-Encoding 等;响应时解压 GZIP
4CacheInterceptor内置读缓存、写缓存、发条件请求(304);命中缓存时可能不再往下走
5ConnectInterceptor内置从连接池取或新建 RealConnection,建立 TCP/TLS,得到 Exchange
6网络拦截器用户添加addNetworkInterceptor() 添加;仅真正发网络请求时执行,可见重定向等中间请求
7CallServerInterceptor内置通过连接写请求行/头/体,读响应行/头/体,完成真实 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) 中:

  1. 拿到 Request request = chain.request(),用 request.newBuilder()...build() 得到新 Request(如加 Header)。
  2. 调用 Response response = chain.proceed(newRequest) 把请求交给下一层(必须调用,否则请求不会发出)。
  3. 可选:用 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 要连接时)

  1. ConnectInterceptor 根据当前请求得到对应的 Route,向 ConnectionPool 要连接。
  2. 连接池在空闲连接里查找:是否有同 Route健康的连接(未关闭、可写)。
  3. 若有:取出该连接交给本次请求使用,不再新建连接。
  4. 若无:新建 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 监控

需要观察连接池状态时,可以用以下方式:

  • 连接池 APIclient.connectionPool().idleConnectionCount() 可获取当前空闲连接数,便于排查“连接是否被复用、池里有多少空闲连接”。
  • EventListener:在构建 OkHttpClient 时设置 EventListener,在 connectionAcquired(从池取出或新建连接时)、connectionReleased(请求结束、连接放回池时)等回调里做简单统计或日志,即可观察连接的获取与释放情况。

适合做简单监控和问题排查,不必深入源码。


7.8 性能优化建议

建议说明
单例 OkHttpClient整个 App 或同一网络层共用同一个 OkHttpClient,从而共用同一个连接池,连接复用率更高。
启用 HTTP/2与服务器协商使用 HTTP/2 后,同一条连接可多路复用多个请求,进一步减少建连次数。
按场景调大连接池请求多、同一 host 并发高时,可适当增大 maxIdlekeepAlive(见 7.3),减少频繁建连与清理;不宜过大,避免占用过多文件描述符和内存。
配合缓存合理使用 OkHttp 缓存(CacheInterceptor),减少重复请求,间接降低建连与请求次数。

第八章:OkHttp 缓存机制

8.1 机制概览

OkHttp 的缓存遵循 HTTP 缓存标准(RFC 7234):根据服务端返回的响应头决定是否缓存、何时过期,下次请求时先看缓存再决定是否发网络请求,由拦截器链里的 CacheInterceptor 统一完成读缓存、写缓存、发条件请求,对业务透明。

两种缓存方式

类型依据行为简述
强制缓存服务端通过 Cache-Control(如 max-age=3600)或 Expires 告诉客户端“多久内可直接用缓存”在有效期内再次请求同一 URL 时,不发请求,直接返回本地缓存;过期后才可能向服务器验证。
协商缓存服务端返回 Last-ModifiedETag,再次请求时客户端带 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-ModifiedETag,客户端缓存;再次请求时客户端带上 If-Modified-SinceIf-None-Match,服务端比较后若资源未变则返回 304,客户端用本地缓存,从而省去传输 body。

流程简述

  1. 首次请求:服务端返回 200 和 Last-Modified / ETag,OkHttp 将响应与这两个头一起写入缓存。
  2. 再次请求:CacheInterceptor 从缓存中取出上次的 Last-Modified/ETag,在请求头中带上 If-Modified-Since / If-None-Match 发往服务端。
  3. 服务端未修改:返回 304 Not Modified(无 body 或 body 为空),OkHttp 用本地缓存内容拼出 Response 并更新缓存元数据。
  4. 服务端已修改:返回 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&lt;Cookie&gt; cookies);  // 响应里有 Set-Cookie 时,OkHttp 调这个把 Cookie 交给你存
    List&lt;Cookie&gt; 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&lt;String, List&lt;Cookie&gt;&gt; store = new HashMap&lt;&gt;();

    @Override
    public void saveFromResponse(HttpUrl url, List&lt;Cookie&gt; cookies) {
        if (cookies != null && !cookies.isEmpty())
            store.put(url.host(), cookies);
    }

    @Override
    public List&lt;Cookie&gt; 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 持久化)

你要做的:实现 saveFromResponseloadForRequest存到哪里自己定。

存法做法特点
内存Map&lt;String, List&lt;Cookie&gt;&gt;,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&lt;Cookie&gt; 转 JSON 写入;读时反序列化并过滤过期。数据多时建议用数据库。
  • 数据库:表里存 host、name、value、expiresAt、path、domain 等;save 时 insert/update,load 时按 host 查并去掉过期。方便按条件删、查。

加载时一定要过滤过期expiresAt &lt; now);Session Cookie 无过期时要自己定策略。


第三部分:Retrofit 基础

第十章:Retrofit 概述

10.1 Retrofit 是什么?作用是什么?

定义
Retrofit 是 Square 出的、基于 OkHttp 的 HTTP 客户端库。它用 Java/Kotlin 接口 + 注解 来描述“请求哪个 URL、什么方法、带什么参数”,调用接口方法时由 Retrofit 自动拼出 Request、交给 OkHttp 发请求,并把响应体通过 Converter 转成你声明的类型(如 UserList&lt;User&gt;),实现类型安全的 API 调用。

和“手写 OkHttp”的区别
手写 OkHttp 时你要自己 Request.Builder().url(...).post(body).build()、自己解析 response.body().string() 成对象。Retrofit 让你只写接口(方法返回值、参数、注解),拼 Request 和解析 Response 由库完成,代码更短、出错更少。

作用概括

作用说明
简化调用接口即文档,调用方直接 api.getUser(1),不用关心 URL 拼接、请求体格式
类型安全返回 Call&lt;User&gt;Observable&lt;User&gt; 等,编译期能检查类型,运行时少出错
可插拔通过 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 时可以传入 OkHttpClientRetrofit.Builder().client(okHttpClient).build())。所以你在 OkHttp 里配的拦截器、连接池、缓存、超时、CookieJar、证书等都会在 Retrofit 发请求时生效;Retrofit 只负责“把接口调用变成 Request”,不替换 OkHttp 的能力。

总结

  • Retrofit = 面向接口的编排层(怎么把一次 API 调用变成 Request + 怎么把 Response 转成你要的类型)。
  • OkHttp = 执行层(怎么发请求、怎么建连、怎么缓存、怎么带 Cookie)。
  • 用 Retrofit 时底层仍然是 OkHttp,所以 OkHttp 的面试点(拦截器、连接池、缓存等)在 Retrofit 场景下一样重要。

10.4 与其他网络框架对比

对比表

特性Retrofit仅 OkHttpHttpURLConnection
抽象层次接口 + 注解,面向 APIRequest/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&lt;T&gt; 等,与 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&lt;T&gt; 或 Observable&lt;T&gt; 等返回给你
        ↓
⑤ OkHttpCall 内部:把 Request 交给 OkHttp 发出去(进入 OkHttp 拦截器链、连接池等)

请求后(从网络响应到方法返回值)

OkHttp 拿到 Response(ResponseBody)
        ↓
① Converter(响应体):把 ResponseBody 转成你声明的类型 T(如 User),例如 Gson 解析 JSON → User
        ↓
② CallAdapter:把 T 包装进 Call&lt;T&gt; / Observable&lt;T&gt; / 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 返回值
发请求OkHttpRequest 真正发 HTTP 请求,走拦截器、连接池;得到 Response
响应体转换ConverterResponseBody 转成 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&lt;T&gt;Observable&lt;T&gt; 或 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依赖(示例)
JSONGsonConverterFactory、JacksonConverterFactory、MoshiConverterFactoryconverter-gson、converter-jackson、converter-moshi
XMLSimpleXmlConverterFactory 等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&lt;T&gt;Observable&lt;T&gt; 或 Kotlin suspend。接口无方法体,参数必须带 @Path/@Query/@Body 等注解。

// 示例 1:GET + @Path
@GET("users/{id}")
Call&lt;User&gt; getUser(@Path("id") long id);

// 示例 2:POST + @Body
@POST("users")
Call&lt;User&gt; create(@Body User user);

// 示例 3:表单 @FormUrlEncoded + @Field
@POST("login")
@FormUrlEncoded
Call&lt;LoginResult&gt; login(@Field("username") String user, @Field("password") String pwd);

// 示例 4:上传 @Multipart + @Part
@Multipart
@POST("upload")
Call&lt;UploadResult&gt; upload(@Part MultipartBody.Part file, @Part("desc") RequestBody desc);

11.3 各 HTTP 方法注解

注解含义典型用法
@GETGET路径可含 {id} 配 @Path;查询用 @Query/@QueryMap
@POSTPOST@Body 发 JSON;@FormUrlEncoded+@Field 表单;@Multipart+@Part 上传
@PUTPUT全量更新,用法同 POST
@DELETEDELETE多无 body;要 body 用 @Body
@PATCHPATCH部分更新,用法同 POST
@HEADHEAD只拿响应头,无 body
@OPTIONSOPTIONS常用于 CORS 预检

11.4 如何调用 API

方式说明
同步调用 call.execute()Response&lt;T&gt;,注意不能主线程。
异步调用 call.enqueue(Callback),在 onResponse/onFailure 里处理。
RxJava接口返回 Observable&lt;T&gt;,需 addCallAdapterFactory(RxJava3CallAdapterFactory.create()),再 subscribeOn/observeOn/subscribe。
协程接口声明 suspend fun xxx(): T,在协程里直接 api.xxx()

示例 1:同步

Call&lt;User&gt; call = api.getUser(123);
Response&lt;User&gt; resp = call.execute();
if (resp.isSuccessful()) User user = resp.body();

示例 2:异步

call.enqueue(new Callback&lt;User&gt;() {
    @Override public void onResponse(Call&lt;User&gt; c, Response&lt;User&gt; r) {
        if (r.isSuccessful()) User user = r.body();
    }
    @Override public void onFailure(Call&lt;User&gt; c, Throwable t) { }
});

示例 3:RxJava(接口返回 Observable&lt;User&gt;

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&lt;&gt;
@QueryMap:动态多参数,key 为参数名,value 为值。

// 示例 1:多个 @Query → ?page=1&limit=10
@GET("users")
Call&lt;List&lt;User&gt;&gt; getUsers(@Query("page") int page, @Query("limit") int limit);

// 示例 2:可选参数(null 不拼)
@GET("users")
Call&lt;List&lt;User&gt;&gt; getUsers(@Query("page") Integer page);

// 示例 3:同名多值 → ?ids=1&ids=2
@GET("users")
Call&lt;List&lt;User&gt;&gt; getUsers(@Query("ids") List&lt;Integer&gt; ids);

// 示例 4:@QueryMap 动态参数
@GET("users")
Call&lt;List&lt;User&gt;&gt; getUsers(@QueryMap Map&lt;String, String&gt; params);

12.2 @Path

路径占位:URL 里写 {name},参数用 @Path("name"),名字一致。值已编码时用 @Path(value = "id", encoded = true) 避免二次编码。

@GET("users/{id}")
Call&lt;User&gt; getUser(@Path("id") long id);
// getUser(123) → .../users/123

@GET("users/{userId}/posts/{postId}")
Call&lt;Post&gt; getPost(@Path("userId") long userId, @Path("postId") long postId);

12.3 @Body

对象作请求体,由 Converter 序列化(如 JSON)。

@POST("users")
Call&lt;User&gt; create(@Body User user);

12.4 表单与文件(@Field/@FieldMap、@Part/@PartMap)

@Field、@FieldMap:与 @FormUrlEncoded 同用,拼表单。
@Part、@PartMap:与 @Multipart 同用,上传文件或混合表单。

// 示例 1:@Field 表单
@POST("login")
@FormUrlEncoded
Call&lt;LoginResult&gt; login(@Field("username") String user, @Field("password") String pwd);

// 示例 2:@FieldMap
@POST("login")
@FormUrlEncoded
Call&lt;LoginResult&gt; login(@FieldMap Map&lt;String, String&gt; fields);

// 示例 3:@Part 文件 + 字段
@Multipart
@POST("upload")
Call&lt;UploadResult&gt; 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&lt;User&gt; getUser(@Header("Authorization") String token);

@GET("user")
Call&lt;User&gt; getUser(@HeaderMap Map&lt;String, String&gt; headers);

12.6 @Url

动态完整 URL,覆盖 baseUrl;调用时传入完整 URL。

@GET
Call&lt;ResponseBody&gt; 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 拼一次请求时,大致按以下顺序处理,便于记忆:

  1. 先确定 URL:如上,baseUrl + 相对路径(或 @Url)+ @Path 替换。
  2. 再拼查询参数@Query@QueryMap 按顺序或合并进 URL 的 query 部分。
  3. 再拼请求头@Header@HeaderMap 加到 Request 的 headers。
  4. 最后处理请求体:根据方法上的注解决定 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,需要时可传空字符串或不传该参数。