你真的了解 OkHttp 缓存控制吗?

本文为博主原创文章,如需转载,请在醒目位置注明出处


前言

最近在写一个开源项目,需要用到Http的缓存机制。由于项目所使用的Http客户端为OkHttp,所以需要了解如何使用OkHttp来实现Http的缓存控制。很惭愧,这一块不太熟悉,所以就到网上CV了一下。虽然我知道网上很多博客不太靠谱,但是没想到,居然真掉坑里了。

错误示例

不点名了,网上很多:

public class CacheControlInterceptor implements Interceptor
{
    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Request request = chain.request();

        if (!NetworkUtil.isNetworkConnected())
        {
            request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
        }

        Response.Builder builder = chain.proceed(request).newBuilder();
        if (NetworkUtil.isNetworkConnected())
        {
            // 有网络时, 不缓存, 最大保存时长为1min
            builder.header("Cache-Control", "public, max-age=60").removeHeader("Pragma");
        } else
        {
            // 无网络时,设置超时为1周
            long maxStale = 60 * 60 * 24 * 7;
            builder.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale).removeHeader("Pragma");
        }
        return builder.build();
    }
}

// 省略...
builder.addNetworkInterceptor(new CacheControlInterceptor());

这段代码的表现结果:请求成功后,断开网络,重新打开页面,1min内可以看到数据,1min后数据消失。

错误原因

在看了OKHttp拦截器调用源码以及Http Cache-Control后,发现上述代码可以说没有一行是正确的,也就是说逻辑完全不对:

  1. 没有网络时,修改请求头设为强制使用缓存的逻辑,应当置于普通拦截器(addInterceptor)中,而不是网络拦截器(addNetworkInterceptor)。因为没有网络时,OkHttp的ConnectInterceptor会抛出UnKnownHostException,终止执行后续拦截器。而networkInterceptors正是位于ConnectInterceptor之后;

  2. 对于OkHttp来说,即使服务器没有设置Cache-Control响应头,客户端也不用额外设置。因为在开启OkHttpClient的缓存功能后,GET请求的响应报文会被自动缓存。若要禁止缓存,在接口上加上@Headers("Cache-Control: no-store")注解即可;

  3. only-if-cached, max-stale是请求头的属性,而非响应头。

错误证明

直接从关键点切入:

RealCall::execute()

  @Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      // 发起请求并获得响应
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }

RealCall::getResponseWithInterceptorChain()

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    // 新建一个数组,并把所有拦截器都加进去。因为是数组,所以只能按照拦截器的添加顺序依次执行
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors()); // 1. 普通拦截器
    interceptors.add(retryAndFollowUpInterceptor); // 2. 连接重试拦截器 
    interceptors.add(new BridgeInterceptor(client.cookieJar())); // 3. 请求头,响应头再加工拦截器
    interceptors.add(new CacheInterceptor(client.internalCache())); // 4. 缓存保存与读取拦截器
    interceptors.add(new ConnectInterceptor(client)); // 5. 创建连接拦截器
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors()); // 6. 网络拦截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket)); // 7. 接口请求拦截器

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

从源码中可看出,所有拦截器都保存在同一个数组中,然后新建一个chain,并将该数组存储到这个chain中。这个chain,就是启动整个拦截器执行链的头结点。具体过程如下:

2722938-8dbea426f84587f0.png
OkHttp拦截器执行链

那么,为什么在网络拦截器中修改请求头为FORCE_CACHE没有用呢?因为在没有网络时,ConnectInterceptor会直接抛出UnKnownHostException,终止执行链继续向下执行,所以位于其后面的网络拦截器不会被执行:

2722938-0e8bdd3245ebdfea.png
UnKnownHostException

至于Cache-Control的值,如何设置才是正确的,Http Cache-Control 里有详细描述。

正确示例

无网时,强制使用缓存:

1. 创建请求头拦截器

public class RequestHeadersInterceptor implements Interceptor
{
    private static final String TAG = "RequestHeadersInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Logger.debug(TAG, "RequestHeadersInterceptor.");
        Request request = chain.request();
        Request.Builder builder = request.newBuilder();
        // builder.header("Content-Type", "application/json;charset=UTF-8")
        //       .header("Accept-Charset", "UTF-8");
        if (!NetworkService.getInstance().getNetworkInfo().isConnected())
        {
            // 无网络时,强制使用缓存
            Logger.debug(TAG, "network unavailable, force cache.");
            builder.cacheControl(CacheControl.FORCE_CACHE);
        }
        return chain.proceed(builder.build());
    }
}

NetworkService 是我写的网络连接探测器,基于API 21,需要的可以自取:点我

2. 添加请求头拦截器

// 缓存大小 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addInterceptor(new RequestHeadersInterceptor());
...

篡改服务器响应头

一般情况下,客户端不应该修改响应头。客户端使用什么样的缓存策略,应当由服务器兄弟确定。只有特殊情况下,才需要客户端额外配置。比如调用的是第三方服务器接口,其缓存策略不符合客户端的要求等。这里给出一个简单示例:

1. 创建响应头拦截器

public class CacheControlInterceptor implements Interceptor
{
    private static final String TAG = "CacheControlInterceptor";

    @Override
    public Response intercept(Chain chain) throws IOException
    {
        Logger.debug(TAG, "CacheControlInterceptor.");
        Response response = chain.proceed(chain.request());
        String cacheControl = response.header("Cache-Control");
        if (StringUtil.isEmpty(cacheControl))
        {
            Logger.debug(TAG, "'Cache-Control' not set by the backend, add it ourselves.");
            return response.newBuilder().removeHeader("Pragma").header("Cache-Control", "public, max-age=60").build();
        }
        return response;
    }
}

2. 添加响应头拦截器

// 缓存大小 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addNetworkInterceptor(new CacheControlInterceptor ());
...

结语

  • 要熟悉Http协议,MDN是个优秀的网站
  • 遇到问题多读源码,毕竟代码不会骗人
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值