2. OkHttp 的请求流程与核心组件
说实话,很多开发者用 OkHttp 只是调一下 OkHttpClient.newCall(request).execute(),然后等着拿 Response。但一旦出问题——比如拦截器启动崩溃、请求莫名其妙卡住、OOM——就抓瞎了。我早年接一个视频上传项目时,就因为在 Dispatcher 里埋了个死循环,导致整个应用 ANR,查了两天才找到根儿。
这一章,我带你把 OkHttp 的请求骨架拆开看。你理解了以下几个组件,后面调拦截器才不会被坑:
- Call / RealCall —— 一次请求的“外壳”和“实干家”
- Dispatcher —— 线程池 + 任务队列的管理器
- Interceptor —— 请求管道的“关卡”
- 内部结构 —— 连接池、缓存、路由等“后勤”
2.1 核心流程图(先看全局)
下面这张图总结了 OkHttp 发起一次 execute() 或 enqueue() 时,各个组件是怎么协作的。你先混个眼熟,后面每一个节点我都会展开讲。
我的习惯: 调试拦截器崩溃时,先定位是同步还是异步调用——看 Call 是 execute 还是 enqueue,再决定查 Interceptor 还是 Dispatcher 的线程状态。
2.2 Call 与 RealCall —— 请求的起点
Call 是一个接口,定义了 execute()、enqueue()、cancel() 等方法。真正的实现类是 RealCall。
每次你写 okHttpClient.newCall(request),其实就 new 了一个 RealCall 对象。里面保存了:
- 原始 Request
- HttpClient 配置(拦截器列表、超时、缓存等)
- 一个 EventListener(用于监听请求生命周期)
- 一个 volatile 的 canceled 标志
注意,RealCall 不是线程安全的——你不能在多个线程里同时 execute 或 enqueue 同一个 Call 实例。我曾经见过有人把同一个 Call 扔了两个线程里去 enqueue,结果回调乱飞,最后崩溃在拦截器里。所以每个线程用独立的 Call,或者用 clone()。
// 典型错误:两个线程共用同一个 Call
Call call = client.newCall(request);
thread1.execute(() -> call.enqueue(callback1));
thread2.execute(() -> call.enqueue(callback2)); // 很可能崩溃
// 正确做法:每个线程自己 newCall
thread1.execute(() -> client.newCall(request).enqueue(callback1));
thread2.execute(() -> client.newCall(request).enqueue(callback2));
2.3 Dispatcher —— 异步任务的“调度员”
调用了 enqueue() 后,请求不会马上发出去,而是交给 Dispatcher。Dispatcer 维护了三个队列:
| 队列 | 类型 | 作用 |
|---|---|---|
readyAsyncCalls | Deque<AsyncCall> | 等待执行的异步任务 |
runningAsyncCalls | Deque<AsyncCall> | 正在执行的异步任务 |
runningSyncCalls | Deque<RealCall> | 同步执行的请求(用于追踪,不限制并发) |
Dispatcer 内部有一个线程池,默认最大并发数是 64,每个主机的最大请求数是 5。我遇到过一个问题:某个拦截器里调了一个网络请求,结果这个网络请求又进了同一个 Dispatcher,导致递归入队——最后线程池爆炸,直接 OOM。教训:不要在拦截器里做同步网络请求,如果要发,请起新的线程或用异步,且不要用同一个 Dispatcher。
隐患点: 如果你重写了 Dispatcher.setMaxRequests() 或 setMaxRequestsPerHost(),一定要确保线程安全。Dispatcer 内部用了 synchronized 锁,但外部修改最大值并不会重新分配正在运行的任务,可能导致超过限制。
2.4 Interceptor —— 管道里的“阀门”
这是你最常打交道的地方。Interceptors 被组织成一个链:
- 用户自定义 应用拦截器(application interceptor) —— 你添加的,比如加 Header、打印日志。
- OkHttp 内置的 重试/重定向拦截器(RetryAndFollowUpInterceptor) —— 处理 3xx 和失败重试。
- 桥接拦截器(BridgeInterceptor) —— 设置 Content-Type、Content-Length、gzip 等。
- 缓存拦截器(CacheInterceptor) —— 读取/写入磁盘缓存。
- 连接拦截器(ConnectInterceptor) —— 建立 TCP 或 TLS 连接。
- 服务器请求/响应阶段,经过 网络拦截器(network interceptor)(用户可加)。
每个拦截器调用 chain.proceed(request) 把请求传给下一个,最终拿到 response 再原路返回。我调试启动崩溃时,最喜欢在应用拦截器里加 try-catch,把整个链的异常抓出来——否则 OkHttp 内部抛的异常可能被吞掉,只在 log 里印一行 WTF。
// 安全的应用拦截器写法
class CrashSafeInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) {
try {
return chain.proceed(chain.request());
} catch (Exception e) {
// 这里做记录,不会让崩溃扩散到 Dispatcher
Log.e("拦截器", "proceed 异常: " + e.getMessage());
throw e; // 仍然要抛,让调用方感知到
}
}
}
2.5 内部结构 —— 你看不见但离不开的“后勤部”
OkHttp 不是单打独斗,它背后有几个模块在支撑:
- 连接池(ConnectionPool) —— 复用 HTTP/1.1 的 keep-alive 连接,以及 HTTP/2 的多路复用。
- 缓存(Cache) —— 根据 Cache-Control 策略读写磁盘缓存。
- 路由选择(RouteSelector) —— 解析 DNS、选择 IP、尝试代理等。
- EventListener —— 回调监听,用于打点、统计。
- HttpCodec —— 真正负责编解码 HTTP 报文的组件(HTTP/1.1 用 Http1Codec,HTTP/2 用 Http2Codec)。
这些组件通常在 RealCall 内部通过 OkHttpClient 的引用获取。比如连接池,默认是 5 分钟空闲回收。我曾经在弱网环境下,因为连接池里积压了大量没释放的连接,导致线程阻塞在 connect() 上——拦截器就这样卡死了。后来调小了 connectionPool.connectionTimeout 才算稳定。
一句话总结:Call 是门面,RealCall 是干活的人,Dispatcher 派活,Interceptor 链是管卡,内部结构是工具箱。你遇到的任何拦截器启动崩溃,十有八九是这几个组件之间的通信或资源管理出了问题。
下一道菜,我们从 Dispatcher 的线程模型开始深挖——那里是异步请求崩溃的重灾区。你会看到我当年是怎么一步一步把 ANR 日志扒出来的。