Okhttp3缓存相关

虽然好多项目中用的都是okhttp3,但是一直没有弄明白缓存到底是怎么回事。正好趁着过完年不忙的这段时间,研究了一下关于缓存的知识。

本篇文章要弄明白的事情

一、网络缓存到底是什么?
二、okhttp3如何实现网络缓存?

下面开始正文

缓存的分类

缓存主要分为两种,服务端缓存,客户端缓存。这里我们只讨论客户端缓存。首先说一下HTTP缓存规则。
http缓存规则主要分为两种

强制缓存

在存在缓存的情况下


image.png

对比缓存

在存在缓存的情况下


image.png

可以看到两类缓存的区别,如果强制缓存生效的话,不需要在跟服务器发生交互,直接从缓存中去数据。而对比缓存,不管缓存是否生效都需要跟服务器发生交互。如果两种规则同时存在的话,强制缓存的优先级要高于对比缓存。也就是说,在执行强制缓存规则时,如果缓存生效,就不会在执行对比缓存。

强制缓存

从上面我们可以知道,如果强制缓存生效的话,客户端不需要在跟服务器发生交互,那么如果判断缓存是否生效呢?我们知道,在没有缓存时,客户端向服务器发出请求,服务器会将数据跟缓存规则一起返回,缓存规则信息包含在响应头中。
对于强制缓存来说,响应头中会包含两个字段来表明失效规则(Expires/Cache-Control)使用Chrome的开发者工具可以清楚的看到强制缓存生效时,网络请求的情况。

image.png

Expires

Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
所以HTTP 1.1 的版本,使用Cache-Control替代。

Cache-Control

Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。
private: 客户端可以缓存
public: 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)
max-age=xxx: 缓存的内容将在 xxx 秒后失效
no-cache: 需要使用对比缓存来验证缓存数据(后面介绍)
no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发(对于前端开发来说,缓存越多越好,so...基本上和它说886)

对比缓存

顾名思义就是需要进行比较来判断缓存是否可以使用,主要有两种

Last-Modified / If-Modified-Since

Last-Modified表示的是服务器返回的数据被最后一次修改的时间。If-Modified-Since是客户端在向服务器进行验证时带上缓存中存入的最后一次修改的时间,通知服务器进行比较,如果小于服务器上最后一次修改的时间,说明数据被再次修改过,则返回最新的数据,状态码200。如果大于等于服务器上最后一次修改的时间,咋说明数据没有被修改过,没有过期,返回状态码304通知客户端。

Etag / If-None-Match(优先级高于Last-Modified / If-Modified-Since)

Etag表示的是资源在服务器上的唯一标识。同样,当本地有缓存时,客户端带上If-None-Match 字段,请求服务器,服务器获取到后跟服务器上最新的Etag进行比较,如果一样,则说明数据是最新的,没有改动,返回304通知客户端缓存有效,否则,返回最新的数据。
上两张图总结一下Http缓存
浏览器第一次请求


image.png

浏览器再次请求时:


image.png

至此,第一个问题基本解决。

Okhttp3如何实现的缓存

我们知道Okhttp3中可以进行Cache的配置

okHttpClient.connectTimeout(TOME_OUT, TimeUnit.SECONDS)
.readTimeout(TOME_OUT,TimeUnit.SECONDS)
.cache(cache)//设置缓存
.addNetworkInterceptor(intercept)
.build();

Okhttp的缓存工作都是在CacheCacheInterceptor中完成的。我们来看一下

@Override public Response intercept(Chain chain) throws IOException {
//首先尝试获取缓存
Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;

long now = System.currentTimeMillis();

  //获取缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

//如果有缓存,更新下相关统计指标:命中率
if (cache != null) {
  cache.trackResponse(strategy);
}

//如果当前缓存不符合要求,将其close
if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// 如果不能使用网络,同时又没有符合条件的缓存,直接抛504错误
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();
}

// 如果有缓存同时又不使用网络,则直接返回缓存结果
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

//尝试通过网络获取回复
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());
  }
}

// 如果既有缓存,同时又发起了请求,说明此时是一个Conditional Get请求
if (cacheResponse != null) {
  // 如果服务端返回的是NOT_MODIFIED,缓存有效,将本地缓存和网络响应做合并
  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();

if (cache != null) {
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // 将网络响应写入cache中
    CacheRequest cacheRequest = cache.put(response);
    return cacheWritingResponse(cacheRequest, response);
  }

  if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }
}

return response;
  }

核心代码都用注释标出来了,这其中有个比较重要的类

Cache

cache管理器。

CacheStrategy

缓存策略,其内部维护着一个Request和一个Response。通过指定Request,Response是否为null来描述是通过网络获取还是缓存获取Response.这里我们主要看策略缓存的生成。

[CacheStrategy$Factory]

/**
 * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
 */
public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }

  return candidate;
}

/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
  // 若本地没有缓存,发起网络请求
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // 如果当前请求是HTTPS,而缓存没有TLS握手,重新发起网络请求
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // 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);
  }
    

  //如果当前的缓存策略是不缓存或者是conditional get,发起网络请求
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  //ageMillis:缓存age
  long ageMillis = cacheResponseAge();
  //freshMillis:缓存保鲜时间
  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());
  }

  //如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,则虽然缓存过期了,     //但是缓存继续可以使用,只是在头部添加 110 警告码
  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());
  }

  // 发起conditional get请求 就是对比缓存
  String conditionName;
  String conditionValue;
  if (etag != null) {
    conditionName = "If-None-Match";   //对应Etag
    conditionValue = etag;
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";  //对应 Last-Modified
    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);
}

其实上面的逻辑是跟我们分析第一个问题时的逻辑是一样的,就是加了各种判断去判断缓存。
至此,第二个问题也差不多清楚了。那么我们在日常开发时应该如何去使用。下面请看示例。

 public void doOkCall() throws IOException {
    Request build = new Request.Builder()
            .url("")
            .cacheControl(new CacheControl.Builder().maxAge(23, TimeUnit.SECONDS).build())
            .build();
    Call call = getClient().newCall(build);
    call.enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {

        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {

        }
    });
   
}

可以在请求的时候给每个Request设置CacheControl,里面有几种设置可选

public static final CacheControl FORCE_NETWORK = (new 
CacheControl.Builder()).noCache().build(); //强制使用网络请求
public static final CacheControl FORCE_CACHE;//强制使用缓存
private final boolean noCache; //不缓存
private final boolean noStore; //不缓存
private final int maxAgeSeconds; //设置过期时间

当然这样有缺点,就是可能有的服务器不支持缓存,那这样就会没效果,而且每个Request都得去设置,这样会很麻烦。
别急,还有一种方法,那就是自己创建一个拦截器,然后配置到Okhttp,然后直接拦截Response,手动给Response添加Header,让它支持缓存。

/**  
 * 缓存拦截器  
 */    
    private static class CacheInterceptor implements Interceptor {    

    @Override    
    public Response intercept(Chain chain) throws IOException {    

        Response originResponse = chain.proceed(chain.request());    

        //设置缓存时间为,并移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性    
        return originResponse.newBuilder().removeHeader("pragma")    //prama表示不支持缓存
                .header("Cache-Control", "max-age=10")//设置10秒    
                .build();    
    }    
}    

将此拦截器添加到你的OkhttpClient中,就可以进行你想要的缓存配置了。
最后,需要说明的一点,Okhttp的缓存时不支持Post的,只支持Get请求。因为post一般都是跟服务器进行交互的,缓存没有太大意义。
本文参考的文章
彻底弄懂http缓存
Okhttp源码分析--缓存策略

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 浅谈浏览器Http的缓存机制 ✦ ✦ ✦ ✦ ✦ ✦ ✦ ✦ 针对浏览器的http缓存的分析也算是老生常谈了,每隔...
    meng_philip123阅读 998评论 0 10
  • 注册了简书后,我慢慢地看了一些能进入首页推荐的文章,很多励志文章都认为生活要过得好,成为牛逼的人一定要拼,这点蘑菇...
    一地蘑菇阅读 638评论 4 3
  • 不久前,赶着秋的最后期限,赴了和银杏的一个约。 我喜欢她的纯净 南方的秋,不像北方的那么彻底,南方,有她一贯柔情与...
    山有木希阅读 606评论 11 11
  • 继上次独自旅行后,本来想好好缓一段时间的,奈何朋友邀约,于是青岛走了一波。 青岛,一个沿海的城市,温差变化较大,但...
    sweetseven七七阅读 296评论 0 0