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 之手。
但大多数人停留在"会用"的层面:OkHttpClient → Request → execute / 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))
}
AsyncCall 是 Runnable 子类,持有 Callback 引用。Dispatcher 决定执行时,提交到线程池运行。
Dispatcher 线程池设计
val executorService: ExecutorService = ThreadPoolExecutor(
0, // corePoolSize = 0,空闲时全部销毁
Int.MAX_VALUE, // maximumPoolSize 无上限(受 maxRequests 约束)
60, TimeUnit.SECONDS, // 空闲存活 60s
SynchronousQueue(), // 直接提交
threadFactory("OkHttp Dispatcher", false)
)
核心参数:
| 参数 | 默认值 | 解释 |
|---|---|---|
maxRequests | 64 | 同时执行的最大请求数 |
maxRequestsPerHost | 5 | 同一主机最大并发 |
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 Interceptor | Network 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
}
三种结果:
| 场景 | networkRequest | cacheResponse | 结果 |
|---|---|---|---|
| 强缓存有效 | 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.1 →
Http1ExchangeCodec(文本协议,逐行写入) - HTTP/2 →
Http2ExchangeCodec(二进制帧,HPACK 压缩头)
核心差异:
| 维度 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 格式 | 文本 | 二进制帧 |
| 头压缩 | 无 | HPACK |
| 并发 | 单连接单请求 | 多路复用 |
| 编码器 | Http1ExchangeCodec | Http2ExchangeCodec |
三、连接池深度
清理机制
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.com 和 static.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?
}
关键点:
synchronized(refreshLock)保证同一时间只有一个线程刷新- 双重检查:刷新后其他线程直接拿新 token
- 只重试一次,避免死循环
统一错误处理拦截器
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)
核心设计原则
- 面向接口编程:Interceptor、ExchangeCodec、Dns、EventListener 都是接口,方便替换扩展
- 职责单一:每个拦截器只做一件事,组合完成完整请求
- 不可变模型:Request、Response 不可变,线程安全
- 链式处理:责任链让横切关注点优雅分离
- 连接复用:连接池 + 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),代码均已简化,保留核心逻辑。