OkHttp 架构剖析:从拦截器链到连接池,源码级理解网络请求

25 阅读13分钟

Android 网络深度系列 · 第 3 篇

系列导航:第1篇:HTTP 协议全解 | 第2篇:HTTPS 与网络安全 | 第3篇:OkHttp 架构剖析 | 第4篇:Retrofit 原理与实战 | 第5篇:WebSocket 与长连接 | 第6篇:网络实战场景

前言

如果你问一个 Android 开发者:"你的 App 怎么发网络请求?"答案十有八九是 Retrofit。再追问一句:"Retrofit 底层呢?"——OkHttp。

OkHttp 是 Square 出品的高性能 HTTP 客户端,Android 4.4+ 的 HttpURLConnection 底层实现已经被替换为 OkHttp。可以说,每个 Android 应用的每一次网络请求,最终都经过 OkHttp 之手

但大多数人停留在"会用"的层面:OkHttpClientRequestexecute / enqueue。线上出现网络问题、性能瓶颈、403 重定向异常时,不了解架构就只能靠猜。

本文从源码出发,把 OkHttp 从起点到终点的完整链路走一遍,重点拆解拦截器链(Interceptor Chain)连接池(ConnectionPool) 。读完后你会发现,OkHttp 不只是一个网络库,更是一份责任链模式和连接复用的教科书级实现。

基于 OkHttp 4.x(okhttp v4.12.0),代码以 Kotlin 为主。


一、整体架构概览

一次网络请求的完整旅程

                 OkHttpClient
                      │
                    Call ─── RealCall
                      │
                 ┌────┴────┐
              execute     enqueue    (同步 / 异步)
                 │          │
              Dispatcher ───┘
                 │
     ┌───────────┼───────────┐
     │           │           │
  Running    Ready(Async)  Finished
     │
     ▼
Interceptor Chain(灵魂所在)
     │
 Application Interceptors(用户自定义)
     │
 RetryAndFollowUpInterceptor    ← 重试 & 重定向
     │
 BridgeInterceptor              ← 补请求头 / gzip / Cookie
     │
 CacheInterceptor              ← 缓存策略
     │
 ConnectInterceptor            ← 建立连接 / 连接池复用
     │
 Network Interceptors(用户自定义)
     │
 CallServerInterceptor         ← 实际写请求 / 读响应
     │
     ▼
   Response

同步调用 vs 异步调用

同步调用(execute):

val client = OkHttpClient()
val request = Request.Builder().url("https://api.example.com/data").build()
val call = client.newCall(request)
val response = call.execute() // 必须在子线程,阻塞直到返回

同步路径很简单:RealCall.execute() 调用 Dispatcher.executed(this) 将 Call 加入 runningSyncCalls,然后同步执行拦截器链。当前线程阻塞直到拿到响应。

异步调用(enqueue):

client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) { /* 失败回调 */ }
    override fun onResponse(call: Call, response: Response) { /* 响应回调 */ }
})

核心在 RealCall.enqueue()

override fun enqueue(responseCallback: Callback) {
    check(executed.compareAndSet(false, true)) { "Already Executed" }
    client.dispatcher.enqueue(AsyncCall(responseCallback))
}

AsyncCallRunnable 子类,持有 Callback 引用。Dispatcher 决定执行时,提交到线程池运行。

Dispatcher 线程池设计

val executorService: ExecutorService = ThreadPoolExecutor(
    0,                     // corePoolSize = 0,空闲时全部销毁
    Int.MAX_VALUE,         // maximumPoolSize 无上限(受 maxRequests 约束)
    60, TimeUnit.SECONDS,  // 空闲存活 60s
    SynchronousQueue(),    // 直接提交
    threadFactory("OkHttp Dispatcher", false)
)

核心参数

参数默认值解释
maxRequests64同时执行的最大请求数
maxRequestsPerHost5同一主机最大并发

maxRequests=64 防止线程无限增长导致 CPU 上下文切换开销过大;maxRequestsPerHost=5 限制同一服务器的并发连接——HTTP/2 单连接已支持多路复用,开太多连接反而因拥塞控制互相干扰。

