美文网首页NetAndroid开发Android技术知识
关于Okhttp3(六)-CacheInterceptor

关于Okhttp3(六)-CacheInterceptor

作者: lowett | 来源:发表于2017-03-09 16:57 被阅读301次

    现在的app没有几个是不联网的了,在流量费用很高、速度一般的今天给用户合理节省流量,以及提高响应速度就显得尤为重要了。所以一个优秀的app都会在发展到一定程度后就会开始引入缓存,什么是缓存呢?

    百度百科:

    缓存就是数据交换的缓冲区(称作Cache),当某一硬件要读取数据时,会首先从缓存中查找需要的数据,如果找到了则直接执行,找不到的话则从内存中找。由于缓存的运行速度比内存快得多,故缓存的作用就是帮助硬件更快地运行。

    通俗一点就是:接杯水放在手边,渴了直接喝,没有去饮水机取。

    原理

    Okhttp3的网络缓存是基于http协议,如果不清楚,请自行搜索。

    对于缓存,可阅读,缓存简介

    使用DiskLruCache缓存策略

    注意点

    1. 目前只支持GET方式,其他请求方式需要自己实现
    2. 需要服务器配合,通过header相关的头来控制缓存
    3. 创建okhttpclient时候需要配置Cache

    流程

    1、如果配置缓存,则从缓存中取一次,不保证存在
    2、缓存策略
    3、缓存监测
    4、禁止使用网络(根据缓存策略),缓存又无效,直接返回
    5、缓存有效,不使用网络
    6、缓存无效,执行下一个拦截器
    7、本地有缓存,根具条件选择使用哪个响应
    8、使用网络响应
    9、 缓存到本地
    

    源码

    @Override 
    public Response intercept(Chain chain) throws IOException {
      // 1、如果配置缓存,则从缓存中取一次,不保证存在
      Response cacheCandidate = cache != null
          ? cache.get(chain.request())
          : null;
    
      long now = System.currentTimeMillis();
    
      // 2、缓存策略
      CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
      Request networkRequest = strategy.networkRequest;
      Response cacheResponse = strategy.cacheResponse;
    
      // 3、缓存监测
      if (cache != null) {
        cache.trackResponse(strategy);
      }
    
      if (cacheCandidate != null && cacheResponse == null) {
        closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
      }
    
      // 4、禁止使用网络(根据缓存策略),缓存又无效,直接返回
      // If we're forbidden from using the network and the cache is insufficient, fail.
      if (networkRequest == null && cacheResponse == null) {
        return new Response.Builder()
            .request(chain.request())
            .protocol(Protocol.HTTP_1_1)
            .code(504)
            .message("Unsatisfiable Request (only-if-cached)")
            .body(Util.EMPTY_RESPONSE)
            .sentRequestAtMillis(-1L)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
      }
    
      // 5、缓存有效,不使用网络
      // If we don't need the network, we're done.
      if (networkRequest == null) {
        return cacheResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .build();
      }
      // 6、缓存无效,执行下一个拦截器
      Response networkResponse = null;
      try {
        networkResponse = chain.proceed(networkRequest);
      } finally {
        // If we're crashing on I/O or otherwise, don't leak the cache body.
        if (networkResponse == null && cacheCandidate != null) {
          closeQuietly(cacheCandidate.body());
        }
      }
    
      // 7、本地有缓存,根具条件选择使用哪个响应
      // If we have a cache response too, then we're doing a conditional get.
      if (cacheResponse != null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {
          Response response = cacheResponse.newBuilder()
              .headers(combine(cacheResponse.headers(), networkResponse.headers()))
              .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
              .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
              .cacheResponse(stripBody(cacheResponse))
              .networkResponse(stripBody(networkResponse))
              .build();
          networkResponse.body().close();
    
          // Update the cache after combining headers but before stripping the
          // Content-Encoding header (as performed by initContentStream()).
          cache.trackConditionalCacheHit();
          cache.update(cacheResponse, response);
          return response;
        } else {
          closeQuietly(cacheResponse.body());
        }
      }
    
      // 8、使用网络响应
      Response response = networkResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .networkResponse(stripBody(networkResponse))
          .build();
     // 9、 缓存到本地
      if (HttpHeaders.hasBody(response)) {
        CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
        response = cacheWritingResponse(cacheRequest, response);
      }
    
      return response;
    }
    

    步骤分析

    读取缓存
    // 入口
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    // 主要是Cache类
    
    1. 通过url生成key(MD5、HEX)
    2. 通过key从内存中读取包装实体类Entry,内存中使用LinkedHashMap<String, Entry>
    3. 通过实体得到一个Snapshot,关联起文件系统中的缓存文件(缓存文件有多个,请求头文件、响应提文件),然后生成流(Source,Okio中的类,时间上就是inputStream)
    4. 通过快照得到一个Response实例
    5. 匹配是否是符合要求的,是返回响应,否关闭
    // 位置 okhttp3/Cache
    Response get(Request request) {
      // 1、
      String key = key(request.url());
      DiskLruCache.Snapshot snapshot;
      Entry entry;
      try {
         // 2、
        snapshot = cache.get(key);
        if (snapshot == null) {
          return null;
        }
      } catch (IOException e) {
        // Give up because the cache cannot be read.
        return null;
      }
    
      try {
         // 3、
        entry = new Entry(snapshot.getSource(ENTRY_METADATA));
      } catch (IOException e) {
        Util.closeQuietly(snapshot);
        return null;
      }
    
       // 4、
      Response response = entry.response(snapshot);
    
       // 5、
      if (!entry.matches(request, response)) {
        Util.closeQuietly(response.body());
        return null;
      }
    
      return response;
    }
    
    缓存策略的配置

    如果上一步能够得到缓存响应,则配置策略,主要是解析缓存中与响应有关的头(Date\Expires\Last-Modified\ETag\Age)

    // 2、缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    1. 解析缓存中与缓存有关的头

      
      public Factory(long nowMillis, Request request, Response cacheResponse) {
        this.nowMillis = nowMillis;
        this.request = request;
        this.cacheResponse = cacheResponse;
       // 有缓存响应
        if (cacheResponse != null) {
          this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
          this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
          Headers headers = cacheResponse.headers();
          for (int i = 0, size = headers.size(); i < size; i++) {
            String fieldName = headers.name(i);
            String value = headers.value(i);
            if ("Date".equalsIgnoreCase(fieldName)) {
              servedDate = HttpDate.parse(value);
              servedDateString = value;
            } else if ("Expires".equalsIgnoreCase(fieldName)) {
              expires = HttpDate.parse(value);
            } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
              lastModified = HttpDate.parse(value);
              lastModifiedString = value;
            } else if ("ETag".equalsIgnoreCase(fieldName)) {
              etag = value;
            } else if ("Age".equalsIgnoreCase(fieldName)) {
              ageSeconds = HttpHeaders.parseSeconds(value, -1);
            }
          }
        }
      }
      
    2. 根据一些条件实例一个CacheStrategy(get())

      private CacheStrategy getCandidate() {
        // No cached response.
        // 1、没有缓存响应,返回一个没有响应的策略
        if (cacheResponse == null) {
          return new CacheStrategy(request, null);
        }
      
        // 2、如果是https,丢失了握手缓存则,返回一个没有响应的策略
        // Drop the cached response if it's missing a required handshake.
        if (request.isHttps() && cacheResponse.handshake() == null) {
          return new CacheStrategy(request, null);
        }
      
        // 3、不能被缓存
        // If this response shouldn't have been stored, it should never be used
        // as a response source. This check should be redundant as long as the
        // persistence store is well-behaved and the rules are constant.
        if (!isCacheable(cacheResponse, request)) {
          return new CacheStrategy(request, null);
        }
      
        // 4、缓存控制
        CacheControl requestCaching = request.cacheControl();
        if (requestCaching.noCache() || hasConditions(request)) {
          return new CacheStrategy(request, null);
        }
      
        // 5、根据响应头
        long ageMillis = cacheResponseAge();
        long freshMillis = computeFreshnessLifetime();
      
        if (requestCaching.maxAgeSeconds() != -1) {
          freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
        }
      
        long minFreshMillis = 0;
        if (requestCaching.minFreshSeconds() != -1) {
          minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
        }
      
        long maxStaleMillis = 0;
        CacheControl responseCaching = cacheResponse.cacheControl();
        if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
          maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
        }
      
        if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
          Response.Builder builder = cacheResponse.newBuilder();
          if (ageMillis + minFreshMillis >= freshMillis) {
            builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
          }
          long oneDayMillis = 24 * 60 * 60 * 1000L;
          if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
            builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
          }
          return new CacheStrategy(null, builder.build());
        }
      
        // Find a condition to add to the request. If the condition is satisfied, the response body
        // will not be transmitted.
        String conditionName;
        String conditionValue;
        if (etag != null) {
          conditionName = "If-None-Match";
          conditionValue = etag;
        } else if (lastModified != null) {
          conditionName = "If-Modified-Since";
          conditionValue = lastModifiedString;
        } else if (servedDate != null) {
          conditionName = "If-Modified-Since";
          conditionValue = servedDateString;
        } else {
          return new CacheStrategy(request, null); // No condition! Make a regular request.
        }
      
        Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
        Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
      
        Request conditionalRequest = request.newBuilder()
            .headers(conditionalRequestHeaders.build())
            .build();
        return new CacheStrategy(conditionalRequest, cacheResponse);
      }
      

    只有一种情况是会有正常的缓存被使用:所有的缓存头符合要求,即第5条。

    缓存监测
    // 3、缓存监测
    if (cache != null) {
      cache.trackResponse(strategy);
    }
    

    此处记录缓存使用情况

    synchronized void trackResponse(CacheStrategy cacheStrategy) {
      requestCount++;
    
      if (cacheStrategy.networkRequest != null) {
        // If this is a conditional request, we'll increment hitCount if/when it hits.
        networkCount++;
      } else if (cacheStrategy.cacheResponse != null) {
        // This response uses the cache and not the network. That's a cache hit.
        hitCount++;
      }
    }
    
    禁止使用网络(根据缓存策略),缓存又无效,直接返回

    根据上面缓存策略的配置,这种情况不会发生,不清楚为什么有这个逻辑

    缓存有效,不使用网络

    通过缓存策略,如果符合要求将会把Request置空,Response不为空,所以直接使用缓存

    // 5、缓存有效,不使用网络
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    
    缓存无效,执行下一个拦截器

    如果缓存无效,将会执行下一个拦截器,等待响应结果

    本地有缓存,根具条件选择使用哪个响应
    // 本地有缓存,响应结果没有修改,合并两个响应
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
    
        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        // 更新缓存
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    
    使用网络响应

    以上都不符合,只能使用网络响应

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    
    缓存到本地
    // 9、 缓存到本地
    // 1.
    if (HttpHeaders.hasBody(response)) {
      // 2.
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }
    
    1. 根据头判断是否支持缓存

      1. 必须有响应体
      2. 内容有变化
    2. 是否符合缓存要求,根据策略

      
      private CacheRequest maybeCache(Response userResponse, Request networkRequest,
          InternalCache responseCache) throws IOException {
        // 1、没有响应体 不缓存
        if (responseCache == null) return null;
      
        // 2、是否支持
        // Should we cache this response for this request?
        // 2.1、根据头
        if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
          // 2.2、根据请求方式,有请求体的方式都不支持
          if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
              responseCache.remove(networkRequest);
            } catch (IOException ignored) {
              // The cache cannot be written.
            }
          }
          return null;
        }
      
        // 写入缓存
        // Offer this request to the cache.
        return responseCache.put(userResponse);
      }
      

    根据请求方式,有请求体的方式都不支持缓存

    1. 通过配置好的cache写入缓存

      写入缓存和读取缓存使用的方式类似,都是通过Cache,DiskLruCache

      CacheRequest put(Response response) {
        String requestMethod = response.request().method();
      
        if (HttpMethod.invalidatesCache(response.request().method())) {
          try {
            remove(response.request());
          } catch (IOException ignored) {
            // The cache cannot be written.
          }
          return null;
        }
        if (!requestMethod.equals("GET")) {
          // Don't cache non-GET responses. We're technically allowed to cache
          // HEAD requests and some POST requests, but the complexity of doing
          // so is high and the benefit is low.
          return null;
        }
      
        if (HttpHeaders.hasVaryAll(response)) {
          return null;
        }
      
        Entry entry = new Entry(response);
        DiskLruCache.Editor editor = null;
        try {
          editor = cache.edit(key(response.request().url()));
          if (editor == null) {
            return null;
          }
          // 提交缓存
          entry.writeTo(editor);
          return new CacheRequestImpl(editor);
        } catch (IOException e) {
          abortQuietly(editor);
          return null;
        }
      }
      

    总结

    缓存实际上是一个比较复杂的逻辑,单独的功能块,实际上不属于okhttp上的功能,只是通过http协议和DiskLruCache做了处理而已。

    系列文章

    1. 关于Okhttp(一)-基本使用
    2. 关于Okhttp(二)-如何下载查看源码
    3. 关于Okhttp3(三)-请求流程
    4. 关于Okhttp3(四)-RetryAndFollowUpInterceptor
    5. 关于Okhttp3(五)-BridgeInterceptor
    6. 关于Okhttp3(六)-CacheInterceptor

    相关文章

      网友评论

        本文标题:关于Okhttp3(六)-CacheInterceptor

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