很多 Android 应用的网络慢,并不是接口本身慢,而是客户端在重复建连、TLS 握手、DNS 解析、请求排队和连接复用上浪费了时间。OkHttp 已经把连接池、Keep-Alive、HTTP/2 多路复用和拦截器体系封装得足够成熟,但如果只把它当成一个普通 HTTP 工具,很容易在项目规模变大后遇到慢请求、偶发超时和资源浪费。
这篇文章从线上慢请求排查的角度,讲清 OkHttp 连接复用的核心机制,以及在 Android 项目里如何把网络层治理成可观测、可复用、可演进的基础设施。
一个常见现象是:同一个接口在服务端耗时只有几十毫秒,但客户端日志里总耗时却动辄数百毫秒,偶尔还会超过一两秒。继续拆开看,会发现耗时可能花在这些阶段:
如果团队只记录接口开始和结束时间,就只能看到“慢了”,很难知道慢在哪里。网络层治理的第一步,是把一次请求拆成可解释的多个阶段。
OkHttp 内部维护了一个连接池。请求完成后,底层 Socket 不会马上关闭,而是进入空闲状态等待后续请求复用。只要目标地址、代理、证书、协议等条件满足,新请求就可以复用已有连接,省掉 TCP 建连和 TLS 握手。
对于 HTTPS 请求,这个收益很明显。一次完整建连通常包含:
连接复用命中后,请求可以直接进入发送阶段。移动网络波动越明显,复用收益越大。
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,才能稳定享受连接池带来的收益。
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,通常说明复用了已有连接。可以在上报里加入这些字段:
示例上报模型:
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 下复用率很高,在移动网络下复用率很低,就要继续看连接被回收、网络切换、证书配置和超时策略。
OkHttp 的 Dispatcher 控制并发请求数量。默认配置对多数 App 足够,但如果项目里有大量图片、埋点、预加载和业务接口同时走同一个 client,就可能出现关键接口排队。
可以按业务重要性拆分 client,但要谨慎:拆太多会降低连接复用收益。更推荐的做法是:
这样既能保护关键接口,也不会把连接池拆得太碎。
很多项目把所有超时都设成同一个值,例如 30 秒。这会让问题难以暴露,也可能让用户等待太久。OkHttp 里常见超时包括:
对于普通 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 上扩展差异化配置。
拦截器很强,但也很容易被滥用。常见问题包括:
拦截器应该更像网络层的管道,而不是业务逻辑容器。认证、公共 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,应明确处理并发刷新、重试次数和请求幂等,不要让每个请求各自刷新。
网络层治理可以按三个层级推进。
基础层:
分析层:
治理层:
这些工作不一定一次完成,但只要网络层有稳定埋点,后续优化就有依据。
在中大型项目里,可以把网络层拆成下面几块:
network/
NetworkModule.kt
ApiClientFactory.kt
AuthInterceptor.kt
ErrorMappingInterceptor.kt
TimingEventListener.kt
NetworkReporter.kt
NetworkTrace.kt职责保持清晰:
这样做的好处是,业务侧只关心接口调用,网络层自己负责稳定性、观测和治理。
OkHttp 的连接池和请求复用,不只是性能优化细节,而是 Android 网络层稳定性的基础。很多线上慢请求,真正的问题不在接口业务逻辑,而在客户端没有复用连接、没有拆分耗时、没有区分不同类型请求。
一个成熟的网络层,应该做到:统一 client、稳定复用连接、能拆解请求阶段、能识别排队和建连问题、能按业务类型配置超时,并且把拦截器控制在合理边界内。
当这些能力补齐后,网络问题就不再只能靠猜,而是可以用数据定位、用结构治理、用工程化手段持续改进。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。