调度源码:

private fun promoteAndExecute(): Boolean {
    val executableCalls = mutableListOf<AsyncCall>()
    synchronized(this) {
        val i = readyAsyncCalls.iterator()
        while (i.hasNext()) {
            val asyncCall = i.next()
            if (runningAsyncCalls.size >= maxRequests) break
            if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue
            i.remove()
            asyncCall.callsPerHost().incrementAndGet()
            executableCalls.add(asyncCall)
            runningAsyncCalls.add(asyncCall)
        }
    }
    for (i in 0 until executableCalls.size) {
        executableCalls[i].executeOn(executorService)
    }
    return executableCalls.isNotEmpty()
}

每次请求完成(finished())时调用 promoteAndExecute(),从等待队列取出满足条件的请求提交到线程池。


二、拦截器链(责任链模式)

责任链模式原理

责任链模式:多个处理器首尾相连成链,请求沿链传递,每个处理器决定自己处理或交给下一个。

OkHttp 将一次 HTTP 请求拆解为多个独立阶段,每个阶段一个拦截器:

Request → [拦截器1][拦截器2] → ... → [CallServer] → Response

每个拦截器只关心自己那一环,可扩展性极强:用户可以插入自定义拦截器,不需要改 OkHttp 源码。

RealInterceptorChain 的 proceed() 递归调用

class RealInterceptorChain(
    private val interceptors: List<Interceptor>,
    private val index: Int,
    private val request: Request,
    ...
) : Interceptor.Chain {
​
    override fun proceed(request: Request): Response {
        check(index < interceptors.size)
        val next = copy(index = index + 1, request = request)
        val interceptor = interceptors[index]
        val response = interceptor.intercept(next) as Response
        return response
    }
}

每个拦截器做两件事:① 处理请求(Before)② 调用 chain.proceed() 交给下一个,递归深入直到 CallServerInterceptor,然后响应层层返回。

Interceptor A.intercept(chain) {
    // before:处理请求
    response = chain.proceed(request)  // → 进入 Interceptor B
    // after:处理响应
    return response
}

这允许拦截器在 proceed() 前后分别处理请求和响应。

Application Interceptor vs Network Interceptor

val client = OkHttpClient.Builder()
    .addInterceptor(MyAppInterceptor())         // Application
    .addNetworkInterceptor(MyNetInterceptor())  // Network
    .build()
对比维度Application InterceptorNetwork Interceptor
位置链的最前面链的最后面
调用次数1 次可能多次(每次重试/重定向都调)
含义处理逻辑请求处理实际网络流量
是否看到重定向❌ 只看最终响应✅ 看到每次中间响应
是否看到缓存命中✅ 能❌ 缓存命中不走这里

一句话:Application 看"逻辑",Network 看"物理"

五大内置拦截器逐个拆解


2.1 RetryAndFollowUpInterceptor

职责:自动重试失败请求、跟随重定向。

重试逻辑

private fun recover(
    e: IOException,
    routeException: RouteException?,
    userRequest: Request,
    shouldSendRequest: Boolean
): Boolean {
    if (call.getAndIncrementFailedRun() > maxRetries) return false  // 最多 20 次
    if (routeException != null && !requestSendStarted) return true   // 换路由重试
    if (!shouldSendRequest) return false  // 请求体不可重发
    if (e is ProtocolException) return false
    if (e is InterruptedIOException) return false
    if (e is SocketTimeoutException && !requestSendStarted) return false
    return true
}

RouteException vs IOException

  • RouteException:连接建立阶段(DNS/TCP 失败),可以换路由重试
  • IOException:请求发送或响应读取阶段,大部分可重试,但 ProtocolException、InterruptedIOException、SocketTimeoutException 不重试

重定向跟随

