2、OkHttp核心配置与优化:OkHttp架构解析、连接池复用机制、超时与重试策略、拦截器链与自定义拦截器、EventListener监控

说实话,OkHttp 现在已经是 Android 网络请求的事实标准了。Retrofit 底层用的就是它,很多图片库也依赖它。但很多人只是把它当个黑盒用,出了问题就抓瞎。

我个人习惯,接手一个项目的第一件事,就是先看看 OkHttp 的配置。配置得好,能省掉后面一大堆性能问题。今天咱们就把 OkHttp 的核心配置和优化思路,掰开了聊一聊。

2.1 OkHttp 架构解析:它到底是怎么工作的?

OkHttp 的设计,说白了就是一套「请求-响应」的流水线。你发一个请求,它经过一系列处理,最后拿到结果。这个流水线的核心,就是 拦截器链

我画个简单的图帮你理解:

用户请求
    ↓
应用拦截器(自定义,比如加 Header、打日志)
    ↓
重定向与重试拦截器(RetryAndFollowUpInterceptor)
    ↓
桥接拦截器(BridgeInterceptor,处理 Cookie、Gzip 等)
    ↓
缓存拦截器(CacheInterceptor)
    ↓
连接拦截器(ConnectInterceptor,负责建立连接)
    ↓
网络拦截器(自定义,比如监控网络耗时)
    ↓
真正的网络请求(Socket 读写)
    ↓
响应返回

每个拦截器只干一件事,干完就传给下一个。这种设计的好处是,你想在哪一步插一脚,都很方便。

我记得有一次排查线上问题,发现某个接口偶尔会返回 302 重定向。默认情况下 OkHttp 会自动跟随重定向,但那次因为重定向次数太多,导致请求超时。后来我在应用拦截器里加了个计数逻辑,超过 3 次就直接断开,问题就解决了。

核心要点:OkHttp 的架构本质是「拦截器链 + 连接池」。理解了这个,你就能掌控它的行为。

2.2 连接池复用机制:别让每次请求都重新握手

你想想看,每次 HTTP 请求都要经历 TCP 三次握手、TLS 握手,这得多慢?尤其是 HTTPS,握手一次可能要几百毫秒。

OkHttp 的解决方案是 连接池。它会把用过的连接缓存起来,下次请求同一个域名时,直接复用已有的连接。

默认配置是这样的:

ConnectionPool pool = new ConnectionPool(
    5,          // 最大空闲连接数
    5,          // 存活时间
    TimeUnit.MINUTES  // 时间单位
);

这个配置够用吗?大部分场景是够的。但我在项目中遇到过一个问题:某个页面会同时发起 10 个请求到同一个域名,默认的 5 个空闲连接就不够了。结果后面的请求只能排队等连接释放,页面加载明显变慢。

我当时怎么调的?把最大空闲连接数改成 10,存活时间改成 10 分钟。效果立竿见影。

调优建议:如果你的应用有大量并发请求到同一个域名,适当增大连接池大小。但别太大,否则会占用过多内存。一般 5-10 个就够了。

还有一个坑要注意:连接泄漏。如果你在 Response 里没有关闭 body,连接就不会归还到池里。我曾经排查过一个内存泄漏问题,最后发现是某个同事在回调里忘了 close(),导致连接池里的连接越来越少,最终所有请求都超时。

避坑指南:一定要确保 Response.body().close() 被调用。用 try-finally 或者 Kotlin 的 use 块,养成习惯。

2.3 超时与重试策略:别让用户等太久

超时设置,是网络请求优化的第一道防线。设置得太短,容易误判;设置得太长,用户等得心焦。

OkHttp 提供了三个超时参数:

参数 默认值 说明
connectTimeout 10 秒 建立连接的超时时间
readTimeout 10 秒 读取数据的超时时间
writeTimeout 10 秒 写入数据的超时时间

我个人习惯,在移动端会把 connectTimeout 设成 5 秒。为什么?因为移动网络环境复杂,如果 5 秒连不上,再等下去大概率也是连不上。不如早点失败,让用户重试。

至于重试策略,OkHttp 默认会重试一次。但我觉得这个默认行为有点「傻」。它不管什么错误都重试,万一服务器已经挂了,重试只会加重负担。

我一般会自定义一个重试策略:

