2. OkHttp 的请求流程与核心组件

说实话,很多开发者用 OkHttp 只是调一下 OkHttpClient.newCall(request).execute(),然后等着拿 Response。但一旦出问题——比如拦截器启动崩溃、请求莫名其妙卡住、OOM——就抓瞎了。我早年接一个视频上传项目时,就因为在 Dispatcher 里埋了个死循环,导致整个应用 ANR,查了两天才找到根儿。

这一章,我带你把 OkHttp 的请求骨架拆开看。你理解了以下几个组件,后面调拦截器才不会被坑:

  • Call / RealCall —— 一次请求的“外壳”和“实干家”
  • Dispatcher —— 线程池 + 任务队列的管理器
  • Interceptor —— 请求管道的“关卡”
  • 内部结构 —— 连接池、缓存、路由等“后勤”

2.1 核心流程图(先看全局)

下面这张图总结了 OkHttp 发起一次 execute() 或 enqueue() 时,各个组件是怎么协作的。你先混个眼熟,后面每一个节点我都会展开讲。

OkHttp 请求流程与核心组件关系 1. 创建 RealCall 2. Dispatcher 调度 3. Interceptor 链 4. 连接与收发 5. 返回 Response 6. 内部结构支撑 execute() 直接走 1→3→4→5 enqueue() 走 1→2→3→4→5 虚线:Dispatcher 返回 task 信号

我的习惯: 调试拦截器崩溃时,先定位是同步还是异步调用——看 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 维护了三个队列:

队列 类型 作用
readyAsyncCallsDeque<AsyncCall>等待执行的异步任务
runningAsyncCallsDeque<AsyncCall>正在执行的异步任务
runningSyncCallsDeque<RealCall>同步执行的请求(用于追踪,不限制并发)

Dispatcer 内部有一个线程池,默认最大并发数是 64,每个主机的最大请求数是 5。我遇到过一个问题:某个拦截器里调了一个网络请求,结果这个网络请求又进了同一个 Dispatcher,导致递归入队——最后线程池爆炸,直接 OOM。教训:不要在拦截器里做同步网络请求,如果要发,请起新的线程或用异步,且不要用同一个 Dispatcher。

隐患点: 如果你重写了 Dispatcher.setMaxRequests() 或 setMaxRequestsPerHost(),一定要确保线程安全。Dispatcer 内部用了 synchronized 锁,但外部修改最大值并不会重新分配正在运行的任务,可能导致超过限制。

2.4 Interceptor —— 管道里的“阀门”

这是你最常打交道的地方。Interceptors 被组织成一个链:

  1. 用户自定义 应用拦截器(application interceptor) —— 你添加的,比如加 Header、打印日志。
  2. OkHttp 内置的 重试/重定向拦截器(RetryAndFollowUpInterceptor) —— 处理 3xx 和失败重试。
  3. 桥接拦截器(BridgeInterceptor) —— 设置 Content-Type、Content-Length、gzip 等。
  4. 缓存拦截器(CacheInterceptor) —— 读取/写入磁盘缓存。
  5. 连接拦截器(ConnectInterceptor) —— 建立 TCP 或 TLS 连接。
  6. 服务器请求/响应阶段,经过 网络拦截器(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 日志扒出来的。