override fun intercept(chain: Interceptor.Chain): Response {
    var request = chain.request()
    var response: Response? = null
​
    while (true) {
        if (followUpCount++ > MAX_FOLLOW_UPS)  // 最多 20 次
            throw ProtocolException("Too many follow-up requests")
​
        response?.close()
        response = if (response == null) chain.proceed(request)
                   else chain.proceed(followUp.request(request)) // 重定向新请求
​
        val followUp = followUpRequest(response, exchange)
        if (followUp == null) return response  // 不需要跟随,返回
    }
}

重定向判断(followUpRequest 简化):

return when (response.code) {
    300, 301, 302, 303 -> { /* POST 转 GET */ }
    307, 308 -> { /* 严格保留方法 */ }
    401 -> authenticate(response, exchange)
    407 -> authenticate(response, exchange) // 代理认证
    408 -> { /* 仅复用连接超时可重试 */ }
    503 -> { /* 有 Retry-After 不自动重试 */ }
    else -> null
}

2.2 BridgeInterceptor

职责:连接用户代码和网络协议,补充 HTTP 协议要求的请求头。

override fun intercept(chain: Interceptor.Chain): Response {
    val userRequest = chain.request()
    val requestBuilder = userRequest.newBuilder()
​
    val body = userRequest.body
    if (body != null) {
        body.contentType()?.let { requestBuilder.header("Content-Type", it.toString()) }
        val contentLength = body.contentLength()
        if (contentLength != -1L) {
            requestBuilder.header("Content-Length", contentLength.toString())
            requestBuilder.removeHeader("Transfer-Encoding")
        } else {
            requestBuilder.header("Transfer-Encoding", "chunked")
        }
    }
​
    requestBuilder.header("Host", userRequest.url.toHostHeader())
    requestBuilder.header("Connection", "Keep-Alive")
​
    // 自动 gzip
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
        transparentGzip = true
        requestBuilder.header("Accept-Encoding", "gzip")
    }
​
    // Cookie
    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
        requestBuilder.header("Cookie", cookieHeader(cookies))
    }
​
    // User-Agent
    if (userRequest.header("User-Agent") == null) {
        requestBuilder.header("User-Agent", userAgent)
    }
​
    val networkResponse = chain.proceed(requestBuilder.build())
​
    // 自动解压 gzip
    val responseBuilder = networkResponse.newBuilder().request(userRequest)
    if (transparentGzip && "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true)) {
        val gzipSource = GzipSource(networkResponse.body!!.source())
        responseBuilder.body(RealResponseBody(
            networkResponse.headers.newBuilder()
                .removeAll("Content-Encoding").removeAll("Content-Length").build(),
            OkIO.source(gzipSource)))
    }
​
    return responseBuilder.build()
}

自动 gzip:请求加 Accept-Encoding: gzip,响应检测 Content-Encoding: gzip 自动解压,上层无感知。

Cookie 管理:OkHttp 不实现存储,只通过 CookieJar 回调:

  • 发送:cookieJar.loadForRequest(url) 获取
  • 接收:cookieJar.saveFromResponse(url, cookies) 保存

默认 CookieJar.NO_COOKIES,需要持久化需自行实现。


2.3 CacheInterceptor

职责:HTTP 缓存策略的实现者。

CacheStrategy 决策逻辑

override fun intercept(chain: Interceptor.Chain): Response {
    val cacheCandidate = cache?.get(chain.request())
    val now = System.currentTimeMillis()
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse
​
    if (networkRequest == null) {
        // 强缓存命中,直接返回
        return cacheResponse!!.newBuilder().cacheResponse(stripBody(cacheResponse)).build()
    }
​
    cacheResponse?.close()
    val networkResponse = chain.proceed(networkRequest)
​
    // 更新缓存
    if (cache != null) {
        if (HttpMethod.invalidatesCache(networkRequest.method)) cache.remove(networkRequest)
        else if (CacheStrategy.isCacheable(networkResponse, networkRequest)) cache.put(networkResponse)
    }
​
    return networkResponse
}

三种结果:

