Android随笔-OkHttp
一、定位OkHttp 是 Android/Java 世界事实标准的高性能 HTTP 客户端负责真正的网络收发。它的三大核心能力拦截器责任链请求/响应的分层加工、连接池复用TCP/TLS 握手成本摊销、现代协议支持HTTP/2 多路复用、WebSocket、HTTP/1.1 keep-alive。Retrofit 管接口抽象OkHttp 管连接与传输——Retrofit 一次网络 IO 都不做全部交给 OkHttp。二、基本用法与角色valclientOkHttpClient.Builder().connectTimeout(15,SECONDS).addInterceptor(MyInterceptor())// 应用拦截器.build()// 全局单例连接池/线程池共享valrequestRequest.Builder().url(https://api.example.com/user/1).build()client.newCall(request).enqueue(object:Callback{overridefunonResponse(call:Call,response:Response){...}overridefunonFailure(call:Call,e:IOException){...}})四个核心角色角色职责OkHttpClient配置中心 Call 工厂。必须全局单例——连接池、Dispatcher 线程池、缓存都挂在它上面多实例会导致资源无法复用Request不可变的请求描述URL、方法、头、体建造者模式CallRealCall一次请求的执行体只能执行一次executed标志防重入Dispatcher异步调度器管理并发上限和线程池三、核心工作流程源码主线client.newCall(request) → 创建 RealCall │ ├─ execute() → 同步dispatcher.executed() 登记 │ → getResponseWithInterceptorChain() │ └─ enqueue(callback) → 异步dispatcher.enqueue(AsyncCall) → 线程池执行 → 同样走 getResponseWithInterceptorChain()主入口 RealCall.getResponseWithInterceptorChain()// 源码核心简化组装拦截器列表valinterceptorsmutableListOfInterceptor()interceptorsclient.interceptors// ① 用户自定义应用拦截器interceptorsRetryAndFollowUpInterceptor// ② 重试与重定向interceptorsBridgeInterceptor// ③ 桥接补全请求头interceptorsCacheInterceptor// ④ 缓存interceptorsConnectInterceptor// ⑤ 建立连接if(!forWebSocket){interceptorsclient.networkInterceptors// ⑥ 用户自定义网络拦截器}interceptorsCallServerInterceptor// ⑦ 真正读写网络链尾// 从 index0 开始责任链递归下传valchainRealInterceptorChain(interceptors,index0,...)returnchain.proceed(request)责任链精髓每个拦截器拿到 chain做三件事——加工请求 → 调 chain.proceed() 向下传递 → 拿到下游响应后加工响应。proceed() 内部是 RealInterceptorChain(index1) 递归所以请求是正序进入响应是倒序流出形成典型的洋葱模型。四、五大内置拦截器详解4.1 RetryAndFollowUpInterceptor —— 重试与重定向循环 while(true) 发起请求根据响应决定下一步3xx 重定向读取 Location 构建新请求继续 follow上限 20 次防死循环401/407 认证失败调 Authenticator 获取凭证比如刷新 Token重放请求——这就是 Retrofit 实战里 Token 自动刷新的挂载点连接失败判断是否可恢复recoverable如连接池拿到坏连接可恢复则换路重试不可恢复或超限才把响应/异常抛回上游4.2 BridgeInterceptor —— 应用层与网络层的桥补全应用开发者不关心、但 HTTP 协议必须的细节请求侧自动加 Content-Length、Content-Type、Host、Connection: Keep-Alive、Accept-Encoding: gzip、User-Agent、Cookie从 CookieJar 读响应侧透明 gzip 解压服务器返回 gzip 而你没手动指定时自动解压并剥掉 Content-Length/Content-Encoding、Set-Cookie 写入 CookieJar问题为什么在网络拦截器里看到的 Content-Length 和最终一致、而应用拦截器里可能还没有——因为补全发生在 Bridge应用拦截器在 Bridge 之前。4.3 CacheInterceptor —— HTTP 缓存策略核心CacheStrategy.Factory 计算出一个策略组合networkRequest / cacheResponse 两个值组合行为networkRequestnull, cacheResponse≠null强缓存命中不发网络直接返回缓存响应Cache-Control: max-age未过期networkRequest≠null, cacheResponsenull只用网络无缓存或缓存被禁两者都≠null协商缓存发条件请求If-None-Match: ETag/If-Modified-Since服务器回304则合并缓存 body省流量两者都 null返回 504 Unsatisfiable Request需要 OkHttpClient.Builder().cache(Cache(dir, size)) 显式开启否则此拦截器直接透传只缓存 GET缓存头由服务器 Cache-Control 驱动客户端可用 request.cacheControl() 强制如 FORCE_CACHE、only-if-cached 离线模式4.4 ConnectInterceptor —— 建立连接最重的一环这一层只做一件事给请求搞到一条可用的 RealConnection。ExchangeFinder.findConnection() │ ├─ ① 复用当前 Call 已持有的连接 ├─ ② 查连接池 ConnectionPool存在 Address 完全匹配 │ host、port、协议、DNS、代理、SSL 配置全部相同 │ 且空闲的连接 → 直接复用 │ ├─ HTTP/1.1复用 keep-alive 的 socket │ └─ HTTP/2同一条 TCP 上开新 Stream多路复用 │ └─ ③ 池里没有 → new RealConnection → connect() TCP 三次握手 → TLS 握手HTTPS→ ALPN 协商协议h2 / http1.1 → 放入连接池供后续复用连接池机制ConnectionPool内部 RealConnectionPool持有连接队列每连接默认空闲保活5 分钟后台 cleanup 任务定期清理过期连接复用判断靠连接的引用计数allocationsHTTP/1.1 同一时刻只能承载一个流HTTP/2 可并发多流上限由 SETTINGS 帧协商意义TCP 握手 TLS 握手是百毫秒级成本池化复用是 OkHttp 快的第一来源4.5 CallServerInterceptor —— 链尾真正的网络 IO写请求行 请求头 → 写请求体流式okio Buffer 分块写不把整个文件读进内存读响应行HTTP/1.1 200 OK→ 读响应头 → 构造 Response响应体同样是流式返回——所以 response.body.string() 只能调一次读完流就关了大文件必须 byteStream() 分块写盘五、自定义拦截器两种对比项addInterceptor应用拦截器addNetworkInterceptor网络拦截器位置链最前端RetryAndFollowUp 之前Connect 与 CallServer 之间触发次数一次请求只触发一次重定向不重入每次实际网络交互都触发重定向/重试可见能看到原始请求可能还没补 Host/gzip传输层的真实请求/响应典型用途Token 注入、全局日志、Mock 数据、加解密观察重定向、精确统计网络耗时六、Dispatcher异步调度三个队列readyAsyncCalls等待、runningAsyncCalls执行中、runningSyncCalls同步并发上限全局最大 64 个请求单 host 最大 5 个maxRequests/maxRequestsPerHost可改线程池SynchronousQueue 0 核心线程 Integer.MAX_VALUE 上限 60s 空闲回收——类似 newCachedThreadPool任务即来即跑、用完即释放回调线程enqueue 的 onResponse 跑在 OkHttp 的 Dispatcher 线程池不是主线程——裸用 OkHttp 更新 UI 必须自己切线程这就是 Retrofit 帮你加 MainThreadExecutor 的原因七、其他核心能力能力要点HTTP/2单 TCP 连接多路复用头部压缩HPACK服务端推送OkHttp 自动 ALPN 协商无需手动开WebSocketnewWebSocket(request, listener)长连接双向通信绕过部分拦截器超时体系connectTimeout / readTimeout / writeTimeout /callTimeout整次调用总时限粒度比 HttpClient 细取消call.cancel()→ 连接层中断 exchange协程场景 Retrofit 的 suspendCancellableCoroutine 就是联动它流式 IO基于okioSquare 自研 IO 库Buffer 链式分块避免大数组拷贝请求体进阶RequestBody.create()子类化可做上传进度回调包装 sink 统计写入字节八、完整的流程以 client.newCall(request).enqueue(callback) 为起点按真实时间顺序走一遍——包括上一轮没展开的 socket 层细节。阶段 1创建 Callclient.newCall(request) └── new RealCall(client, request, forWebSocketfalse) └── 此时只是包装了配置executedfalse什么都没发生阶段 2调度异步路线enqueueRealCall.enqueue(callback) ├── check(!executed) → executed true // 一个 Call 只能执行一次 └── dispatcher.enqueue(AsyncCall) ├── 判断runningAsyncCalls 64 且 同host运行数 5 │ ├── 是 → 移入 runningAsyncCalls交给线程池执行 │ └── 否 → 放入 readyAsyncCalls 等待队列 └── executorService.execute(AsyncCall) // SynchronousQueue 缓存线程池 └── AsyncCall.run() └── getResponseWithInterceptorChain() ← 进入正题同步路线execute跳过 Dispatcher 队列在当前线程直接调 getResponseWithInterceptorChain()仅登记 runningSyncCalls。阶段 3进入责任链getResponseWithInterceptorChain() └── 组装拦截器列表 → RealInterceptorChain(index0).proceed(request) └── 递归驱动每个拦截器 intercept(chain) 内部再调 chain.proceed(index1)请求开始沿链正序下钻阶段 4请求下钻逐层加工① 应用拦截器你的 Token/日志拦截器 │ chain.proceed() ▼ ② RetryAndFollowUpInterceptor │ 开启 while(true) 循环重试/重定向时请求会从这里重新发出 │ chain.proceed() ▼ ③ BridgeInterceptor │ 补全Host、Content-Type、Content-Length、 │ Connection: Keep-Alive、Accept-Encoding: gzip、Cookie、User-Agent │ chain.proceed() ▼ ④ CacheInterceptor │ ├─ 强缓存命中→ 直接返回缓存 Response请求终止倒序流出 │ └─ 否则放行可能带上 If-None-Match 做协商 │ chain.proceed() ▼ ⑤ ConnectInterceptor —— 拿到一条可用连接见阶段 5 │ chain.proceed() ▼ ⑥ 网络拦截器可观察到真实的传输层数据 │ chain.proceed() ▼ ⑦ CallServerInterceptor —— 真正的网络 IO见阶段 6阶段 5ConnectInterceptor 内部——连接怎么来的ExchangeFinder.findConnection() │ ├─ 查当前 Call 已分配的连接是否可复用 ├─ 遍历 RealConnectionPool.connections │ Address 全匹配host/port/协议/DNS/代理/SSL配置 │ 引用计数空闲HTTP/1.1 无占用流HTTP/2 未达流上限 │ → 命中返回已有连接零握手成本 │ └─ 未命中 → new RealConnection() → connect() │ ├─ 1. 建立 TCPsocket.connect(address, connectTimeout) │ → 内核三次握手 ├─ 2. TLS 握手HTTPSSSLSocket 握手、证书校验信任链 hostname 验证 ├─ 3. ALPN 协议协商服务器说 h2 → HTTP/2否则 http/1.1 │ ├─ h2启动 Http2Connection 读写线程发 SETTINGS 帧 │ └─ http/1.1保持 socket 即可 └─ 4. 连接入池供本次及后续请求复用阶段 6CallServerInterceptor 内部——数据怎么发出去Exchange由 Connect 阶段创建持有 codec connection │ ├─ 写请求行GET /user/1 HTTP/1.1\r\n ├─ 写请求头逐行写出Host/Accept/... ├─ 写请求体RequestBody.writeTo(sink) │ → okio Buffer 分块 → socket.getOutputStream() → 内核 TCP 发送缓冲区 │ → flushRequest() 确保全部发出 │ ├─ readResponseHeaders() │ 读响应行HTTP/1.1 200 OK→ 解析状态码 │ 读响应头 → 构建 Response此时 body 还没读 │ └─ openResponseBody()挂上流式读取器不预读内容注意此时Response 对象已经生成但响应体还躺在 socket 里——只有调用 body.string()/byteStream() 时才真正从网络流里读数据。这就是大文件不会 OOM 的原因。阶段 7响应倒序流出回程加工⑦ CallServer 返回 Response ▲ ⑥ 网络拦截器观察真实响应 ▲ ⑤ Connect无操作透传 ▲ ④ CacheInterceptor响应可缓存→ 写入磁盘缓存异步 │ 协商请求收到 304→ 合并缓存 body 返回 ▲ ③ BridgeInterceptorgzip 透明解压、Set-Cookie 存入 CookieJar ▲ ② RetryAndFollowUp判定响应码 │ ├─ 3xx → 构建新请求while 循环重新下钻阶段 4 │ ├─ 401 → 调 Authenticator 刷新凭证重放 │ └─ 2xx/其他 → 放行给上游 ▲ ① 应用拦截器拿到最终响应可做全局解密封装 ▲ 回到 getResponseWithInterceptorChain() 返回 Response阶段 8交付与收尾AsyncCall.run() 收尾 ├── callback.onResponse(call, response) // Dispatcher 线程池非主线程 │ └── 你在这里读 body → 真正触发网络流的读取 │ ├── finally 里 dispatcher.finished() │ → runningAsyncCalls 移除本任务 │ → promoteAndExecute()从 readyAsyncCalls 捞下一个等待任务执行 │ └── 连接归还Exchange 释放对 RealConnection 的引用 → 引用计数归零 → 连接回到池中空闲状态保活 5 分钟等待复用九、全流程一图速记enqueue() │ Dispatcher 限流调度64/5缓存线程池 ▼ RealInterceptorChain │ ① 应用拦截器 ─────────────────────────────┐ ▼ │ ② RetryAndFollowUpwhile 循环 │ ▼ │ ③ Bridge 补全请求头 │ 响 ▼ │ 应 ④ Cache强缓存可直接终结请求 │ 倒 ▼ │ 序 ⑤ Connect池里找 → 没有则 TCP握手→TLS→ALPN │ 流 ▼ │ 出 ⑥ 网络拦截器 │ Cache写盘、 ▼ │ Bridge解压、 ⑦ CallServer写请求 → socket → 读响应头 │ Retry判定 │ │ └──────────── Response 生成 ────────────────┘ ▼ onResponseDispatcher 线程池读 body 才真正读网络流 ▼ 任务出队 → 捞下一个等待任务 → 连接归还池中保活完整流程请求经 newCall 包装成 RealCallenqueue 后由 Dispatcher 按全局 64、单 host 5限流丢进缓存线程池执行时进入责任链——应用拦截器加工、RetryAndFollowUp 开启重试循环、Bridge 补全 Host/gzip 等协议头、Cache 判定能否直接命中、Connect 从连接池找 Address 匹配的空闲连接找不到就 TCP 三次握手TLS 握手ALPN 协商新建并入池、最后 CallServer 用 okio 流式写出请求并读取响应头生成 Response响应倒序流出时 Cache 写盘、Bridge 解压、Retry 判定重定向或 401 重放最终回调在 Dispatcher 线程池交付连接归还池中保活复用。真正的 body 数据在你调用 body.string() 时才从流中读出。十、常见问题OkHttp 整体流程—— newCall → Dispatcher 调度 → 责任链五大拦截器逐层加工 → CallServer 收发 → 响应倒序流出。五大拦截器各自职责—— 上表背熟尤其 Bridge 补全了什么、Cache 的强/协商缓存逻辑、Connect 的连接复用条件。连接池怎么复用—— Address 全匹配host/port/协议/DNS/代理 空闲引用计数HTTP/1.1 复用 socketHTTP/2 单连接多路复用默认保活 5 分钟。应用拦截器和网络拦截器区别—— 位置、触发次数、可见内容三维度答。异步回调在哪个线程—— Dispatcher 线程池非主线程裸用要自切Retrofit 帮你切了。Dispatcher 的并发上限—— 全局 64、单 host 5线程池是 SynchronousQueue 缓存式。大文件下载怎么防 OOM—— CallServer 流式返回用 byteStream() 分块写盘配合 Retrofit 的 Streaming。Token 过期自动刷新挂哪—— Authenticator401 由 RetryAndFollowUp 触发重放。为什么是责任链而不是一坨 if-else—— 每层单一职责、可插拔自定义拦截器插进链里、请求/响应双向加工天然对称。ConnectionPool有限制吗—— OkHttp 的 ConnectionPool 默认最多缓存5 条空闲连接、空闲保活 5 分钟都可以自定义。注意它限制的是空闲连接而不是总连接数——活跃连接没有池级上限并发由 Dispatcher 的 64/5 控制。池内有一个后台 cleanup 任务定期扫描空闲连接数超限或保活超时就把空闲最久的连接关掉不健康连接和被标记 noNewExchanges 的连接会被立即驱逐。复用要求 Addresshost/port/协议/DNS/代理/SSL全匹配HTTP/1.1 一连接一请求HTTP/2 一连接多路复用。多个 OkHttpClient 可以共享一个池这也是要求 client 全局单例的原因。

相关新闻