美文网首页Android 开发技术源码解析
你真的了解 OkHttp 缓存控制吗?

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

作者: 1cf2c90a5564 | 来源:发表于2018-12-19 23:40 被阅读105次

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


    前言

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

    OkHttp拦截器执行链

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

    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 ());
    ...
    

    结语

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

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

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

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

    • 要正确理解Http协议的约定,MDN 是个优秀的网站
    • 遇到问题多读源码,只有源码才不会骗人
    • 实践是检验真理的唯一标准

    相关文章

      网友评论

        本文标题:你真的了解 OkHttp 缓存控制吗?

        本文链接:https://www.haomeiwen.com/subject/euxskqtx.html