10、网络优化(下):OkHttp拦截器链、Retrofit缓存策略、弱网环境优化、预连接技术

网络优化这个话题,咱们上回聊了基础层面。今天接着往下挖,聊聊那些真正能让App在网络层面“飞起来”的硬核技术。说白了,就是OkHttp的拦截器链怎么玩、Retrofit的缓存怎么配、弱网下怎么保命、以及预连接这种“预判”技术。

我个人习惯把网络优化分成两派:一派是“防守型”,比如重试、降级、缓存;另一派是“进攻型”,比如预连接、预加载。今天咱们攻守兼备,全盘拿下。

网络优化(下)知识体系 网络优化核心 OkHttp拦截器链 应用拦截器 → 网络拦截器 Retrofit缓存策略 Cache-Control / 离线缓存 弱网环境优化 超时 / 重试 / 降级 预连接技术 ConnectionPool / Warm-up 攻守兼备,全面提升网络体验

一、OkHttp拦截器链:你以为是排队,其实是套娃

OkHttp的拦截器链,很多人刚接触时觉得就是“请求进来,一层层过,最后出去”。其实没那么简单。它分两种:应用拦截器网络拦截器。区别在哪?我直接说结论:应用拦截器只调用一次,网络拦截器可能被调用多次(比如重定向或重试时)。

举个例子,你加了一个日志拦截器。如果放在应用拦截器里,你只会看到一次请求日志;如果放在网络拦截器里,重试三次你就看到三条日志。嗯,这里要注意,别被日志数量吓到。

核心区别速览:
  • 应用拦截器(addInterceptor):在请求发出前、响应返回后各执行一次。适合加公共Header、打印日志、统计流量。
  • 网络拦截器(addNetworkInterceptor):在每次网络请求前后执行。适合处理重定向、重试、缓存策略。

我在项目中遇到过一个问题:有个同事把Token刷新逻辑放在了网络拦截器里,结果一次请求因为重定向触发了三次Token刷新,直接把服务器打挂了。你想想看,这得多冤?所以我的建议是:业务逻辑放应用拦截器,网络层面的操作放网络拦截器

// 应用拦截器示例:统一添加Token
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(chain -> {
        Request original = chain.request();
        Request newRequest = original.newBuilder()
                .header("Authorization", "Bearer " + getToken())
                .build();
        return chain.proceed(newRequest);
    })
    .addNetworkInterceptor(chain -> {
        // 这里只做网络层面的监控
        Request request = chain.request();
        Response response = chain.proceed(request);
        // 记录网络耗时
        return response;
    })
    .build();
小技巧:如果你想统计“真实网络耗时”,一定要用网络拦截器。应用拦截器统计的时间包含了缓存命中、重定向等非网络时间,不准。

二、Retrofit缓存策略:别让服务器背锅

Retrofit本身不负责缓存,它只是OkHttp的封装。缓存策略其实是在OkHttp层面配的。但很多人只配了OkHttp的缓存,却忘了在Retrofit的接口注解里加Cache-Control。结果呢?缓存没生效,还怪服务器。

我习惯的做法是:服务器端和客户端双管齐下。服务器返回的响应头里带上Cache-Control,客户端再根据业务需求覆盖或补充。

缓存策略 适用场景 注意事项
no-cache 每次都要验证,但可以用缓存 配合ETag或Last-Modified使用
no-store 敏感数据,完全不缓存 比如支付Token、验证码
max-age=3600 一小时内的数据可复用 适合列表页、配置信息
only-if-cached 离线模式,只用缓存 配合离线策略使用

我曾经接手过一个项目,用户反馈“列表数据一直不更新”。查了半天,发现是OkHttp的缓存目录没配权限,缓存根本写不进去。嗯,这种坑,你不踩一次真的想不到。

