OkHttp缓存使用指南

在Http协议中,缓存的控制是通过首部的Cache-Control来控制,通过对Cache-Control进行设置,即可实现不同的缓存策略。

Http缓存

Cache-Control和其他的首部字段一样,使用key:value结构,同时value可有多个值, 值之间以,分隔(具体参考HTTP详解)。Cache-Control是一个通用首部字段,在Http请求报文中可使用,也可在应答报文中使用。

请求指令集(在请求报文中的取值):

  • no-cache: 不要缓存数据,直接从源服务器获取数据;
  • no-store: 不缓存请求或响应的任何内容;
  • max-age: 表示可接受过期过久的缓存数据,同指定了参数的max-stale;
  • max-stale: 表示接收过期的缓存,如后面未指定参数,则表示永远接收缓存数据。如max-stale: 3600, 表示可接受过期1小时内的数据;
  • min-fresh: 表示指定时间内的缓存数据仍有效,与缓存是否过期无关。如min-fresh: 60, 表示60s内的缓存数据都有效,60s之后的缓存数据将无效。
  • only-if-cache: 表示直接获取缓存数据,若没有数据返回,则返回504(Gateway Timeout)

应答指令集(在应答报文中的取值):

  • public: 可向任一方提供缓存数据;
  • private: 只向指定用户提供缓存数据;
  • no-cache: 缓存前需确认其有效性;
  • no-store: 不缓存请求或响应的任何内容;
  • max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;

  • must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。

更详细内容可参考:Http首部字段定义

了解了HTTP的理论知识,后面我们对OkHttp中的缓存进行简单的介绍。

OkHttp拦截器

OkHttp默认对Http缓存进行了支持,只要服务端返回的Response中含有缓存策略,OkHttp就会通过CacheInterceptor拦截器对其进行缓存。但是OkHttp默认情况下构造的HTTP请求中并没有加Cache-Control,即便服务器支持了,我们还是不能正常使用缓存数据。所以需要对OkHttp的缓存过程进行干预,使其满足我们的需求。

OkHttp的优雅之处就在于使用了责任链模式,将请求-应答过程中的每一步都通过一个拦截器来实现,并对此过程的头部和尾部都提供了扩展,这也为我们干预缓存过程提供了可能。所以在实现缓存之前,我们需要对OkHttp对拦截器的处理过程有个大概的了解。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest, this, eventListener);
    return chain.proceed(originalRequest);
}

以上代码就是整个拦截器的处理过程,具体的流程可参考源码,这里我们只说一下基本的流程:发起请求时,会按interceptors中加入的顺序依次执行,返回Response时按照逆序执行:

自定义拦截器 <-> 内置拦截器(retryAndFollowUpInterceptor...ConnectInterceptor)
<-> 网络拦截器 <-> CallServerInterceptor

其中CallServerInterceptor就是负责发送请求与接收应答的拦截器。由于我们关注的只是缓存,所以只考虑内置拦截器中的CacheInterceptor。那么流程可简化为:

Request <-> 自定义拦截器 <-> CacheInterceptor <-> 网络拦截器 <-> Response

从这个流程可以看出,如果服务端返回的Response中没有Cache-Control, 那么我们可通过添加网络拦截器来实现。同样,在访问缓存数据时,我们可通过添加自定义拦截器来实现。

使用OkHttp缓存

在开始添加缓存策略之前,我们先了解一个完整的缓存策略:
image

整体来说,在有网络的情况下,使用缓存还是比较复杂,这里我们通过简化版的缓存策略(有网络时访问服务器,无网络时返回缓存数据)来演示OkHttp使用缓存的过程。

image

首先,我们通过定义一个网络拦截器来为Response添加缓存策略:

public class HttpCacheInterceptor implements Interceptor {

    private Context context;

    public HttpCacheInterceptor(Context context) {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        return chain.proceed(chain.request()).newBuilder()
            .request(newRequest)
            .removeHeader("Pragma")
            .header("Cache-Control", "public, max-age=" + 1)
            .build();

        return response;
    }
}

其次,通过自定义拦截器设置Request使用缓存的策略:

public class BaseInterceptor implements Interceptor {

    private Context mContext;

    public BaseInterceptor(Context context) {
        this.mContext = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {

        if (NetworkUtil.isConnected(mContext)) {
            return chain.proceed(chain.request());    
        } else { // 如果没有网络,则返回缓存未过期一个月的数据
            Request newRequest = chain.request().newBuilder()
                    .removeHeader("Pragma")
                .header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
            return chain.proceed(newRequest);    
        }
    }
}

Pragma是Http/1.1之前版本遗留的字段,用于做版本兼容,但不同的平台对此有不同的实现,所以在使用缓存策略时需要将其屏蔽,避免对缓存策略造成影响。

将对修改Request和Response缓存策略的拦截器应用于OkHttp:

OkHttpClient httpClient = new OkHttpClient.Builder()
    .addInterceptor(new BaseInterceptor(context))
    .addNetworkInterceptor(new HttpCacheInterceptor(context))
    .cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 设置缓存路径和缓存容量
    .build();

接下来就可以在无网络的情况下愉快地使用缓存数据了。

不使用OkHttp的缓存

如果觉得OkHttp的缓存太复杂,想自己来缓存数据怎么办呢?有两种方案来实现:

  • 自定义拦截器,
  • 监听OkHttp的请求过程,在请求完成时缓存数据;

自定义拦截器

这种方案首先需要考虑应使用普通的拦截器还是网络拦截器,上面我们已经了解了整个请求过程中拦截器的执行顺序,需要注意的是:在无网络的情况下,请求在执行到CacheIntercepter,如果没有缓存数据,将会直接返回,并不会执行到自定义的网络拦截器中,所以不适合在网络拦截器中缓存数据。那么我们可通过自定义普通拦截器来实现,基本的过程如下:

@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {

    Response response = null;
    if (NetworkUtil.isConnected(mContext)) {
        response = chain.proceed(newRequest);
        saveCacheData(response); // 保存缓存数据
    } else { // 不执行chain.proceed会打断责任链,即后面的拦截器不会被执行
        response = getCacheData(chain.request().url()); // 获取缓存数据
    }

    return response;
}

监听OkHttp的请求过程

OkHttp: 使用这种方案你良心不会痛吗?

这种方案可以说摒弃了OkHttp扩展拦截器这一强大的功能,直接与请求和应答进行交互,基本的过程如下:

Request request = new Request.Builder()
    .url(realUrl)
    .build();
if (NetworkUtil.isConnected()) {
    httpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
            // 返回缓存数据
        }

        @Override
        public void onResponse(Response response) throws IOException {
            // 1. 缓存数据
            // 2. 返回请求结果
        }
    });
} else {
    // 返回缓存数据
}

优缺点比较

这两种方案都抛弃了OkHttp自己实现的缓存策略,所以更加灵活,尤其是监听OkHttp请求过程这种方法。但也都有一个很大的缺点:需要实现一个缓存模块。在开发中具体使用哪种缓存策略,根据已有代码模块和需求衡量即可。

注意点

  1. 对Response的缓存策略进行修改的拦截器一定要应用于网络拦截器,否则无法缓存数据,因为在Response返回的过程中,普通的拦截器在内置的CacheInterceptor之后执行;
  2. 修改Response的Cache-Control时,max-Age不能太大,否则你将在指定的max-Age时间内访问的始终是缓存数据(即便是有网的情况下);
  3. 实际的开发过程中,我们在网络请求中会添加一些公共参数,对于一些可变的公共参数,在缓存数据和访问缓存数据的过程中需要删除,比如网络类型,有网络时其值为Wifi或4G等,无网络时可能为none, 这时访问缓存时就会因url不一致导致访问缓存失败。
@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {

        // 添加公共参数
    HttpUrl.Builder urlBuilder = chain.request().url().newBuilder()
            .addQueryParameter("a", "a")
            .addQueryParameter("b", "b");
    Request.Builder requestBuilder = chain.request().newBuilder();
    if (NetworkUtil.isConnected(mContext)) {
        urlBuilder.addQueryParameter("network", NetworkUtil.getNetwokType(mContext));
    } else { // 无网络时不添加可变的公共参数
        requestBuilder.removeHeader("Pragma")
                .header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
    }
    Request newRequest = requestBuilder
            .url(urlBuilder.build())
            .build();

    return chain.proceed(newRequest);
}


@Override // HttpCacheInterceptor.java
public Response intercept(Chain chain) throws IOException {

    Response response = chain.proceed(chain.request());
    HttpUrl newUrl = chain.request().url().newBuilder()
                .removeAllQueryParameters("network")
                .build(); // 缓存数据前删除可变的公共参数
    Request newRequest = chain.request().newBuilder()
            .url(newUrl)
            .build();
    return response.newBuilder()
            .request(newRequest)
            .removeHeader("Pragma")
            .header("Cache-Control", "public, max-age=" + 1)
            .build();
}
,