首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >OkHttp 连接池与请求复用:从线上慢请求到网络层治理

OkHttp 连接池与请求复用:从线上慢请求到网络层治理

原创
作者头像
hunter android
发布2026-07-24 10:29:44
发布2026-07-24 10:29:44
30
举报

[Android 从零到一] OkHttp 连接池与请求复用:从线上慢请求到网络层治理

很多 Android 应用的网络慢,并不是接口本身慢,而是客户端在重复建连、TLS 握手、DNS 解析、请求排队和连接复用上浪费了时间。OkHttp 已经把连接池、Keep-Alive、HTTP/2 多路复用和拦截器体系封装得足够成熟,但如果只把它当成一个普通 HTTP 工具,很容易在项目规模变大后遇到慢请求、偶发超时和资源浪费。

这篇文章从线上慢请求排查的角度,讲清 OkHttp 连接复用的核心机制,以及在 Android 项目里如何把网络层治理成可观测、可复用、可演进的基础设施。

问题为什么会出现

一个常见现象是:同一个接口在服务端耗时只有几十毫秒,但客户端日志里总耗时却动辄数百毫秒,偶尔还会超过一两秒。继续拆开看,会发现耗时可能花在这些阶段:

  • DNS 查询耗时不稳定
  • TCP 建连被频繁触发
  • TLS 握手重复发生
  • 请求在 Dispatcher 队列里等待
  • 连接池没有命中已有连接
  • HTTP/2 连接没有被充分复用
  • 拦截器里做了过重的同步逻辑

如果团队只记录接口开始和结束时间,就只能看到“慢了”,很难知道慢在哪里。网络层治理的第一步,是把一次请求拆成可解释的多个阶段。

OkHttp 的连接复用在做什么

OkHttp 内部维护了一个连接池。请求完成后,底层 Socket 不会马上关闭,而是进入空闲状态等待后续请求复用。只要目标地址、代理、证书、协议等条件满足,新请求就可以复用已有连接,省掉 TCP 建连和 TLS 握手。

对于 HTTPS 请求,这个收益很明显。一次完整建连通常包含:

  • DNS 解析
  • TCP 三次握手
  • TLS 握手
  • 协议协商
  • 请求发送与响应读取

连接复用命中后,请求可以直接进入发送阶段。移动网络波动越明显,复用收益越大。

OkHttp 默认连接池配置已经适合多数应用:最多保留若干空闲连接,空闲一段时间后回收。真正需要调整的场景通常不是“默认不够强”,而是业务自己破坏了复用条件。

最容易破坏复用的写法

很多项目慢请求的根源,是每次请求都创建新的 OkHttpClient:

代码语言:javascript
复制
fun createApi(): ApiService {
    val client = OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .readTimeout(10, TimeUnit.SECONDS)
        .build()

    return Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()
        .create(ApiService::class.java)
}

这段代码看起来干净,问题却很重:每个 client 都有自己的连接池、线程池和调度器。频繁创建 client 会让连接复用失效,也会带来额外资源开销。

更稳妥的做法是把 OkHttpClient 作为应用级单例,由依赖注入统一管理:

代码语言:javascript
复制
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(
        authInterceptor: AuthInterceptor,
        eventListenerFactory: EventListener.Factory
    ): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .eventListenerFactory(eventListenerFactory)
            .addInterceptor(authInterceptor)
            .build()
    }
}

同一个业务域名、同一套证书和同一个 client,才能稳定享受连接池带来的收益。

用 EventListener 拆开慢请求

OkHttp 提供了 EventListener,可以监听 DNS、连接、TLS、请求头、响应体等阶段。它比在拦截器里手动打点更接近网络真实链路。

一个简化版本如下:

代码语言:javascript
复制
class NetworkTimingEventListener(
    private val callId: String,
    private val reporter: NetworkReporter
) : EventListener() {

    private val marks = linkedMapOf<String, Long>()

    private fun mark(name: String) {
        marks[name] = SystemClock.elapsedRealtime()
    }

    override fun callStart(call: Call) = mark("callStart")

    override fun dnsStart(call: Call, domainName: String) = mark("dnsStart")

    override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) = mark("dnsEnd")

    override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) = mark("connectStart")

    override fun secureConnectStart(call: Call) = mark("tlsStart")

    override fun secureConnectEnd(call: Call, handshake: Handshake?) = mark("tlsEnd")

    override fun connectionAcquired(call: Call, connection: Connection) = mark("connectionAcquired")

    override fun requestHeadersStart(call: Call) = mark("requestHeadersStart")

    override fun responseHeadersEnd(call: Call, response: Response) = mark("responseHeadersEnd")

    override fun callEnd(call: Call) {
        mark("callEnd")
        reporter.report(callId, marks)
    }

    override fun callFailed(call: Call, ioe: IOException) {
        mark("callFailed")
        reporter.report(callId, marks, ioe)
    }
}

再通过工厂为每个请求创建独立监听器:

代码语言:javascript
复制
class TimingEventListenerFactory(
    private val reporter: NetworkReporter
) : EventListener.Factory {
    override fun create(call: Call): EventListener {
        val callId = UUID.randomUUID().toString()
        return NetworkTimingEventListener(callId, reporter)
    }
}

有了这些阶段数据,排查会从“接口慢”变成更具体的问题:DNS 慢、建连慢、TLS 慢、服务端慢、响应体读取慢,或者本地排队慢。

连接池命中怎么判断

如果一次请求没有触发 connectStart 和 secureConnectStart,却直接拿到了 connectionAcquired,通常说明复用了已有连接。可以在上报里加入这些字段:

  • host
  • protocol
  • reusedConnection
  • dnsDuration
  • connectDuration
  • tlsDuration
  • serverDuration
  • totalDuration

示例上报模型:

代码语言:javascript
复制
data class NetworkTrace(
    val urlHost: String,
    val protocol: String?,
    val reusedConnection: Boolean,
    val dnsMs: Long?,
    val connectMs: Long?,
    val tlsMs: Long?,
    val serverMs: Long?,
    val totalMs: Long,
    val error: String?
)

线上看趋势时,重点不是单次请求,而是连接复用率和慢阶段分布。比如同一个域名在 Wi-Fi 下复用率很高,在移动网络下复用率很低,就要继续看连接被回收、网络切换、证书配置和超时策略。

Dispatcher 也会影响体感速度

OkHttp 的 Dispatcher 控制并发请求数量。默认配置对多数 App 足够,但如果项目里有大量图片、埋点、预加载和业务接口同时走同一个 client,就可能出现关键接口排队。

可以按业务重要性拆分 client,但要谨慎:拆太多会降低连接复用收益。更推荐的做法是:

  • 图片加载交给图片库自己的 client 或专用 client
  • 业务 API 使用统一 client
  • 大文件上传下载使用独立 client
  • 埋点请求做批量上报,减少小请求洪峰

这样既能保护关键接口,也不会把连接池拆得太碎。

超时配置不要只看一个值

很多项目把所有超时都设成同一个值,例如 30 秒。这会让问题难以暴露,也可能让用户等待太久。OkHttp 里常见超时包括:

  • connectTimeout:连接建立超时
  • readTimeout:读取响应超时
  • writeTimeout:发送请求体超时
  • callTimeout:整个请求生命周期超时

对于普通 JSON 接口,可以把连接超时设短一点,把读取超时控制在可接受范围。对于上传下载,则应该使用独立 client,并根据文件大小和网络状态配置更长的读写超时。

示例:

代码语言:javascript
复制
val apiClient = baseClient.newBuilder()
    .connectTimeout(8, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .callTimeout(20, TimeUnit.SECONDS)
    .build()

val uploadClient = baseClient.newBuilder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .writeTimeout(120, TimeUnit.SECONDS)
    .callTimeout(0, TimeUnit.SECONDS)
    .build()

注意,newBuilder 会复用原 client 的连接池和调度器,适合在基础 client 上扩展差异化配置。

拦截器要保持克制

拦截器很强,但也很容易被滥用。常见问题包括:

  • 在拦截器里同步读写大量本地文件
  • 每次请求都刷新 token
  • 打印完整大响应体
  • 在主流程里做复杂加解密
  • 错误重试没有幂等判断

拦截器应该更像网络层的管道,而不是业务逻辑容器。认证、公共 Header、轻量日志、错误码归一化可以放进去;复杂业务决策应该回到 Repository 或 UseCase 层处理。

一个相对克制的认证拦截器:

代码语言:javascript
复制
class AuthInterceptor(
    private val tokenProvider: TokenProvider
) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val token = tokenProvider.cachedToken()
        val request = chain.request().newBuilder()
            .apply {
                if (!token.isNullOrBlank()) {
                    header("Authorization", "Bearer $token")
                }
            }
            .build()

        return chain.proceed(request)
    }
}

如果需要刷新 token,应明确处理并发刷新、重试次数和请求幂等,不要让每个请求各自刷新。

线上治理建议

网络层治理可以按三个层级推进。

基础层:

  • 统一 OkHttpClient 创建
  • 明确业务 API、图片、上传下载的 client 边界
  • 接入 EventListener 阶段耗时
  • 记录请求 ID、host、path、状态码和错误类型

分析层:

  • 统计连接复用率
  • 统计 DNS、连接、TLS、服务端和总耗时分位值
  • 识别排队时间过长的请求
  • 按网络类型、系统版本、地域和运营商拆分问题

治理层:

  • 对慢域名启用更稳定的 DNS 策略
  • 对高频小请求做合并或缓存
  • 对上传下载拆独立 client
  • 对接口超时做业务分级
  • 对错误重试做幂等保护

这些工作不一定一次完成,但只要网络层有稳定埋点,后续优化就有依据。

一个推荐的工程结构

在中大型项目里,可以把网络层拆成下面几块:

代码语言:javascript
复制
network/
  NetworkModule.kt
  ApiClientFactory.kt
  AuthInterceptor.kt
  ErrorMappingInterceptor.kt
  TimingEventListener.kt
  NetworkReporter.kt
  NetworkTrace.kt

职责保持清晰:

  • NetworkModule 负责依赖注入
  • ApiClientFactory 负责 Retrofit 创建
  • Interceptor 负责请求管道增强
  • EventListener 负责阶段耗时
  • Reporter 负责埋点上报
  • Trace 负责结构化数据

这样做的好处是,业务侧只关心接口调用,网络层自己负责稳定性、观测和治理。

总结

OkHttp 的连接池和请求复用,不只是性能优化细节,而是 Android 网络层稳定性的基础。很多线上慢请求,真正的问题不在接口业务逻辑,而在客户端没有复用连接、没有拆分耗时、没有区分不同类型请求。

一个成熟的网络层,应该做到:统一 client、稳定复用连接、能拆解请求阶段、能识别排队和建连问题、能按业务类型配置超时,并且把拦截器控制在合理边界内。

当这些能力补齐后,网络问题就不再只能靠猜,而是可以用数据定位、用结构治理、用工程化手段持续改进。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • [Android 从零到一] OkHttp 连接池与请求复用:从线上慢请求到网络层治理
    • 问题为什么会出现
    • OkHttp 的连接复用在做什么
    • 最容易破坏复用的写法
    • 用 EventListener 拆开慢请求
    • 连接池命中怎么判断
    • Dispatcher 也会影响体感速度
    • 超时配置不要只看一个值
    • 拦截器要保持克制
    • 线上治理建议
    • 一个推荐的工程结构
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档