1、OkHttp简介与核心组件归纳

说到 Android 网络请求,OkHttp 几乎是绕不开的标配。我最早接触它的时候,还在用 HttpClient 那套老东西。后来项目里换了 OkHttp,第一感觉就是:清爽,而且默认就支持 HTTP/2。说实话,现在让我回去用别的方案,我是真不习惯了。

OkHttp 是什么?

OkHttp 是一款高效、稳定的 HTTP 客户端。它由 Square 公司开源,目前在 Android 和 Java 生态里占有率极高。它不只是简单的网络请求库,更像一个全功能的 HTTP 引擎。

它帮你处理了这些头疼的事:

  • 连接复用(减少握手延迟)
  • 自动重定向
  • 请求/响应拦截
  • 连接失败重试
  • Cookie 持久化

嗯,这是它的主要功能。但很多人把它当黑盒用,直到线上崩溃才发现,很多问题出在初始化阶段。

核心组件拆解

要理解初始化崩溃,你得先搞懂它内部是怎么组装的。我之前在一个日活千万级的 App 里排查问题,发现崩溃往往不是 OkHttp 本身有 bug,而是组件配置不当导致的。

下面这张图,是我个人习惯用来向团队新人解释 OkHttp 内部结构的框架图。

OkHttp 核心组件架构 OkHttpClient 构建器 配置连接超时 / 拦截器 设置缓存 / 代理 / DNS Dispatcher 调度器 管理请求队列 / 线程池 控制并发请求数 ConnectionPool 复用 TCP 连接 HTTP/2 多路复用 Interceptor 拦截器链 重试 & 重定向 → 桥接 → 缓存 → 连接 → 网络 EventListener & Call 事件监听 请求生命周期回调 异常 / 成功 / 耗时记录 初始化时所有组件必须正确配置,否则可能引发启动崩溃 核心:OkHttpClient 构建 → Dispatcher → Interceptor → ConnectionPool → EventListener

每个组件都承担了什么角色?

组件 核心职责 初始化阶段注意点
OkHttpClient 全局配置容器,线程安全 构建时若传入空值或非法参数,直接抛异常
Dispatcher 调度异步请求,管理线程池 自定义线程池未启动?可能卡死
ConnectionPool 复用 TCP 连接,减少延迟 默认 5 分钟空闲回收,但配置不当会内存泄漏
Interceptor 拦截并处理请求/响应 自定义拦截器里做了网络请求?循环调用引发崩溃
EventListener 监控请求各阶段耗时 回调中操作 UI?非主线程抛异常

深入理解:为什么这些组件会引发初始化崩溃?

你想想看,OkHttpClient 的构建看起来很简单,不就是 new Builder() 然后 build() 吗?但实际项目里,很多人在这里翻车。比如:

常见错误示例:

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(0, TimeUnit.MILLISECONDS)  // 超时设置为0,等于无限等待?
    .build();

嗯,超时传 0 在某些版本里等于不超时,但实际上底层会触发除零异常。我就在同事的代码里见到过这种写法。crash 日志直接指向 OkHttp 初始化。

Dispatcher 如何影响启动?

Dispatcher 内部维护了一个线程池。默认情况下,最大并发请求数是 64,每个主机的最大并发是 5。这个配置在绝大多数场景下够用。

但如果你自己传了一个自定义线程池进去,就得小心了。我之前遇到过这个问题:

  • 自定义线程池的核心线程数设为 0
  • 队列用 SynchronousQueue
  • 结果第一个请求就把线程池塞满
  • 后续请求全部被拒绝
  • 崩溃直接抛 RejectedExecutionException

我曾经踩过的坑:

在 Application.onCreate() 里初始化 OkHttpClient,同时又在同一个线程池里执行第一个网络请求。如果 Dispatcher 的线程池没来得及启动,就会死锁。说白了,主线程等请求结果,请求等线程池启动,线程池又在等主线程释放锁——典型的闭环死锁。

ConnectionPool 的黑暗面

ConnectionPool 默认最多保持 5 个空闲连接,超过 5 分钟后回收。听起来挺合理是吧?但如果你在 Application 启动时一口气创建了 20 个 OkHttpClient 实例,每个都有自己的连接池——那后果就严重了。

我见过一个 App 启动时创建了 30 个 OkHttpClient 对象。每个连接池都占用线程和内存。最后 OOM 了。嗯,这里要注意:OkHttpClient 设计成全局单例是有道理的。

Interceptor 的顺序问题

拦截器分为应用拦截器和网络拦截器。它们在链中的顺序不同,执行的时机也不同。如果你在初始化时注册了多个拦截器,并且它们的执行顺序有依赖关系,那启动阶段的第一个请求就可能走错链路。

举个例子:

  1. 你加了一个 LoggingInterceptor 打印请求日志
  2. 又加了一个 HeaderInterceptor 添加鉴权头
  3. 如果 HeaderInterceptor 放在 LoggingInterceptor 后面,日志里就没有鉴权头
  4. 但这不会崩溃,只是逻辑错乱
  5. 真正崩溃的,是拦截器里又调用了 OkHttpClient 发起新请求,形成递归

我的建议:在初始化阶段,先只加必要的拦截器。像日志、统计这类非核心拦截器,等 App 启动完成再动态添加。这样既能避免初始化崩溃,又能保证核心功能不受影响。

核心初始化流程总结

再回头看整个初始化流程,其实就是一句话:

构建 OkHttpClient → 配置核心参数 → 注册拦截器 → 设置事件监听 → 准备就绪

每个环节都可能成为崩溃的导火索。特别是在多人协作的项目里,一个人改了线程池配置,另一个人改了超时时间,结果合到一起就爆炸了。

我习惯的做法是:写一个单独的 OkHttpClientProvider,把初始化逻辑封装起来,加上参数校验和异常捕获。这样就算配错了,也不至于让整个 App 闪退。

好了,OkHttp 的组件和它们可能引发的初始化问题,先聊到这儿。后续我们会深入每个组件的具体崩溃场景和解决方案。