场景networkRequestcacheResponse结果
强缓存有效null有值不发请求,直接返回缓存
需要协商带 If-None-Match/If-Modified-Since有值发请求协商,304 复用缓存
无缓存原始请求null正常请求
// CacheStrategy.Factory.compute() 简化
fun compute(): CacheStrategy {
    val cacheResponse = this.cacheResponse
    if (cacheResponse == null) return CacheStrategy(request, null)
    if (request.cacheControl.noCache()) return CacheStrategy(request, null)
​
    val ageMillis = cacheResponse.cacheResponseAge()
    var freshMillis = cacheResponse.cacheControlMaxAgeSeconds() * 1000L
    if (freshMillis == 0L) freshMillis = cacheResponse.expiresTime() - nowMillis
​
    if (ageMillis < freshMillis) return CacheStrategy(null, cacheResponse) // 强缓存
​
    // 协商缓存
    val conditionalRequest = request.newBuilder()
    cacheResponse.header("ETag")?.let { conditionalRequest.header("If-None-Match", it) }
    cacheResponse.header("Last-Modified")?.let { conditionalRequest.header("If-Modified-Since", it) }
​
    return CacheStrategy(conditionalRequest.build(), cacheResponse)
}

DiskLruCache 底层

缓存存储结构:

cache_dir/
  journal        # 操作日志,重启重建索引
  1a2b3c.0       # 元数据(URL、请求头、响应头等)
  1a2b3c.1       # 响应体二进制数据

每次写入新缓存时检查容量,LRU 策略淘汰。


2.4 ConnectInterceptor

职责:建立(或复用)TCP 连接。

object ConnectInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val realChain = chain as RealInterceptorChain
        val exchange = realChain.call.initExchange(realChain)
        return realChain.proceed(exchange.request)
    }
}

真正工作在 ExchangeFinder 中。

连接建立完整流程

initExchange()
    │
    ▼
ExchangeFinder.find()
    │
    ├── 1. 尝试从连接池获取复用连接
    │       ├── 匹配 → 复用
    │       └── 不匹配 → 下一步
    │
    ├── 2. RouteSelector.next() 选择路由
    │       │  Route(代理类型, IP, Port)
    │       │
    │       ├── RealConnection.connect()
    │       │   ├── TCP 三次握手
    │       │   ├── TLS 握手(HTTPS)
    │       │   ├── HTTP/2 协商(ALPN)
    │       │   └── 初始化 HTTP/2 连接
    │       │
    │       └── 新连接加入连接池
    │
    └── 3. 创建 Exchange → 返回

连接池复用

// OkHttp 4.x 简化版(实际通过 ExchangeFinder 调用)
internal fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
): Boolean {
    for (connection in connections) {
        if (requireMultiplexed && !connection.isMultiplexed) continue
        if (!connection.isEligible(address, routes)) continue
        call.acquireConnectionNoEvents(connection)
        return true
    }
    return false
}

连接复用匹配条件

fun isEligible(address: Address, routes: List<Route>?): Boolean {
    if (!isAlive()) return false
    // 校验 dns、proxySelector、sslSocketFactory 等配置是否一致
    if (this.route.address.url.host != address.url.host) return false
​
    // HTTP/2 连接可被多个请求复用
    if (protocol == Protocol.HTTP_2) return true
​
    // HTTP/1.1 连接必须空闲
    return isIdle()
}

HTTP/1.1:一个连接一次只能处理一个请求,用完还池。 HTTP/2:多路复用,一个连接同时处理多个请求,只需主机名匹配。

路由选择(RouteSelector)

路由选择顺序:代理 → DNS → 端口

fun next(): Route {
    while (true) {
        if (proxies == null) proxies = retrieveProxies()
        val proxy = proxies!![nextProxyIndex]
​
        if (inetSocketAddresses == null) {
            inetSocketAddresses = address.dns.lookup(address.url.host)
                .map { InetSocketAddress(it, address.url.port) }
        }
​
        val route = Route(address, proxy, inetSocketAddresses!![nextInetSocketAddressIndex])
​
        // 跳过之前失败的路由
        if (!routeDatabase.shouldPostpone(route)) return route
        nextInetSocketAddressIndex++
    }
}

