关于Okhttp3(六)-CacheInterceptor

现在的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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容