乐于分享
好东西不私藏

我啃了第七遍 OkHttp 源码,今天把这本"私房笔记"摊给你看

我啃了第七遍 OkHttp 源码,今天把这本"私房笔记"摊给你看

每次跳槽前我都有个习惯——把 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-TypeContent-LengthHostConnection: keep-aliveAccept-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(dataByteArray, key: ByteArray): ByteArray = TODO()
privatefundecrypt(dataByteArray, 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 的时候用了不下五次。源码读三遍,能用到工作中一辈子。


临走前再扔三个高频题

抄完笔记,给你压三道这个领域最容易被问的题:

  1. OkHttp 的连接池工作原理?默认参数是啥?ConnectionPool 默认 5 个空闲连接,每个最长闲置 5 分钟。基于一个守护线程定时清理。复用条件:同 host、同端口、同协议、连接还活着。

  2. 同一个 OkHttpClient 实例的并发上限?Dispatcher 默认最多 64 个并发请求,单 host 最多 5 个。这俩参数在 Dispatcher.maxRequests 和 maxRequestsPerHost,可以调。

  3. HTTP/2 上 OkHttp 怎么用一个 TCP 连接发多个请求?多路复用。HTTP/2 在一个 TCP 连接上跑多个 stream,OkHttp 在 Http2Connection 里维护这些 stream,发请求就是给这个连接 new 一个 stream,不需要新开 TCP。这就是为啥 HTTP/2 下连接池里一个连接能扛多个并发。


笔记到这儿。下回我把 Retrofit 那一套 Call.Factory + 动态代理 + Adapter 的玩法也抄给你——它跟 OkHttp 是一个体系里的好兄弟,但又是完全不同的设计哲学。

读源码这事儿,慢慢来。一次抄一页,一辈子吃不完。