Retrofit2 本身并不包含缓存功能,且代码中目前也没有开启 OkHttp 的磁盘缓存功能
观察到的"第二次请求特别快",并不是因为数据被缓存了 ,而是因为 OkHttp 的连接池复用机制(Connection Pooling / Keep-Alive)。
下面详细解释为什么会这样,以及如果真的想要"缓存"该怎么做
1. 为什么第二次请求变快了?(连接复用)
在网络请求中,建立连接是最耗时的步骤之一。
- 第一次请求:DNS 解析 -> 建立 TCP 连接 (三次握手) -> SSL/TLS 握手 (加密验证) -> 发送数据 -> 接收数据。
- 第二次请求 :由于 OkHttp 默认开启了连接池 ,它发现还在请求同一个域名(
BASE_URL),且之前的连接还没有断开,它会直接跳过前面的握手步骤:直接利用已有连接发送数据 -> 接收数据。
这省去了几百毫秒甚至上秒的握手时间,所以会感觉"特别快"
注意:这并不意味着数据被缓存了。如果手机断网,第二次请求依然会失败
2. Retrofit 和 OkHttp 的关系
- Retrofit:只是一个外壳(Wrapper),它负责把 Interface 方法转换成 HTTP 请求,解析 JSON 数据等。它不负责底层的网络传输,也不负责缓存。
- OkHttp:是 Retrofit 底下的引擎,它负责真正的网络传输和缓存
3. 代码中为何没有缓存?
在 HTTP 协议中,标准的缓存是基于磁盘的(Disk Cache)。要启用它,需要显式地给 OkHttpClient 配置一个 Cache 对象。
查看代码:
kotlin
return OkHttpClient.Builder()
// ...
// 这里缺少了 .cache(cache) 的配置
.addInterceptor(logging)
.addInterceptor(TokenHeaderInterceptor())
.build()
因为没有配置 .cache(),OkHttp 不会将服务器返回的数据写入手机存储
4. 还有一个关键点:POST 请求通常不被缓存
ApiService,大部分请求是 @POST:
kotlin
@POST("/v1/media/getRecapInfo")
fun getRecapInfo(...)
HTTP 标准缓存协议通常只针对 GET 请求有效。 对于 POST 请求(通常代表提交数据或获取非幂等数据),浏览器和 OkHttp 默认都不会去缓存结果
所以,即便开启了 OkHttp 的缓存, getRecapInfo 这种 POST 请求依然不会被缓存。这进一步印证感觉到的"快"纯粹是因为网络连接复用
5. 如果想要真正的"数据缓存"该怎么做?
如果希望在没网的时候也能读取上次的数据,或者减少服务器压力,有两种选择:
方案 A:开启 OkHttp 标准缓存(仅对 GET 有效)
需要修改 buildOkHttpClient,传入 Context 来获取缓存路径:
kotlin
// 需要传入 context
fun buildOkHttpClient(context: Context): OkHttpClient {
// 设置缓存大小,比如 10MB
val cacheSize = 10 * 1024 * 1024L
val cache = Cache(File(context.cacheDir, "http_cache"), cacheSize)
return OkHttpClient.Builder()
.cache(cache) // 开启缓存
// ... 其他配置
.build()
}
前提:后端接口必须是 GET 请求,并且后端返回的 Header 中包含 Cache-Control 信息(比如 max-age=60)。
方案 B:业务层缓存(推荐,适合 POST 和复杂业务)
既然接口多是 POST,且业务逻辑较重,建议不要依赖 HTTP 协议缓存,而是自己控制:
-
数据库缓存 (Room):请求成功后,把 JSON 数据存入本地数据库(SQLite/Room)。下次请求前,先读数据库显示旧数据,同时去请求网络更新。
-
RxJava 内存缓存 :如果只是想在页面没销毁前缓存,可以使用 RxJava 的操作符。
kotlin// 示例:使用 replay(1).autoConnect() 让流变热,从而缓存最近一次的结果 val cachedObservable = apiService.getRecapInfo(req).replay(1).autoConnect()
总结
目前的框架没有缓存。
- 第一次慢:建立连接 + 传输数据
- 第二次快:复用连接 + 传输数据
这就是 HTTP 协议中的 Keep-Alive(HTTP 持久连接) 机制
在 OkHttp 中,这个机制是通过 ConnectionPool(连接池) 来实现的
1. 为什么叫"类似长连接"?
严格来说,它不是像 WebSocket 那种一直不断的"永久长连接",而是 "在一段时间内保持连接不断开"。
- 没有复用(短连接) :请求 -> 建立连接(3次握手+TLS) -> 传输 -> 断开连接。下次再请求,重新建立。
- 连接池复用(Keep-Alive) :请求 -> 建立连接 -> 传输 -> 连接闲置放入池中 (不断开)。
- 如果在 5分钟 (默认)内又有请求 -> 直接复用该连接。
- 如果超过 5分钟 没人用 -> 自动断开并回收。
2. 配置在哪里?(默认配置)
在代码中,没有显式配置它,因为 OkHttp 默认已经帮你配置好了
源码位置: okhttp3.OkHttpClient.Builder
kotlin
// OkHttp 源码片段 (Kotlin 版本)
class Builder constructor() {
// ...
internal var connectionPool: ConnectionPool = ConnectionPool() // 这里!默认创建了一个连接池
// ...
}
如果点进去看 ConnectionPool 的无参构造函数:
kotlin
// okhttp3.ConnectionPool 源码
class ConnectionPool(
maxIdleConnections: Int = 5, // 默认最大闲置连接数:5个
keepAliveDuration: Long = 5, // 默认保持存活时间:5
timeUnit: TimeUnit = TimeUnit.MINUTES // 单位:分钟
) {
// 这意味着:默认情况下,OkHttp 会维护最多 5 个空闲连接,
// 每个空闲连接如果不被使用,5 分钟后会被自动清理。
}
如果想修改这个配置(比如为了优化性能,想让连接保持更久,或者允许更多并发闲置连接),可以这样写:
kotlin
private fun buildOkHttpClient(): OkHttpClient {
// 自定义连接池
val myConnectionPool = ConnectionPool(10, 10, TimeUnit.MINUTES)
return OkHttpClient.Builder()
.connectionPool(myConnectionPool) // 显式传入
// ... 其他配置
.build()
}
3. 源码探究:它是如何"偷偷"复用的?
既然对源码好奇,来看看 OkHttp 是如何管理这些连接的。核心逻辑在 okhttp3.internal.connection 包下。
A. 存:请求结束后不关连接
当请求(比如 getRecapInfo)执行完毕拿到数据后,OkHttp 的底层流处理类(StreamAllocation 或新版的 Exchange)会调用 release 方法。
正常逻辑是关闭 Socket,但 OkHttp 做了一个判断: "如果是 HTTP/1.1 且 header 里有 keep-alive,我不关 Socket,而是把它标记为 idle(闲置),放回 ConnectionPool 的双端队列(Deque)中。"
B. 取:请求开始前先找连接
当发起第二次 请求时,OkHttp 不会立刻 new Socket()
它会去 ConnectionPool 里找(源码类 ExchangeFinder):
- 遍历池子:看有没有 host 和 port 都匹配的、而且是可以复用的连接
- 找到了 :直接返回这个
RealConnection对象 - 没找到:才去执行 TCP 三次握手和 TLS 握手(就是觉得耗时的那部分)
C. 清理:后台线程自动打扫
OkHttp 怎么知道什么时候关闭那些 5 分钟没用的连接?
在 ConnectionPool 内部,有一个静态的线程池 (executor),专门运行一个 cleanupRunnable 任务。
kotlin
// ConnectionPool 伪代码逻辑
private val cleanupRunnable = Runnable {
while (true) {
// 1. 计算下一个连接多久过期
val waitNanos = cleanup(System.nanoTime())
// 2. 如果池子空了,退出循环,线程结束
if (waitNanos == -1L) return
// 3. 还没到期,线程挂起等待(wait),到时间再醒来检查
if (waitNanos > 0) {
lock.wait(waitNanos)
}
}
}
这个设计非常精妙:如果没有空闲连接,清理线程是不运行的,完全不占资源。一旦有了空闲连接,它才会启动并计时。
4. 为什么握手这么耗时?(直观对比)
感觉到的"第一次慢,第二次快",在 HTTPS 请求中差异巨大
第一次请求(冷启动):
- DNS:域名 -> IP (几毫秒到几百毫秒)
- TCP 握手:SYN -> SYN/ACK -> ACK (1个 RTT,往返时延)
- TLS 握手 (最贵):
- Client Hello
- Server Hello + Certificate
- Key Exchange
- Finished
- (如果是 TLS 1.2,这里至少消耗 2个 RTT。跨国网络下,这可能就是 500ms+ 的时间)
- 发送 HTTP 数据
- 接收数据
第二次请求(复用):
- (跳过 DNS)
- (跳过 TCP 握手)
- (跳过 TLS 握手) -> 省下了最耗时的部分!
- 发送 HTTP 数据
- 接收数据
所以,在移动端开发中,保持 OkHttpClient 单例(像代码里做的 object NetworkApi + lazy)是非常重要的,这样才能利用连接池。如果每次请求都 new OkHttpClient(),连接池就会失效,每次都要重新握手,速度就会很慢
不需要显式设置,header中Keep-Alive默认是开启的。 而且,目前的日志里可能看不到这个 Header,是因为拦截器的位置原因
下面一步步验证和揭秘
1. 为什么不需要设置?(协议默认值)
- HTTP/1.1 协议规定 :只要是 HTTP/1.1,默认就是
Keep-Alive(持久连接)。除非显式发送Connection: close,否则服务器和客户端都默契地保持连接。 - HTTP/2 协议规定 :在 HTTP/2 中,连接复用是核心特性(Multiplexing),
Connectionheader 甚至被视为非法或被忽略,因为连接默认就是持久且多路复用的。
2. 为什么日志里可能看不到?(拦截器陷阱)
现在的代码是这样写的:
kotlin
// ...
.addInterceptor(logging) // <--- 注意这里
// ...
这叫做 Application Interceptor(应用拦截器)。
OkHttp 的工作流程是这样的:
- 应用拦截器 (Application Interceptor) :就是
logging。这时候请求还是"原始"的,是代码里写的样子。OkHttp 还没来得及加默认 Header。 - 桥接拦截器 (BridgeInterceptor) :OkHttp 内部的一个拦截器。它负责补全 Header(比如
Host,User-Agent,Gzip,Connection: Keep-Alive)。 - 网络拦截器 (Network Interceptor):在真正发包之前。
- 发送网络请求。
结论 :因为日志拦截器在第 1 步,而添加 Keep-Alive 发生在第 2 步,所以在日志里看不到它
3. 如何亲眼看到它?(两个方法)
方法一:临时修改代码(最快验证)
把代码中的 addInterceptor(logging) 改为 addNetworkInterceptor(logging)。
kotlin
// NetworkApi.kt 修改
private fun buildOkHttpClient(): OkHttpClient {
// ... 前面省略
return OkHttpClient.Builder()
// ...
// 删掉这行 .addInterceptor(logging)
// 改用下面这行:
.addNetworkInterceptor(logging)
.addInterceptor(TokenHeaderInterceptor())
.build()
}
重新运行并看日志: 会在 Header 里清晰地看到: Connection: Keep-Alive Accept-Encoding: gzip (OkHttp 也会自动帮加这个,之前看不到,现在能看到了) User-Agent: okhttp/4.x.x
验证完记得改回来,通常我们开发用 addInterceptor 就够了,addNetworkInterceptor 会打印一些底层的、解压前的数据,有时候比较乱
方法二:使用 Android Studio Network Inspector(不用改代码)
- 运行 App。
- 打开 Android Studio 底部的 App Inspection 或 Network Inspector 标签页。
- 点击发起一个网络请求。
- 点击左侧列表里的请求,查看右侧的 Request Headers。
- 会看到
Connection: Keep-Alive赫然在列。这是最真实的、发给服务器的数据。
4. 源码验证(核心证据)
既然想看源码实现,这个逻辑在 okhttp3.internal.http.BridgeInterceptor 类中
OkHttp 的责任链模式中,BridgeInterceptor 专门负责把用户的请求转换为网络友好的请求
kotlin
// OkHttp 源码:BridgeInterceptor.kt
class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val userRequest = chain.request()
val requestBuilder = userRequest.newBuilder()
// ... 省略其他 Header 设置 (Content-Type, Content-Length 等)
// 重点在这里!!!
if (userRequest.header("Connection") == null) {
// 如果用户没有自己设置 Connection Header,OkHttp 帮加上 Keep-Alive
requestBuilder.header("Connection", "Keep-Alive")
}
// ... 省略 Gzip 设置 (Accept-Encoding)
val networkResponse = chain.proceed(requestBuilder.build())
// ...
}
}
代码逻辑解读: 它检查 userRequest.header("Connection") == null。 没有设置,所以它确实是空的 于是它执行 requestBuilder.header("Connection", "Keep-Alive")。
这就是为什么没写,但它确实存在,且 OkHttp 会帮助维护连接池的原因