[Android 从零到一] OkHttp 连接池与请求复用:从线上慢请求到网络层治理
[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:
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 作为应用级单例,由依赖注入统一管理:
@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、请求头、响应体等阶段。它比在拦截器里手动打点更接近网络真实链路。
一个简化版本如下:
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)
}
}
再通过工厂为每个请求创建独立监听器:
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
示例上报模型:
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,并根据文件大小和网络状态配置更长的读写超时。
示例:
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 层处理。
一个相对克制的认证拦截器:
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
- 对接口超时做业务分级
- 对错误重试做幂等保护
这些工作不一定一次完成,但只要网络层有稳定埋点,后续优化就有依据。
一个推荐的工程结构
在中大型项目里,可以把网络层拆成下面几块:
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、稳定复用连接、能拆解请求阶段、能识别排队和建连问题、能按业务类型配置超时,并且把拦截器控制在合理边界内。
当这些能力补齐后,网络问题就不再只能靠猜,而是可以用数据定位、用结构治理、用工程化手段持续改进。

浙公网安备 33010602011771号