RouteDatabase 记录失败路由,在冷却期内跳过。


2.5 CallServerInterceptor

职责:最后一个拦截器,实际发送请求并读取响应。

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val realChain = chain as RealInterceptorChain
        val exchange = realChain.exchange!!
        val request = realChain.request
        var responseBuilder: Response.Builder? = null
​
        if (HttpMethod.permitsRequestBody(request.method) && request.body != null) {
            when {
                "100-continue".equals(request.header("Expect"), ignoreCase = true) -> {
                    exchange.flushRequest()
                    responseBuilder = exchange.readResponseHeaders(expectContinue = true)
                }
                else -> {
                    exchange.writeRequestHeaders(request)
                    exchange.createRequestBody(request, true).writeTo(...)
                }
            }
        } else {
            exchange.writeRequestHeaders(request)
        }
​
        if (responseBuilder == null) {
            responseBuilder = exchange.readResponseHeaders(expectContinue = false)
        }
​
        var response = responseBuilder
            .request(request)
            .handshake(exchange.connection.handshake())
            .build()
​
        if (response.body == null && response.promisesBody()) {
            response = response.newBuilder()
                .body(exchange.openResponseBody(response))
                .build()
        }
​
        return response
    }
}

Exchange / ExchangeCodec

Exchange 封装一次 HTTP 交互,底层使用 ExchangeCodec

  • HTTP/1.1Http1ExchangeCodec(文本协议,逐行写入)
  • HTTP/2Http2ExchangeCodec(二进制帧,HPACK 压缩头)

核心差异:

维度HTTP/1.1HTTP/2
格式文本二进制帧
头压缩HPACK
并发单连接单请求多路复用
编码器Http1ExchangeCodecHttp2ExchangeCodec

三、连接池深度

清理机制

class ConnectionPool(
    maxIdleConnections: Int = 5,
    keepAliveDuration: Long = 5,
    timeUnit: TimeUnit = TimeUnit.MINUTES
) {
    private val cleanupRunnable = object : Runnable {
        override fun run() {
            while (true) {
                val waitNanos = cleanup(System.nanoTime())
                if (waitNanos == -1L) return  // 无连接,停止
                if (waitNanos > 0L) {
                    synchronized(this@ConnectionPool) {
                        (this@ConnectionPool as Object).wait(
                            waitNanos / 1_000_000,
                            (waitNanos % 1_000_000).toInt()
                        )
                    }
                }
            }
        }
    }
}

cleanup() 计算逻辑

fun cleanup(now: Long): Long {
    var idleCount = 0
    var longestIdleDuration = Long.MIN_VALUE
    var longestIdleConnection: RealConnection? = null
​
    synchronized(this) {
        for (connection in connections) {
            if (connection.isIdle()) {
                idleCount++
                val idleDuration = now - connection.idleAtNanos
                if (idleDuration > longestIdleDuration) {
                    longestIdleDuration = idleDuration
                    longestIdleConnection = connection
                }
            }
        }
​
        // 清理策略
        when {
            // 1. 空闲数超过 maxIdleConnections → 关闭最久空闲的
            idleCount > maxIdleConnections -> {
                connections.remove(longestIdleConnection!!)
                longestIdleConnection!!.socket().closeQuietly()
                return 0L  // 立即再次清理
            }
            // 2. 有空闲连接 → 计算下一个清理时间
            longestIdleDuration >= keepAliveDurationNs -> {
                connections.remove(longestIdleConnection!!)
                longestIdleConnection!!.socket().closeQuietly()
                return 0L
            }
            // 3. 有空闲连接但未超时 → 等超时后清理
            idleCount > 0 -> {
                return keepAliveDurationNs - longestIdleDuration
            }
            // 4. 有活跃连接 → 等 keepAliveDuration 后再检查
            connections.isNotEmpty() -> {
                return keepAliveDurationNs
            }
            // 5. 没有连接了 → 停止清理
            else -> return -1L
        }
    }
}

调度逻辑

  • 空闲连接数超过 maxIdleConnections(5) → 关闭最久的空闲连接
  • 有空闲连接且超时 → 关闭最久空闲的连接
  • 有空闲但未超时 → 等剩余时间到了再清理
  • 无空闲但有活跃 → 等 keepAliveDuration(5min) 再检查
  • 无连接 → 停止清理线程

每次清理后立即检查是否还有需要清理的,直到完全满足条件才 wait()

连接复用匹配条件

HTTP/1.1 复用:域名、端口、代理、SSL 配置全部一致,且连接当前空闲。 HTTP/2 复用:只要主机名匹配即可(多路复用)。

HTTP/2 连接合并

如果 api.example.comstatic.example.com 解析到同一 IP,HTTP/2 可以共享连接(通过 CONNECT 请求)。OkHttp 的 isEligible() 对 HTTP/2 更宽松,如果一个连接的 host*.example.com,匹配时忽略子域名。


四、自定义拦截器实战

日志拦截器

class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.nanoTime()
​
        log("→ ${request.method} ${request.url}")
        request.headers.forEach {
            if (it.first != "Authorization") log("  ${it.first}: ${it.second}")
        }
​
        request.body?.let {
            val buffer = Buffer()
            it.writeTo(buffer)
            log("  Body: ${buffer.readUtf8()}")
        }
​
        val response = chain.proceed(request)
        val elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)
​
        log("← ${response.code} ${response.message} (${elapsedMs}ms)")
        response.body?.let {
            val source = it.source()
            source.request(Long.MAX_VALUE)
            log("  Body: ${source.buffer.clone().readUtf8()}")
        }
​
        return response
    }
}

注意response.body.string() 只能读一次。日志拦截器中用 buffer.clone() 查看内容,不消耗原始数据。

Token 自动刷新拦截器(带并发控制)

多请求同时 401 时只刷新一次 Token:

class TokenInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    private val refreshLock = Any()
​
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        val accessToken = tokenProvider.getAccessToken()
        request = request.newBuilder()
            .header("Authorization", "Bearer $accessToken")
            .build()
​
        val response = chain.proceed(request)
​
        if (response.code == 401) {
            response.close()
​
            val newToken = synchronized(refreshLock) {
                val currentToken = tokenProvider.getAccessToken()
                if (currentToken != accessToken) currentToken  // 其他线程已刷新
                else tokenProvider.refreshToken()  // 只有第一个线程进来刷新
            }
​
            if (newToken == null) throw IOException("Token refresh failed")
​
            request = request.newBuilder()
                .header("Authorization", "Bearer $newToken")
                .build()
            return chain.proceed(request)
        }
​
        return response
    }
}
​
interface TokenProvider {
    fun getAccessToken(): String?
    fun refreshToken(): String?
}

关键点:

  1. synchronized(refreshLock) 保证同一时间只有一个线程刷新
  2. 双重检查:刷新后其他线程直接拿新 token
  3. 只重试一次,避免死循环

统一错误处理拦截器

class ErrorHandlingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        try {
            val response = chain.proceed(request)
            if (!response.isSuccessful) {
                when (response.code) {
                    502, 503 -> notifyMaintenance()
                    429 -> notifyRateLimited()
                }
            }
            return response
        } catch (e: SocketTimeoutException) {
            throw NetworkException.Timeout(request.url.toString(), e)
        } catch (e: UnknownHostException) {
            throw NetworkException.NoNetwork(request.url.toString(), e)
        } catch (e: IOException) {
            throw NetworkException.IOError(request.url.toString(), e)
        }
    }
}
​
sealed class NetworkException(message: String, cause: Throwable?) : IOException(message, cause) {
    class Timeout(url: String, cause: Throwable) :
        NetworkException("Request timed out: $url", cause)
    class NoNetwork(url: String, cause: Throwable) :
        NetworkException("No network: $url", cause)
    class IOError(url: String, cause: Throwable) :
        NetworkException("IO error: $url", cause)
}

上层用 when 精确处理不同类型错误:

viewModelScope.launch {
    try { val data = api.fetchData() }
    catch (e: NetworkException) {
        when (e) {
            is NetworkException.Timeout -> showToast("请求超时")
            is NetworkException.NoNetwork -> showToast("网络不可用")
            is NetworkException.IOError -> showToast("网络异常")
        }
    }
}

请求签名拦截器

class SignInterceptor(private val appSecret: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()
        val url = original.url
        val timestamp = System.currentTimeMillis() / 1000
        val nonce = UUID.randomUUID().toString().replace("-", "")
​
        val sign = calculateSign(
            method = original.method,
            path = url.encodedPath,
            queryParams = url.queryParameterNames.associateWith { url.queryParameter(it) ?: "" },
            timestamp = timestamp,
            nonce = nonce,
            secret = appSecret
        )
​
        val signedUrl = url.newBuilder()
            .addQueryParameter("timestamp", timestamp.toString())
            .addQueryParameter("nonce", nonce)
            .addQueryParameter("sign", sign)
            .build()
​
        return chain.proceed(original.newBuilder().url(signedUrl).build())
    }
​
    private fun calculateSign(...): String {
        val sortedParams = queryParams.entries
            .filter { it.key !in setOf("sign", "timestamp", "nonce") }
            .sortedBy { it.key }
            .joinToString("&") { "${it.key}=${it.value}" }
        val signStr = "$method\n$path\n$sortedParams\ntimestamp=$timestamp\nnonce=$nonce\n$secret"
        return HmacSHA256(signStr, secret)
    }
}

五、OkHttp 调优

连接池配置

val client = OkHttpClient.Builder()
    .connectionPool(ConnectionPool(
        maxIdleConnections = 10,       // 默认 5
        keepAliveDuration = 30,        // 默认 5 分钟
        timeUnit = TimeUnit.SECONDS
    ))
    .build()

建议

  • 高频 API(即时通讯/长轮询)→ maxIdleConnections 增到 10-20
  • 低频使用 → keepAliveDuration 减到 15-30s
  • 普通 App → 默认值已够用

超时设置

val client = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)    // DNS + TCP 连接
    .readTimeout(30, TimeUnit.SECONDS)       // 两次数据读取间隔
    .writeTimeout(30, TimeUnit.SECONDS)      // 两次数据写入间隔
    .callTimeout(60, TimeUnit.SECONDS)       // 总超时(4.5+)
    .build()
超时含义
connectTimeout建立连接的最长等待(TCP 三次握手 + TLS)
readTimeout两次数据读取之间的最大间隔
writeTimeout两次数据写入之间的最大间隔
callTimeout整个请求从开始到结束的总时间(含重试/重定向)

DNS 优化

// 自定义 DNS + 内存缓存
val client = OkHttpClient.Builder()
    .dns(object : Dns {
        private val cache = LruCache<String, List<InetAddress>>(100)
        override fun lookup(hostname: String): List<InetAddress> {
            return cache.get(hostname) ?: run {
                val result = try {
                    InetAddress.getAllByName(hostname).toList()
                } catch (e: UnknownHostException) { emptyList() }
                cache.put(hostname, result)
                result
            }
        }
    })
    .build()

HttpDns 方案(推荐高稳定性场景):

class HttpDnsService(private val httpDnsUrl: String) : Dns {
    private val dnsCache = mutableMapOf<String, DnsRecord>()
​
    override fun lookup(hostname: String): List<InetAddress> {
        val cached = dnsCache[hostname]
        if (cached != null && !cached.isExpired()) return cached.ips
        val ips = queryHttpDns(hostname)
        dnsCache[hostname] = DnsRecord(ips)
        return ips
    }
}

HttpDns 好处:绕过本地 DNS 缓存、避免 DNS 劫持、解析更快、支持 CDN 精准调度。

EventListener 监控网络性能

最强大的监控能力之一,获取每次请求的完整时间线:

class OkHttpMonitor : EventListener() {
    private val callStartTimes = ConcurrentHashMap<Call, Long>()
​
    override fun callStart(call: Call) {
        callStartTimes[call] = System.nanoTime()
    }
​
    override fun dnsStart(call: Call, domainName: String) {
        log("DNS lookup: $domainName")
    }
    override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
        log("DNS resolved: $domainName${inetAddressList.joinToString()}")
    }
​
    override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
        log("Connecting: ${inetSocketAddress.hostString}:${inetSocketAddress.port}")
    }
​
    override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
        log("Connected: ${inetSocketAddress.hostString} via $protocol")
    }
​
    override fun secureConnectStart(call: Call) = log("TLS handshake start")
    override fun secureConnectEnd(call: Call, handshake: Handshake?) {
        log("TLS handshake done: ${handshake?.cipherSuite}")
    }
​
    override fun connectionAcquired(call: Call, connection: Connection) {
        log("Connection acquired from pool")
    }
​
    override fun responseBodyEnd(call: Call, byteCount: Long) {
        val elapsed = callStartTimes.remove(call)?.let {
            TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - it)
        } ?: 0L
        NetworkMonitor.report(call.request().url.toString(), elapsed)
        log("Completed in ${elapsed}ms")
    }
​
    override fun callFailed(call: Call, ioe: IOException) {
        val elapsed = callStartTimes.remove(call)?.let {
            TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - it)
        } ?: 0L
        NetworkMonitor.reportError(call.request().url.toString(), ioe, elapsed)
    }
}
​
// 使用
val client = OkHttpClient.Builder()
    .eventListenerFactory(EventListener.Factory { OkHttpMonitor() })
    .build()

有了这些数据,可以精确分析:DNS 解析慢?连接建立慢?TLS 握手慢?还是服务端响应慢?定位瓶颈不再靠猜


六、总结

一句话总结 OkHttp 架构

OkHttp = 配置驱动(OkHttpClient)+ 调度执行(Dispatcher)+ 职责链处理(Interceptor Chain)+ 连接复用(ConnectionPool)+ 协议适配(ExchangeCodec)

核心设计原则

  1. 面向接口编程:Interceptor、ExchangeCodec、Dns、EventListener 都是接口,方便替换扩展
  2. 职责单一:每个拦截器只做一件事,组合完成完整请求
  3. 不可变模型:Request、Response 不可变,线程安全
  4. 链式处理:责任链让横切关注点优雅分离
  5. 连接复用:连接池 + HTTP/2 多路复用,最大限度减少连接开销

面试重点提炼(Q&A)

Q:OkHttp 请求流程是怎样的? A:OkHttpClient 创建 Call(RealCall)→ Dispatcher 调度 → 拦截器链:RetryAndFollowUp(重试+重定向)→ Bridge(补头+gzip+Cookie)→ Cache(缓存策略)→ Connect(建立/复用连接)→ CallServer(实际通信)。

Q:Application Interceptor 和 Network Interceptor 的区别? A:Application 在链首,调一次,适合统一处理(Token/日志);Network 在链尾,每次网络请求都调(含重试/重定向),适合监控实际流量。Application 看逻辑请求,Network 看物理请求。

Q:OkHttp 连接池怎么工作的? A:默认保留最多 5 个空闲连接,每空闲连接最多存活 5 分钟。定时清理线程周期性检查,超限或超时就关闭最久未用连接。复用条件:协议、域名、端口、SSL 配置、代理全部一致。HTTP/2 只需主机名匹配。

Q:OkHttp 如何处理 HTTPS? A:通过 Platform 适配自动使用系统 TrustManager,支持证书固定(CertificatePinner)、TLS 1.2/1.3、H2 和 H2 Prior Knowledge。

Q:Dispatcher 的 maxRequestsPerHost 为什么是 5? A:TCP 连接有三次握手、TLS 握手、慢启动等开销,对同一服务器并发太多连接互相干扰。HTTP/2 单连接已支持多路复用,5 个绰绰有余。这个限制防止不必要的资源消耗。


本文参考 OkHttp 4.x 源码(okhttp v4.12.0),代码均已简化,保留核心逻辑。