// 配置OkHttp缓存
int cacheSize = 10 * 1024 * 1024; // 10MB
Cache cache = new Cache(cacheDir, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(chain -> {
        Request request = chain.request();
        // 弱网环境下强制使用缓存
        if (isWeakNetwork()) {
            request = request.newBuilder()
                    .header("Cache-Control", "only-if-cached")
                    .build();
        }
        return chain.proceed(request);
    })
    .build();
注意:Retrofit的@Headers注解里写的Cache-Control优先级高于OkHttp拦截器里设置的。如果你在接口注解里写了“no-cache”,那拦截器里再怎么配也没用。我建议统一在拦截器里控制,方便动态调整。

三、弱网环境优化:别让用户干等

弱网优化,说白了就是“在信号不好的时候,怎么让App不死、不卡、不崩溃”。我见过太多App在弱网下直接白屏、ANR、甚至闪退。用户本来信号就不好,你还给他添堵?

我个人习惯从三个层面入手:超时控制、重试策略、降级方案

超时控制:别用默认的超时时间。OkHttp默认10秒,但弱网下10秒可能根本不够。我一般设成30秒,但配合“快速失败”机制——如果前两次重试都超时,第三次直接放弃。

重试策略:不要无脑重试。我见过一个App在弱网下重试了8次,每次间隔1秒,结果用户等了8秒,最后还失败了。我的做法是:指数退避 + 最大重试次数。第一次等1秒,第二次等2秒,第三次等4秒,最多重试3次。

// 自定义重试拦截器
public class RetryInterceptor implements Interceptor {
    private int maxRetry = 3;
    private long baseDelay = 1000; // 1秒

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        IOException lastException = null;

        for (int i = 0; i <= maxRetry; i++) {
            try {
                response = chain.proceed(request);
                // 如果成功,直接返回
                if (response.isSuccessful()) {
                    return response;
                }
            } catch (IOException e) {
                lastException = e;
                // 指数退避
                long delay = baseDelay * (long) Math.pow(2, i);
                try {
                    Thread.sleep(delay);
                } catch (InterruptedException ignored) {}
            }
        }
        throw lastException != null ? lastException : new IOException("重试失败");
    }
}

降级方案:这是最关键的。弱网下,别想着“完美体验”,先保证“能用”。比如图片加载失败就显示占位图,列表加载失败就显示缓存数据,接口超时就提示“网络不给力,请稍后再试”。

避坑指南:我曾经在弱网环境下测试,发现重试时没有关闭上一次的Response,导致内存泄漏。记住:每次重试前,如果上一次的Response没有关闭,一定要先close()。

四、预连接技术:提前握手,减少等待

预连接,说白了就是“在用户真正需要网络请求之前,先把TCP连接建好”。你想想看,每次HTTP请求都要经历DNS解析、TCP三次握手、TLS握手(如果是HTTPS),这些时间加起来可能几百毫秒。如果能在用户点击按钮之前就完成这些,那体验不就丝滑了吗?

OkHttp内部有一个ConnectionPool,默认最多保持5个空闲连接,每个连接最多存活5分钟。但这是“被动”的——只有请求发出去才会创建连接。预连接要做的是“主动”创建。

我常用的做法有两种:

  1. 启动时预连接:App启动后,立即向核心API域名发起一个“空请求”(比如一个轻量级的配置接口),这样连接池里就有了热连接。
  2. 空闲时预连接:在用户浏览当前页面时,提前预判他下一步可能访问的域名,提前建立连接。
// 预连接示例:启动时预热
public class NetworkPreWarm {
    public static void warmUp(OkHttpClient client, String url) {
        Request request = new Request.Builder()
                .url(url)
                .header("Connection", "keep-alive")
                .build();
        // 异步发起一个轻量请求,只为了建立连接
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                // 预连接失败不影响主流程
            }

            @Override
            public void onResponse(Call call, Response response) {
                // 关闭响应,但连接会留在连接池中
                response.close();
            }
        });
    }
}
预连接的注意事项:
  • 不要预连接太多域名,否则会占用系统资源。我一般控制在3-5个核心域名。
  • 预连接的请求一定要轻量,最好是一个空GET请求或者HEAD请求。
  • 如果用户长时间没有后续请求,连接会被回收。所以预连接的时间窗口很重要——我一般在用户进入页面后的2秒内完成预连接。

我记得有一次优化一个电商App的首页加载速度。首页有6个接口,每次打开都要等所有接口返回。我做了两件事:一是把其中3个不关键的接口改成懒加载;二是对核心的2个接口做预连接。结果首页加载时间从2.8秒降到了1.2秒。你想想看,这提升多明显?

好了,网络优化这块的内容就这些。核心思路就是:能缓存就缓存,能预连就预连,弱网下别硬撑,该降级就降级。把这些技术用好,你的App在网络层面基本就稳了。