OkHttp
OkHttp 是运行于 Android 和 JVM 的 HTTP 客户端。一次请求除了发送字节,还涉及排队、缓存、建立或复用连接、重定向、失败恢复与响应消费;OkHttp 把这些工作组织成一条调用链。理解它的重点,是分清一次业务请求、一次网络交换和一条连接各自的生命周期。
本文以 OkHttp 4.12.0 为例,说明普通 HTTP 调用。WebSocket 的调度和连接生命周期另有约束。
1. 面试核心
先记住这几条,再读正文OkHttpClient持有可复用的配置和资源,Call表示一次可执行调用,同一个 Call 只能执行一次,重复请求需要新建或 clone。卡 · Client、Request、Call 与 Response- Dispatcher 决定异步调用何时执行,异步并发上限、同步调用数量和连接池容量不是同一个限制。卡 · Dispatcher 与线程
- 拦截器通过
proceed()组成调用链,应用拦截器围绕逻辑调用,网络拦截器围绕实际网络交换。卡 · 拦截器链 - 连接池复用满足条件的连接,默认保留 5 条空闲连接不等于一个域名最多只能有 5 条连接。卡 · 连接池与连接复用
- HTTP 缓存取决于响应规则和缓存配置,新鲜命中可以不访问网络,条件校验可能合并 304 与已有响应体。卡 · HTTP 缓存
- 超时与取消限定本地等待和资源占用,请求失败或取消不能证明服务端没有执行操作。卡 · 不同超时限制不同阶段
- ResponseBody 通常是只能消费一次的流,读完或放弃都要关闭,大文件不能随意全部读成字符串。卡 · 响应体与资源释放
- 内部恢复并不等于业务可安全重试,请求体能否重放、操作是否幂等和总尝试次数要分别判断。卡 · 内部恢复与应用重试
2. Client、Request、Call 与 Response
OkHttpClient 保存连接池、Dispatcher、拦截器和超时等配置。通常为一组相同网络策略复用客户端,避免每次请求都创建独立连接池。需要少量不同配置时,newBuilder() 可以基于已有客户端构建新实例,默认共享其连接池和 Dispatcher 等资源;是否继续共享仍取决于后续有没有显式替换。
Request 描述 URL、方法、头和请求体。调用 client.newCall(request) 得到 Call,此时不代表已经发送请求。execute() 同步执行,enqueue() 交给异步调度。
import java.io.IOException
import java.util.concurrent.TimeUnit
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
val httpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.build()
fun fetchText(client: OkHttpClient, url: HttpUrl): String {
val request = Request.Builder().url(url).build()
return client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("HTTP ${response.code}")
val body = response.body ?: throw IOException("响应体不可用")
body.string()
}
}
fetchText() 在调用线程等待请求并读完响应,Android 中必须从合适的后台执行环境调用。use 保证离开代码块时关闭 Response;HTTP 状态不成功、读取异常或正常返回都会经过资源释放。
isSuccessful 判断 HTTP 状态码是否在 200–299 范围内。HTTP 404、500 仍然是有效 HTTP 响应,不会仅因状态码自动进入 OkHttp 的网络失败回调。业务层还需要定义这些响应如何映射为页面错误、重试或登录流程。
同一个 Call 不能先 execute() 再 enqueue(),也不能重复执行。clone() 创建的是另一个可执行 Call,不会保证请求体可以安全重放,更不会保证业务操作可以重复。
3. Dispatcher 与线程
Dispatcher 管理调用的运行状态,并为异步调用决定排队和执行。OkHttp 4.12.0 维护待执行异步队列、运行中异步集合和运行中同步集合。
| 调用方式 | 执行线程 | Dispatcher 的职责 |
|---|---|---|
execute() | 调用者线程 | 登记与移除同步调用,不按异步上限排队 |
enqueue() | Dispatcher 使用的 Executor | 满足异步并发条件后执行,并调用回调 |
这一版本默认 maxRequests = 64、maxRequestsPerHost = 5,用于异步调用的调度。后者按 URL 主机名计数,不是按目标 IP,也不是直接限制 TCP 连接数;多个主机名可以解析到同一个 IP。
异步回调默认在执行该调用的工作线程运行,不能直接操作 Android View。异步调用只有在 onResponse() 或 onFailure() 返回后,才从相应运行集合退出。回调里做很久的计算,也会占用这个异步调度名额。
如果把响应交给其他线程处理,回调提前返回,Dispatcher 可以调度下一条调用,但原响应体仍可能没有读完。这说明“Dispatcher 空闲”不等于“所有响应体与连接都已释放”。同步 execute() 返回时也存在相同的生命周期区别。
提高并发数可能让请求更早开始,也可能增加带宽竞争、服务端压力和内存占用。应先判断瓶颈来自排队、建连、服务端还是响应消费,再调整参数。
4. 拦截器链
4.1 proceed 与执行顺序
拦截器是在请求向下执行、响应向上返回时插入处理的组件。调用 chain.proceed(request) 把请求交给下一环;它返回时,下游已经取得相应响应,但响应体通常还没有被全部读取。
OkHttp 4.12.0 普通调用的主要顺序如下:
| 顺序 | 组件 | 主要职责 |
|---|---|---|
| 1 | 应用拦截器 | 面向业务调用处理公共头、策略和应用观察 |
| 2 | RetryAndFollowUpInterceptor | 判断失败恢复及重定向、认证等后续请求 |
| 3 | BridgeInterceptor | 把应用请求补成网络请求,处理 Cookie、透明 gzip 等适用行为 |
| 4 | CacheInterceptor | 选择缓存、网络或条件校验,并处理缓存写入 |
| 5 | ConnectInterceptor | 为当前网络交换取得可用连接并准备交换对象 |
| 6 | 网络拦截器 | 观察、处理当前实际网络交换 |
| 7 | CallServerInterceptor | 向服务端发送请求并读取响应头等数据 |
响应按调用栈返回。重定向或恢复可能让中间部分再次执行;完全从缓存返回时,后面的建连和网络阶段可以跳过。这条链表达的是责任顺序,不代表七个阶段各自开一个线程。
BridgeInterceptor 的“桥接”也有具体条件。例如透明 gzip 不是无条件覆盖用户配置:它会根据请求是否已经设置 Accept-Encoding、是否为 Range 请求等判断是否自行请求 gzip,再在相应情况下解压返回体。
4.2 应用拦截器与网络拦截器
应用拦截器位于恢复和重定向逻辑外层,通常观察一次逻辑调用。网络拦截器位于连接建立之后,能够接触当前连接,看到的是一次次实际网络交换;一次调用经历重定向时,它可能执行多次,纯缓存命中时则不会执行。
应用拦截器可以按明确规则短路返回响应,也可以自行再次调用 proceed()。如果再次调用,必须先关闭前一个响应体,并控制重试条件。网络拦截器则必须恰好调用一次 proceed(),并保持当前交换要求的主机与端口约束,不能随意变成另一次请求的调度器。
下面的应用拦截器给一次调用加入请求标识。它不读取响应体,也不改变调用结果:
import java.util.UUID
import okhttp3.Interceptor
import okhttp3.Response
class RequestIdInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("X-Request-Id", UUID.randomUUID().toString())
.build()
return chain.proceed(request)
}
}
通过 addInterceptor(RequestIdInterceptor()) 注册后,它在应用拦截器的位置执行。请求标识用于追踪,不自动具备服务端幂等去重语义;若服务器需要幂等键,应按对应接口协议生成并在业务重试间正确复用。
日志也要注意这一层的边界。打印 body.string() 会消费响应流;粗略记录 proceed() 前后的时间,通常只覆盖到取得响应,不能当成完整下载耗时。需要分段观察 DNS、建连、TLS 和响应消费,可以使用 EventListener,同时避免记录令牌和敏感响应内容。
5. 连接池与连接复用
连接池保留仍可使用的连接,让后续请求减少重复建连成本。复用时不仅看“域名是否一样”,还需要满足地址和配置、代理、协议、TLS 验证、连接健康状态以及当前容量等条件。
HTTP/1.1 连接通常顺序承载交换;HTTP/2 可以在同一连接上多路复用多个流,但仍受服务端配置、流并发能力和连接状态限制。协议机制见 HTTP。
因此,一个域名可以有多条连接,不能回答成“永远只有一条”。并发需求、已有连接是否可用、协议以及连接是否正在退出使用,都可能影响新连接的创建。HTTP/2 在满足证书、路由等严格条件时还可能跨主机复用连接,不能简单归纳成“域名不同一定不能复用”。
OkHttp 4.12.0 默认 ConnectionPool 最多保留 5 条空闲连接,空闲保留时间为 5 分钟。这里的“5”描述空闲保留策略,不是每域名最大连接数,也不是整个应用最多只能建立 5 条活跃连接。
响应体使用完毕后,连接才有机会按协议和状态继续复用。如果服务端要求关闭、连接发生错误或客户端主动取消,连接也可能被关闭或退出复用。关闭响应体不等于无条件关闭底层 socket。
TLS 证书与主机名验证是连接可信性的必要部分。不能为了让请求成功,安装信任所有证书的 TrustManager 或无条件通过的 HostnameVerifier;具体校验关系见 TLS。
6. HTTP 缓存
OkHttp 的 HTTP 缓存需要配置 Cache 和合适的缓存目录,默认没有为客户端启用磁盘响应缓存。即使启用,是否能存储与复用仍取决于方法、响应头、请求头以及缓存规则。
主要路径有三种:
- 已有响应仍满足新鲜度与请求条件,可以直接返回缓存,不执行网络拦截器。
- 缓存需要校验时,使用 ETag 或 Last-Modified 等信息发起条件请求;服务端返回 304 后,可以合并响应头并继续使用已有响应体。
- 没有可用缓存或需要取得新内容时,访问网络;响应符合规则且正确消费后,才有机会形成可用缓存。
no-cache 主要要求复用前校验,不等于禁止存储;no-store 才是不应保存的关键指令。Vary、鉴权和请求条件也会影响命中,完整语义见 HTTP 缓存。
可以通过 response.cacheResponse、response.networkResponse 判断缓存与网络参与情况。条件校验后两个字段可能都有值,外层响应通常已经是合并后的可用结果;不能只看一次服务器返回 304,就认为业务层一定直接收到没有实体的 304。
不要为了离线可用而无条件改写所有响应的缓存头。用户私有数据、会变化的状态和共享缓存边界需要按业务设计。强制只用缓存而没有可接受条目时,也可能得到缓存未满足的响应,而不是自动恢复网络。
7. 超时、取消与重试
7.1 不同超时限制不同阶段
| 参数 | 主要范围 | 不能推导的结论 |
|---|---|---|
connectTimeout | 建立相应 socket 连接的等待 | 不等于完整请求耗时上限 |
readTimeout | 读取操作的等待 | 持续缓慢返回数据的整个下载不一定在这一时长内结束 |
writeTimeout | 写入操作的等待 | 不等于服务端处理完成时限 |
callTimeout | 一次调用执行期间的整体期限,涵盖适用的 DNS、连接、写入、服务端处理、读取及后续尝试 | 不等于任意上层任务从创建开始的总期限 |
OkHttp 4.12.0 的异步调用在 AsyncCall.run() 开始时进入 call timeout,所以 Dispatcher 待执行队列中的等待不包含在这一段期限内。若产品要求从用户操作开始计算总等待时间,应在上层定义总期限和取消策略,不能只设置一个 callTimeout 就默认覆盖所有排队。
同步调用也不会因为参数名是“连接超时”,就替 DNS、响应读取和完整业务流程设置同样的上限。排查慢请求时,应区分排队、域名解析、连接建立、首字节和响应体消费。
7.2 取消与业务结果
Call.cancel() 用于尽快终止当前或尚未开始的调用,并释放相应网络资源。取消前后存在竞争:请求可能还没发出,也可能服务端已经收到甚至已经处理完成,本地随后才停止等待。
因此,取消支付或提交接口不能被解释为服务端已撤销操作。服务端幂等键、操作状态查询或补偿流程需要单独设计。页面离开时取消无用请求,主要是减少资源占用和避免过期结果更新页面。
回调失败可能来自取消、DNS、连接、TLS 或读写错误,不能全部显示成“没有网络”。HTTP 错误状态又是另一类结果;解析响应内容失败通常在读取或转换阶段发生。
7.3 内部恢复与应用重试
OkHttp 可以在满足条件时从部分连接错误中恢复,例如尝试其他可用路由。默认开启的 retryOnConnectionFailure 不是“任何错误无限重试”的开关:是否已经发送请求、异常类型、请求体是否只能发送一次以及是否还有可用路由都会影响决定。
重定向、认证挑战和部分特定响应状态也可能触发后续请求,但规则与版本有关,不能把所有 HTTP 失败状态当成自动重试条件。应用拦截器自己重试、Repository 重试和服务器端处理可能叠加,必须控制总次数与总等待时间。
请求体能再次读取,只说明技术上可能重放;业务操作是否幂等是另一件事。提交订单即使可以重新编码相同 JSON,也不能因此无条件重复提交。反过来,标记一次性请求体,也不能替代服务端的幂等与状态协议。
8. 响应体与资源释放
ResponseBody 是响应内容的流式入口,通常只能消费一次。string() 和 bytes() 会把内容读入内存并关闭相应资源,不能先读成字符串用于日志,再期待读取到同一份流。
大文件应使用流式复制,并保证输入、输出和响应在成功、失败、取消时正确关闭。还要处理磁盘空间、临时文件、最终文件替换和需要恢复时的 Range 与校验条件;不能只把 string() 改成 bytes() 就称为流式下载。
异步 onResponse() 得到的 Response 也需要关闭。若把它交给另一个线程,应同时转交明确的关闭责任;回调返回不会自动替接收方消费并关闭响应体。
响应读取异常应在实际读取处处理。得到响应头时调用成功,不代表整个响应体都已成功下载。业务层需要把“收到响应”“读完内容”“解析成功”和“写入本地成功”按实际需求区分。
9 · 面试题卡片
本页题目与参考回答
9.1 OkHttpClient 与 Call 分别代表什么?同一个 Call 可以执行两次吗?
考察重点:资源复用、请求描述与可执行调用的区别。
相关内容:Client、Request、Call 与 Response。
参考回答
Client 保存可复用的配置和资源,Call 表示一次只能执行一次的调用。 Request 是请求描述,创建 Call 不代表已经发送;同步执行使用调用者线程,异步执行进入调度。重复执行需要新 Call 或 clone,但复制调用不保证请求体可重放或业务可重复。
9.2 OkHttp 的 Dispatcher 怎样调度请求?
考察重点:待执行队列、异步并发限制、同步登记与回调线程。
相关内容:Dispatcher 与线程。
参考回答
异步请求满足并发条件后交给 Executor 执行,同步请求直接在调用者线程执行,Dispatcher 主要登记其状态。 4.12.0 默认异步总上限为 64、每 URL 主机为 5,这不是连接数限制。异步回调返回后释放调度名额,但若响应体交给其他线程,连接资源还可能继续被占用。
9.3 OkHttp 的拦截器怎样工作,默认拦截器分别负责什么?
考察重点:责任链、恢复与重定向范围、缓存短路和 proceed 约束。
相关内容:拦截器链。
参考回答
普通调用依次经过应用拦截器、恢复与后续请求、桥接、缓存、连接、网络拦截器和服务端交换。 应用拦截器观察逻辑调用,网络拦截器观察实际交换;重定向时网络阶段可能多次执行,纯缓存命中不会执行网络阶段。网络拦截器必须恰好调用一次 proceed();应用拦截器再次调用前必须关闭前一个响应,并控制业务重试语义。
9.4 同一个域名可以有几条 OkHttp 连接,怎样复用连接?
考察重点:协议并发能力、连接复用条件与空闲连接策略。
相关内容:连接池与连接复用。
参考回答
没有一个适用于所有情形的固定域名连接数,取决于协议、并发、连接可用性和配置。 HTTP/2 可以在一条连接上复用多个流,但不保证永远只建一条连接。4.12.0 默认 ConnectionPool 保留最多 5 条空闲连接、空闲保留 5 分钟;Dispatcher 每主机异步上限默认也为 5,但这是另一项调度规则,二者都不能直接当成域名活跃连接数。
9.5 OkHttp 的 HTTP 缓存策略是什么?
考察重点:缓存配置、新鲜度、条件校验和合并响应。
相关内容:HTTP 缓存。
参考回答
启用 Cache 后,满足新鲜度的缓存可以直接返回,过期但可校验的缓存可以通过条件请求验证。 服务端返回 304 时,OkHttp 可以合并响应头并沿用缓存体;外层结果不一定是原始 304。可以结合 cacheResponse 和 networkResponse 判断参与路径。no-cache 要求复用前校验,不能误当成 no-store。
9.6 设置 callTimeout 或取消 Call,是否就能保证业务操作没有超时或没有执行?
考察重点:本地执行期限、异步排队与服务端结果的不确定性。
相关内容:不同超时限制不同阶段、取消与业务结果。
参考回答
不能。callTimeout 限制相应调用执行期间,4.12.0 异步队列等待不包含在 AsyncCall 启动后的计时内。 上层总期限需要另行定义。取消或网络超时也只说明本地不再继续等待,服务端可能已经执行;应通过幂等键、状态查询或补偿协议处理,不能直接认定操作失败且可无条件重复。
9.7 为什么 ResponseBody 必须关闭?为什么不能读取两次 string()?
考察重点:流式内容、资源归属与连接复用。
相关内容:响应体与资源释放。
参考回答
响应体持有流及相关网络资源,读完或放弃都必须释放。 string() 通常一次性消费并关闭内容,不是可反复读取的缓存对象。关闭后连接可能被复用,也可能因协议或错误被关闭。大文件应流式处理;异步回调返回不会自动替另一个接收线程关闭其尚未处理的响应。
9.8 retryOnConnectionFailure 能否保证失败请求可以安全重试?
考察重点:传输恢复、请求体重放和业务幂等的区别。
相关内容:内部恢复与应用重试。
参考回答
不能。它只允许 OkHttp 在特定连接失败条件下尝试恢复,并不证明业务重复执行安全。 是否还有路由、异常是否可恢复、请求是否发送及请求体是否一次性都会影响行为。应用层还应限制总次数和总时长,避免多层重试叠加,并按接口协议处理幂等和操作状态。
10. 参考资料
- OkHttp 4.12.0 发布源码包
- OkHttp 4.12.0 RealCall:调用链与超时
- OkHttp 4.12.0 Dispatcher
- OkHttp 4.12.0 ConnectionPool
- OkHttp 4.12.0 CacheInterceptor
- OkHttp 4.12.0 RetryAndFollowUpInterceptor
- OkHttp 4.12.0 ResponseBody
- OkHttp 4.12.0 RealInterceptorChain
来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明