OKHTTP连接保持

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 协议缓存,而是自己控制:

  1. 数据库缓存 (Room):请求成功后,把 JSON 数据存入本地数据库(SQLite/Room)。下次请求前,先读数据库显示旧数据,同时去请求网络更新。

  2. 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):

  1. 遍历池子:看有没有 host 和 port 都匹配的、而且是可以复用的连接
  2. 找到了 :直接返回这个 RealConnection 对象
  3. 没找到:才去执行 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 请求中差异巨大

第一次请求(冷启动):

  1. DNS:域名 -> IP (几毫秒到几百毫秒)
  2. TCP 握手:SYN -> SYN/ACK -> ACK (1个 RTT,往返时延)
  3. TLS 握手 (最贵):
    • Client Hello
    • Server Hello + Certificate
    • Key Exchange
    • Finished
    • (如果是 TLS 1.2,这里至少消耗 2个 RTT。跨国网络下,这可能就是 500ms+ 的时间)
  4. 发送 HTTP 数据
  5. 接收数据

第二次请求(复用):

  1. (跳过 DNS)
  2. (跳过 TCP 握手)
  3. (跳过 TLS 握手) -> 省下了最耗时的部分!
  4. 发送 HTTP 数据
  5. 接收数据

所以,在移动端开发中,保持 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),Connection header 甚至被视为非法或被忽略,因为连接默认就是持久且多路复用的。

2. 为什么日志里可能看不到?(拦截器陷阱)

现在的代码是这样写的:

kotlin 复制代码
// ...
.addInterceptor(logging) // <--- 注意这里
// ...

这叫做 Application Interceptor(应用拦截器)。

OkHttp 的工作流程是这样的:

  1. 应用拦截器 (Application Interceptor) :就是 logging。这时候请求还是"原始"的,是代码里写的样子。OkHttp 还没来得及加默认 Header。
  2. 桥接拦截器 (BridgeInterceptor) :OkHttp 内部的一个拦截器。它负责补全 Header(比如 Host, User-Agent, Gzip, Connection: Keep-Alive)。
  3. 网络拦截器 (Network Interceptor):在真正发包之前。
  4. 发送网络请求。

结论 :因为日志拦截器在第 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(不用改代码)

  1. 运行 App。
  2. 打开 Android Studio 底部的 App Inspection 或 Network Inspector 标签页。
  3. 点击发起一个网络请求。
  4. 点击左侧列表里的请求,查看右侧的 Request Headers。
  5. 会看到 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 会帮助维护连接池的原因

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui