继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

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

白板的微信
关注TA
已关注
手记 341
粉丝 70
获赞 387

前言

最近在写一个开源项目,需要用到 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,就是启动整个拦截器执行链的头结点。具体过程如下:

spacer.gif

https://img3.mukewang.com/5c2e18bb00012c9812400550.jpg


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

https://img.mukewang.com/5c2e18dc000122bb10380670.jpg



至于请求头与响应头,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 ());
...

结语

请求与响应的本质是不同主机利用各自的 IP 地址和端口号,通过 Socket 编程接口互相发送信息。为了约束数据交换格式,产生了 Http 协议。由于 Http 是明文传输,为了传输安全,又产生了 Https 协议。既然是协议,那么只有在双方都遵守的情况下才会生效。所以,在项目开发中,我们经常需要跟服务器兄弟进行接口联调,以保证约定被正确实现。OkHttp 扮演的角色类似于浏览器,共同点是都将请求与响应封装成了用户友好的形式,都支持错误重连、报文缓存等机制,不同的是浏览器还需要负责网页渲染等。

本文表面上描述的是如何利用 OkHttp 实现缓存控制,实则阐述了 OkHttp 的请求与响应的执行机制。所谓通则一通百通,利用 OKHttp 实现其它功能现在应该也不是问题了。比如实现一个加解密拦截器,对请求体进行加密,对响应报文进行解密,显然,这个拦截器,需要加到网络拦截器中。

OkHttp 的 Response 对象,是对真正响应报文(networkResponse 和 cacheResponse)的封装。所以,只要不在拦截器中调用 response.body() 方法,就不会导致请求阻塞,尤其是响应报文很大的时候,更不能调用。

最后,针对 Cahce-Control 有三点总结:

  • 要正确理解 Http 协议的约定,MDN 是个优秀的网站

  • 遇到问题多读源码,只有源码才不会骗人

  • 实践是检验真理的唯一标准


作者:Yuloran
原文链接:https://juejin.im/post/5c2cc4f15188256d0e5b0808



打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP