我啃了第七遍 OkHttp 源码,今天把这本"私房笔记"摊给你看
不是因为我记性差。是因为这套东西——整个 Android 生态最优雅的责任链实现,没有之一——藏着一种通用的架构思想。你把它咀嚼透了,下次自己写 SDK、写埋点框架、写权限校验中间层,都能拿来直接套。
今天这篇就是我笔记本上的原文,我抄给你。
不要把它当面试题看。把它当一份”读源码的方法论”看。看完之后再去面试官面前,你不光是能答 OkHttp,是能答任何责任链模式相关的题。
第一页:一次完整的 client.newCall(req).execute(),到底过了几道关?
你以为你调一个 HTTP 请求,就是发个数据包出去?拿衣服。
OkHttp 默认会让你的请求依次经过下面这五道关:
你的代码
↓
[ 自定义 application interceptor(你加的)]
↓
1) RetryAndFollowUpInterceptor ← 重试和重定向
↓
2) BridgeInterceptor ← 给请求"穿衣服",给响应"脱衣服"
↓
3) CacheInterceptor ← 看缓存能不能直接用
↓
4) ConnectInterceptor ← 找个连接(建新的或复用旧的)
↓
[ 自定义 network interceptor(你加的)]
↓
5) CallServerInterceptor ← 真·写 socket,发包收包
↓
返回 Response 一路冒回去
每一关都可以修改请求(往下传之前)或者修改响应(往上回的时候)。
这就是责任链——**链上每个节点都是平权的,都可以决定”我要不要 proceed 到下一个”**。
第二页:application 和 network 拦截器的差别——这是面试高频区
新人最容易混。我把差别揉碎了写:
application interceptor(应用拦截器)
-
加在链的最外层。 -
不管中间发生什么——重试了几次、重定向了几次、命中缓存了——它只触发一次。 -
看不到中间过程的中间响应。 -
加在哪儿: OkHttpClient.Builder().addInterceptor(...)
network interceptor(网络拦截器)
-
加在 ConnectInterceptor之后、真正发包之前。 -
每发一次包就触发一次——重试三次它就跑三次,重定向一次它就多跑一次。 -
能看到真实的网络数据,包括 OkHttp 自动加的 Cookie、 Accept-Encoding: gzip这些。 -
加在哪儿: OkHttpClient.Builder().addNetworkInterceptor(...)
什么时候用哪个?我的判断是:
-
业务层逻辑(统一加 token、签名、加密参数)——用 application。 -
监控层逻辑(流量统计、抓包、看真实头部)——用 network。
面试官可能挖一刀:「我加了一个统一加 Cookie 的 application interceptor,结果发现请求里 Cookie 有时候多、有时候少,咋回事?」
答:OkHttp 自带的 BridgeInterceptor 在 application 之后才往请求里塞 Cookie。如果你又重试一次,BridgeInterceptor 会再塞一次。如果你想保证最终发出去的请求是啥样,得加 network interceptor——它在 BridgeInterceptor 之后,能看见 OkHttp 自动塞的所有东西。
第三页:每个内置拦截器干了啥,一句话讲透
笔记体,每个我就给一段。
RetryAndFollowUpInterceptor——链的入口。负责:
-
你请求挂了(超时、SocketException)它判断要不要重试。 -
服务端返回 30x,它给你跟着跳转——默认最多跟 20 次重定向。 -
401/407 鉴权失败了,它会调用你设置的 Authenticator 让你补一个新请求。 不重试也不重定向,它就把响应原样冒上去。
BridgeInterceptor——这是个”翻译官”。负责:
-
给请求加 Content-Type、Content-Length、Host、Connection: keep-alive、Accept-Encoding: gzip(如果你没自己写)、Cookie。 -
收到响应,自动解 gzip——所以你拿到的 ResponseBody 已经是解压完的了。 -
把响应里的 Set-Cookie 写回 CookieJar。
它的存在意义是——让上层业务代码不用关心这堆 HTTP 协议细节。你扔给它一个干净的 Request,它给你包装成符合 HTTP 规范的真实请求;服务端回来一个压缩的、带一堆头的响应,它给你脱掉外壳还原成”业务可读”的 Response。
CacheInterceptor——这是 HTTP 缓存的实现。负责:
-
算这个请求有没有可用缓存(Cache-Control、Etag、Last-Modified 那一套规则)。 -
全命中——直接返回缓存,不发包。 -
命中但过期——发条件请求(带 If-Modified-Since或If-None-Match)。 -
服务端返 304 Not Modified——用本地缓存的 body + 服务端返回的新头拼一个响应给你。 -
服务端返 200——把新响应存进缓存,返给你。
要让它生效,你得给 OkHttpClient 配一个 Cache(file, size)。很多人没配,于是写了一堆”为啥我没缓存”的 issue。
ConnectInterceptor——它是连接池的入口。负责:
-
看连接池里有没有现成的可用连接(同 host 同端口同协议)——有就直接复用。 -
没有就新建一个,跑一次 TCP 三次握手 + TLS 握手。 -
把连接绑到当前请求上,链跑完后连接归还到池子里——这就是 OkHttp 默认的连接复用。
为啥 OkHttp 性能高?很大程度上靠这个池子。一个 host 同时复用 5 个连接、连接默认空闲 5 分钟回收——这俩参数你能在源码里搜到 ConnectionPool。
CallServerInterceptor——链的终点。负责:
-
用绑定的连接,把请求头、请求体写到 socket。 -
读响应头、读响应体。 -
处理 Expect: 100-continue这种特殊握手。
到这一步才是真正的”网络通信”。前面四个都是在做”准备工作”和”事后处理”。
第四页:责任链是怎么用 List + index 串起来的?这是这套设计最值钱的地方
OkHttp 的责任链实现简洁得过分。我手抄精简版:
classRealInterceptorChain(
val interceptors: List<Interceptor>,
val index: Int,
val request: Request,
// ... 还有连接、调用之类的
) : Interceptor.Chain {
overridefunproceed(request: Request): Response {
// 关键三行
val next = RealInterceptorChain(interceptors, index + 1, request, ...)
val interceptor = interceptors[index]
return interceptor.intercept(next)
}
}
就这。链不是显式存的”链表节点”,它是一个递归的函数调用栈。每次 proceed 创建一个 index + 1 的新 chain,把它传给当前拦截器;当前拦截器调用 chain.proceed() 又创建下一个 chain……一直到 CallServerInterceptor 拿到响应,响应沿着调用栈一层一层冒回去,每一层都可以顺手改一改响应。
为啥这么写好?
-
代码极简,没有维护链表节点、没有指针操作。 -
天然支持双向劫持——proceed 之前改请求,proceed 之后改响应。 -
每个拦截器都是无状态的纯函数,单测起来跟玩似的。
这套思路你完全可以抄到你自己的项目里。我做埋点框架的时候就照抄了这个结构——一个事件从产生到上报,中间走一串”过滤、采样、加签、批量、压缩、发送”的拦截器。同事看完代码说:”这不就是 OkHttp 那套吗?”我说对,好东西就该被抄。
第五页:手写一个”全局加密”拦截器作为收官
讲再多不如自己写一个。需求——所有 POST 请求的 body,都要加密一次再发;所有响应 body 都要解密一次再交给业务。
classCryptoInterceptor(privateval key: ByteArray) : Interceptor {
overridefunintercept(chain: Interceptor.Chain): Response {
val original = chain.request()
// proceed 之前——加密请求 body
val newReq = if (original.body != null) {
val plain = Buffer().also { original.body!!.writeTo(it) }.readByteArray()
val encrypted = encrypt(plain, key)
original.newBuilder()
.post(encrypted.toRequestBody("application/octet-stream".toMediaType()))
.build()
} else original
val response = chain.proceed(newReq)
// proceed 之后——解密响应 body
val decrypted = decrypt(response.body!!.bytes(), key)
return response.newBuilder()
.body(decrypted.toResponseBody("application/json".toMediaType()))
.build()
}
privatefunencrypt(data: ByteArray, key: ByteArray): ByteArray = TODO()
privatefundecrypt(data: ByteArray, key: ByteArray): ByteArray = TODO()
}
把它加到客户端:
val client = OkHttpClient.Builder()
.addInterceptor(CryptoInterceptor(key)) // application 层,业务无感
.build()
你的整个业务层代码——Repository、Retrofit 接口——一行都不用改。所有人都不知道传输层加密了。这就是责任链 + 拦截器最大的价值:业务代码和横切关注点彻底分离。
笔记结尾:抄完源码我学到的三件事
抄完这本笔记,我每次都会顺手记下三件事,今天分享给你:
第一——好的架构是”插拔式”的。OkHttp 把所有可定制点都做成 Interceptor,于是它的能力可以无限扩展。Retrofit 在它之上、Picasso/Glide 借用它做下载、第三方监控库往里塞自己的拦截器——整个生态都建立在这一个抽象之上。
第二——好的接口设计是”对扩展开放,对修改封闭”。OkHttp 几年没大改 Interceptor 接口的形态,但用它写出来的拦截器无穷无尽。这就是开闭原则的工业级范本。
第三——读源码不是为了”记住源码”。是为了抽出可迁移的设计模式。我读 OkHttp 学到的那套责任链,在我后来写埋点 SDK、网关 SDK、权限 SDK 的时候用了不下五次。源码读三遍,能用到工作中一辈子。
临走前再扔三个高频题
抄完笔记,给你压三道这个领域最容易被问的题:
-
OkHttp 的连接池工作原理?默认参数是啥?
ConnectionPool默认 5 个空闲连接,每个最长闲置 5 分钟。基于一个守护线程定时清理。复用条件:同 host、同端口、同协议、连接还活着。 -
同一个 OkHttpClient 实例的并发上限?
Dispatcher默认最多 64 个并发请求,单 host 最多 5 个。这俩参数在Dispatcher.maxRequests和maxRequestsPerHost,可以调。 -
HTTP/2 上 OkHttp 怎么用一个 TCP 连接发多个请求?多路复用。HTTP/2 在一个 TCP 连接上跑多个 stream,OkHttp 在
Http2Connection里维护这些 stream,发请求就是给这个连接 new 一个 stream,不需要新开 TCP。这就是为啥 HTTP/2 下连接池里一个连接能扛多个并发。
笔记到这儿。下回我把 Retrofit 那一套 Call.Factory + 动态代理 + Adapter 的玩法也抄给你——它跟 OkHttp 是一个体系里的好兄弟,但又是完全不同的设计哲学。
读源码这事儿,慢慢来。一次抄一页,一辈子吃不完。
夜雨聆风