OkHttp 总结
以下是按难度分层整理,覆盖原理、源码和实战场景。
一、基础概念类
Q1:为什么选择 OkHttp?它相比 HttpURLConnection 有什么优势?
答:
-
连接复用:自动维护连接池,复用 TCP 连接,减少握手开销
-
HTTP/2 支持:多路复用、头部压缩、服务器推送
-
自动处理:GZIP 压缩、重试重定向、响应缓存
-
拦截器机制:强大的请求/响应拦截能力
-
流式 API:基于 Okio 的高效 I/O,减少内存拷贝
-
现代协议:支持 WebSocket、TLS 1.3
Q2:OkHttp 中有哪些核心类?它们各自的作用是什么?
答:
| 类 | 作用 |
|---|---|
OkHttpClient |
配置中心,管理连接池、拦截器、超时等(Builder 模式,建议单例) |
Request |
封装 HTTP 请求(URL、Method、Headers、Body) |
Response |
封装 HTTP 响应(Code、Headers、Body) |
Call |
代表一次请求,可执行或取消 |
Dispatcher |
调度器,管理异步请求的线程池和并发数 |
ConnectionPool |
连接池,复用 TCP 连接 |
Interceptor |
拦截器接口,用于拦截和修改请求/响应 |
Q3:同步请求和异步请求的区别?
答:
kotlin
// 同步 - 阻塞当前线程,直接返回 Response
val response = client.newCall(request).execute()
// 异步 - 非阻塞,通过 Callback 回调结果
client.newCall(request).enqueue(callback)
区别:
-
同步 :在当前线程执行,会阻塞,不能在主线程调用 (Android 4.0+ 会抛
NetworkOnMainThreadException) -
异步 :内部通过
Dispatcher分配到线程池执行,回调在子线程,需手动切回主线程更新 UI
二、拦截器机制(高频)
Q4:应用拦截器和网络拦截器有什么区别?
答:
| 维度 | 应用拦截器 addInterceptor |
网络拦截器 addNetworkInterceptor |
|---|---|---|
| 调用次数 | 每个请求只调用一次 | 可能多次(重定向、重试时) |
| 观察内容 | 应用层看到的请求/响应 | 实际发送到网络的请求/响应 |
| 是否经过缓存 | 是 | 否(在网络层) |
| 是否经过重试 | 是 | 否 |
| 适用场景 | 日志打印、统一添加 Header、签名 | 网络调试、监控实际传输数据 |
执行顺序:
plain
应用拦截器1 → 应用拦截器2 → RetryAndFollowUp → Bridge → Cache → Connect → 网络拦截器 → CallServer
Q5:写一个日志拦截器,要注意什么?
答:
kotlin
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
// 记录请求
val t1 = System.nanoTime()
Log.d("OkHttp", "Sending request ${request.url}")
val response = chain.proceed(request)
// 记录响应
val t2 = System.nanoTime()
Log.d("OkHttp", "Received response for ${response.request.url} in ${(t2 - t1) / 1e6}ms")
return response
}
}
注意点:
-
ResponseBody只能读取一次,读取后需要重新构建 Response 返回 -
大文件下载时避免打印 Body,防止 OOM
三、源码原理类(进阶)
Q6:OkHttp 的请求流程是怎样的?从 enqueue 到拿到 Response 经历了什么?
答:
plain
1. OkHttpClient.newCall(request) → 创建 RealCall
2. RealCall.enqueue() → 交给 Dispatcher 调度
3. Dispatcher 将任务放入线程池执行
4. 进入拦截器链(Chain of Responsibility 责任链模式):
① RetryAndFollowUpInterceptor
- 处理重试(连接失败、超时等)
- 处理重定向(3xx 响应)
② BridgeInterceptor
- 补全请求头(Content-Type、Content-Length、Host、Cookie 等)
- 处理 GZIP 解压
③ CacheInterceptor
- 根据缓存策略决定是否使用缓存
- 处理 304 Not Modified
④ ConnectInterceptor
- 从 ConnectionPool 获取或创建连接
- 建立 TCP + TLS 握手
⑤ 网络拦截器(如果有)
⑥ CallServerInterceptor
- 真正向服务器写入请求数据
- 读取响应数据
Q7:OkHttp 的连接池(ConnectionPool)是如何工作的?
答:
-
默认配置:最大空闲连接数 5,保活时间 5 分钟
-
复用逻辑 :通过
Route(IP + Port + Proxy)匹配,相同 Route 的连接可以复用 -
HTTP/2 多路复用:一个连接可以同时承载多个请求(Stream)
-
清理机制:使用后台线程定期清理过期连接
kotlin
// 源码关键逻辑
class ConnectionPool(
maxIdleConnections: Int = 5,
keepAliveDuration: Long = 5,
timeUnit: TimeUnit = TimeUnit.MINUTES
)
延伸:HTTP/1.1 是串行复用(一个连接一次一个请求),HTTP/2 是多路复用(一个连接多个请求并行)。
Q8:OkHttp 的 Dispatcher 是如何管理并发请求的?
答:
Dispatcher 内部维护三个队列:
kotlin
class Dispatcher {
// 最大并发请求数(默认 64)
var maxRequests = 64
// 每个 Host 最大并发数(默认 5)
var maxRequestsPerHost = 5
// 等待执行的异步请求队列
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
// 正在执行的异步请求队列
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
// 正在执行的同步请求队列
private val runningSyncCalls = ArrayDeque<RealCall>()
}
调度规则:
-
异步请求超过
maxRequests或同一 Host 超过maxRequestsPerHost时,放入readyAsyncCalls等待 -
有请求完成时,从等待队列取出补充到执行队列
-
同步请求无并发限制,只记录
Q9:OkHttp 的缓存机制是如何实现的?
答:
-
基于 HTTP 缓存协议(
Cache-Control、Expires、ETag、Last-Modified) -
通过
CacheInterceptor实现 -
默认使用磁盘缓存(LRU 策略)
缓存策略判定:
plain
1. 网络不可用 + 有缓存 → 使用缓存
2. Cache-Control: no-cache / no-store → 不使用缓存
3. 缓存未过期 → 直接返回缓存
4. 缓存过期但有 ETag/Last-Modified → 发送条件请求(If-None-Match)
5. 服务器返回 304 → 更新缓存时间,返回缓存内容
Q10:OkHttp 用到了哪些设计模式?
答:
| 设计模式 | 应用位置 |
|---|---|
| Builder 模式 | OkHttpClient.Builder、Request.Builder |
| 责任链模式 | Interceptor.Chain --- 拦截器链依次处理 |
| 工厂模式 | Call.Factory |
| 单例模式 | OkHttpClient 建议全局单例 |
| 策略模式 | ConnectionSpec(TLS 配置策略) |
四、实战与优化类
Q11:OkHttpClient 为什么要使用单例?
答:
-
每个
OkHttpClient实例都有自己的 连接池 和 线程池 -
创建多个实例会导致:
-
连接无法复用(每个池独立)
-
线程资源浪费(多个 Dispatcher 线程池)
-
缓存不共享
-
-
正确做法 :全局持有一个
OkHttpClient实例,通过newBuilder()创建副本进行局部定制
kotlin
// 复用基础配置,局部修改
val newClient = baseClient.newBuilder()
.readTimeout(60, TimeUnit.SECONDS) // 仅修改超时
.build()
Q12:如何处理 OkHttp 的内存泄漏?
答:
常见泄漏场景:
-
ResponseBody 未关闭 :
response.body?.string()后未close() -
Callback 持有 Activity 引用:异步回调中使用匿名内部类持有外部引用
-
取消请求不及时:Activity 销毁时未取消未完成的请求
解决方案:
kotlin
// 1. 使用 use 自动关闭
client.newCall(request).execute().use { response ->
// 自动关闭
}
// 2. Activity 销毁时取消请求
override fun onDestroy() {
super.onDestroy()
client.dispatcher.cancelAll() // 或取消特定 Tag 的请求
}
// 3. 使用弱引用或 ViewModel 管理生命周期
Q13:如何实现请求的统一 Token 刷新?
答:
使用 应用拦截器 拦截 401 响应,自动刷新 Token 并重试:
kotlin
class TokenInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(addToken(request))
if (response.code == 401) {
// 同步刷新 Token
val newToken = refreshTokenSync()
// 使用新 Token 重试
response.close()
return chain.proceed(addToken(request, newToken))
}
return response
}
}
注意:刷新 Token 需要加锁防止并发刷新多次。
Q14:OkHttp 支持哪些超时配置?分别代表什么?
答:
kotlin
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) // TCP 握手建立连接的超时
.readTimeout(30, TimeUnit.SECONDS) // 读取服务器响应数据的超时
.writeTimeout(30, TimeUnit.SECONDS) // 向服务器写入数据的超时
.callTimeout(60, TimeUnit.SECONDS) // 整个请求(含重试)的总超时
.build()
Q15:OkHttp 与 Retrofit 的关系是什么?
答:
-
OkHttp:底层 HTTP 客户端,负责网络 I/O、连接管理、协议处理
-
Retrofit:基于 OkHttp 的上层封装,提供:
-
接口注解定义 API(
@GET、@POST) -
自动数据转换(Gson、Moshi 等)
-
适配器模式支持 RxJava、Coroutines 等
-
关系 :Retrofit 的 create() 方法通过动态代理生成接口实现,最终调用 OkHttp 执行实际网络请求。
五、加分项(源码深度)
Q16:OkHttp 基于什么做 I/O?为什么高效?
答:
基于 Okio(Square 开源的 I/O 库),相比 Java 原生 IO:
-
Segment 机制:数据以 8KB 的 Segment 为单位传输,减少内存拷贝
-
Buffer 共享:多个 Buffer 可以共享同一个 Segment
-
超时封装:统一的超时检测机制
Q17:HTTP/2 在 OkHttp 中是如何实现的?
答:
-
OkHttp 使用
Http2Connection管理 HTTP/2 连接 -
通过
OkHttpClient.protocols配置支持的协议 -
自动协商:TLS 握手时通过 ALPN(Application-Layer Protocol Negotiation)协商使用 HTTP/2 还是 HTTP/1.1
总结
| 难度 | 重点问题 |
|---|---|
| ⭐ 基础 | 核心类、同步异步区别、为什么用单例 |
| ⭐⭐ 进阶 | 拦截器区别、请求流程、连接池原理、Dispatcher 调度 |
| ⭐⭐⭐ 深入 | 缓存机制、设计模式、Okio 原理、HTTP/2 实现、Token 刷新机制 |
建议结合源码阅读 RealCall.kt、RetryAndFollowUpInterceptor.kt、ConnectionPool.kt 等核心类,能够画出请求流程图会非常有说服力。