4、OkHttp高级:连接池与复用、缓存策略、超时与重试、自定义DNS

好,咱们继续深入。前面几章我们把OkHttp的基础用法和拦截器机制聊透了。今天要讲的这几个点,说白了就是让你的网络请求「又快又稳又省钱」。连接池、缓存、超时重试、自定义DNS——每一个都是我实际项目中踩过坑的地方。

4.1 连接池与复用:别让TCP握手续命

你想想看,每次发一个HTTP请求,都要经历三次握手、四次挥手。如果请求频繁,这开销就大了去了。OkHttp怎么解决的?它搞了个连接池,把用过的TCP连接缓存起来,下次请求直接复用。

我个人习惯,在构建OkHttpClient时,会显式配置连接池参数:

OkHttpClient client = new OkHttpClient.Builder()
    .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
    .build();

这里两个参数:最大空闲连接数5个,空闲存活时间5分钟。什么意思?就是池子里最多保留5个空闲连接,超过5分钟没人用就干掉。

我的经验:连接数不是越大越好。我曾经在一个高并发项目里把最大连接数设成20,结果服务器扛不住了。后来压测发现,5个连接复用,配合Keep-Alive,性能反而最好。

OkHttp默认的连接池是5个连接,5分钟超时。大部分场景够用。但如果你做的是长连接应用,比如IM或推送,建议调大连接数,缩短超时时间。

4.2 缓存策略:让数据少跑网线

缓存这东西,说白了就是用空间换时间。OkHttp的缓存机制基于HTTP协议本身的缓存头,比如Cache-ControlExpiresETag这些。

先配缓存目录和大小:

File cacheDir = new File(context.getCacheDir(), "http_cache");
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024); // 10MB

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

然后服务端返回的响应头里带上缓存策略。比如:

Cache-Control: max-age=3600

这表示这个响应在1小时内可以直接用缓存,不用发请求。

但问题来了——服务端不一定配合。我遇到过很多后端同学根本不设缓存头。怎么办?

嗯,这里要注意。你可以在拦截器里手动添加缓存策略:

Interceptor cacheInterceptor = chain -> {
    Request request = chain.request();
    Response response = chain.proceed(request);
    
    // 如果服务端没设缓存,我们强制缓存5分钟
    if (response.cacheControl().maxAgeSeconds() == -1) {
        return response.newBuilder()
            .header("Cache-Control", "max-age=300")
            .build();
    }
    return response;
};
避坑指南:我曾经在一个新闻客户端里强制缓存了10分钟,结果用户刷新后看到的还是旧新闻。后来改成「先展示缓存,再后台刷新」的策略,体验好多了。缓存策略一定要跟业务场景匹配。

4.3 超时与重试:别让用户等太久

网络请求最怕什么?卡死。用户等个十几秒没反应,直接关应用了。所以超时设置是必须的。

OkHttp提供了三个超时维度:

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

我个人习惯这样配:

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .build();

为什么读和写设30秒?因为上传大文件或下载大文件时,10秒可能不够。连接超时反而要短一点,15秒连不上就别等了。

再说重试。OkHttp默认不会自动重试,但你可以通过拦截器实现:

Interceptor retryInterceptor = 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;
};
核心原则:重试只针对网络异常,不要对业务错误码(比如404、500)重试。而且重试要有间隔,别像机关枪一样突突突发请求,会把服务器打挂的。

4.4 自定义DNS:打破域名解析的限制

DNS解析,说白了就是把域名转成IP。默认情况下,OkHttp用的是系统DNS。但有些场景需要自定义:

  • 你想绕过DNS污染
  • 你想做域名到IP的映射(比如测试环境)
  • 你想实现动态域名解析

OkHttp提供了Dns接口,实现起来很简单:

OkHttpClient client = new OkHttpClient.Builder()
    .dns(new Dns() {
        @Override
        public List<InetAddress> lookup(String hostname) throws UnknownHostException {
            if ("api.example.com".equals(hostname)) {
                // 直接返回固定IP
                return Collections.singletonList(
                    InetAddress.getByAddress(
                        new byte[]{(byte)192, (byte)168, 1, 100}
                    )
                );
            }
            // 其他域名走系统DNS
            return Dns.SYSTEM.lookup(hostname);
        }
    })
    .build();

更高级的用法,你可以集成HTTPDNS服务。比如阿里云HTTPDNS,它能避免Local DNS劫持,提升解析速度。

我的经验:有一次客户反馈某个地区网络特别慢,查了半天发现是当地DNS解析出了问题。后来接入了HTTPDNS,问题直接解决。如果你做的是全国性应用,强烈建议考虑HTTPDNS。

还有个细节:自定义DNS返回的IP列表,OkHttp会按顺序尝试连接。如果第一个IP连不上,会自动尝试下一个。这相当于内置了故障转移。

小结

今天这四个点,说白了就是让网络请求更高效、更稳定。连接池省握手开销,缓存省网络流量,超时重试防卡死,自定义DNS解域名问题。

嗯,这些配置看起来简单,但每个参数背后都有实际场景的考量。我建议你在项目里动手试试,调一调参数,看看效果。纸上得来终觉浅,绝知此事要躬行。

下一章我们聊聊Retrofit,它是OkHttp的最佳拍档。到时候你会发现,很多今天讲的东西,在Retrofit里都能用上。