OkHttpClient client = new OkHttpClient.Builder()
    .retryOnConnectionFailure(true)  // 开启重试
    .addInterceptor(chain -> {
        Request request = chain.request();
        Response response = null;
        int retryCount = 0;
        while (retryCount < 3) {
            try {
                response = chain.proceed(request);
                if (response.isSuccessful()) {
                    return response;
                }
            } catch (IOException e) {
                // 只有网络异常才重试
                retryCount++;
                if (retryCount >= 3) {
                    throw e;
                }
                // 等待一段时间再重试
                Thread.sleep(1000 * retryCount);
            }
        }
        return response;
    })
    .build();

这个策略只对网络异常重试,而且每次重试间隔递增。避免了无脑重试带来的问题。

经验之谈:重试不是万能的。对于 4xx 错误(比如 401、403),重试也没用。对于 5xx 错误,可以重试,但最好加上指数退避。

2.4 拦截器链与自定义拦截器:在请求的每个环节插一脚

拦截器是 OkHttp 最灵活的地方。你可以用它做很多事情:

  • 应用拦截器:在请求发出前,添加公共 Header、打印日志、统计请求耗时。
  • 网络拦截器:在真正发起网络请求时,监控网络状态、修改请求体。

两者的区别在于:应用拦截器只调用一次,而网络拦截器在重定向和重试时会被多次调用。

我常用的一个自定义拦截器,是给所有请求加上设备信息:

public class DeviceInfoInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request request = original.newBuilder()
            .header("Device-Id", getDeviceId())
            .header("App-Version", getAppVersion())
            .header("Platform", "Android")
            .build();
        return chain.proceed(request);
    }
}

这样做的好处是,后端可以根据设备信息做统计和问题排查。有一次线上出了个 bug,只出现在某个旧版本上。就是因为我在 Header 里加了版本号,后端一查就定位到了。

小技巧:用拦截器做请求耗时统计。在 intercept 方法前后记录时间,差值就是请求耗时。配合 EventListener,能精确到每个环节。

2.5 EventListener 监控:把请求的每个细节都记录下来

EventListener 是 OkHttp 3.11 引入的监控接口。它能告诉你请求的每个阶段花了多少时间,比如 DNS 解析、连接建立、TLS 握手、请求发送、响应接收等。

用法很简单:

OkHttpClient client = new OkHttpClient.Builder()
    .eventListener(new EventListener() {
        @Override
        public void dnsStart(Call call, String domainName) {
            System.out.println("DNS 开始解析: " + domainName);
        }

        @Override
        public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList) {
            System.out.println("DNS 解析完成: " + inetAddressList);
        }

        @Override
        public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy) {
            System.out.println("开始建立连接: " + inetSocketAddress);
        }

        @Override
        public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy, Protocol protocol) {
            System.out.println("连接建立完成");
        }

        @Override
        public void requestHeadersEnd(Call call, Request request) {
            System.out.println("请求头发送完成");
        }

        @Override
        public void responseHeadersEnd(Call call, Response response) {
            System.out.println("响应头接收完成");
        }
    })
    .build();

我在项目中用 EventListener 做过一个性能监控系统。每次请求结束后,把各个阶段的时间上报到后台。然后我们就能看到:

  • 哪个接口的 DNS 解析特别慢?可能是 DNS 缓存没命中。
  • 哪个接口的 TLS 握手时间异常?可能是证书链太长。
  • 哪个接口的响应体接收时间过长?可能是数据量太大。

有一次我们发现某个接口的「连接建立」时间特别长,平均 800ms。排查后发现,是因为服务器配置了多个 IP,但其中一个 IP 的延迟很高。后来我们优化了 DNS 解析策略,把延迟高的 IP 排到后面,问题就解决了。

核心价值:EventListener 让你能看到请求的「黑盒」内部。没有它,你只能靠猜。有了它,你就能用数据说话。

2.6 总结:配置 OkHttp 的黄金法则

说了这么多,我总结几条黄金法则:

  1. 连接池别太小:根据并发量调整,一般 5-10 个空闲连接。
  2. 超时时间别太长:移动端 connectTimeout 建议 5 秒,readTimeout 建议 10 秒。
  3. 重试要有策略:只重试网络异常,加上指数退避。
  4. 拦截器做通用处理:加 Header、打日志、统计耗时。
  5. EventListener 做监控:把每个阶段的耗时记录下来,方便排查问题。

嗯,这些就是我这些年用 OkHttp 攒下来的经验。下一章咱们聊聊 DNS 优化与 HTTPDNS 实践,这可是解决「域名劫持」和「DNS 解析慢」的利器。