OkHttp source code analysis (5) cache center CacheInterceptor

Get into the habit of writing together! This is the second day of my participation in the "Nuggets Daily New Plan · April Update Challenge", click to view the details of the event .

CacheInterceptorAs you can see from the name, it is the interceptor responsible for caching. The first article said that one of the features of OkHttp is that it provides default cache support, and the Cacheclass is responsible for saving and retrieving the cache.
The internal configuration cache strategy is also consistent with the Http configuration, and is configured using the Cache-ControlHeader field. Equivalent to the front-end's own caching proxy server, OkHttp also implements the Cache-Controllogic of the fields in Http.
Since it is a cache, then analyze its source code through four aspects, that is 增删改查, comprehensively analyze its working principle.

Cache class and InternalCache

CacheA class is a caching functional unit implemented by the OkHttp framework and CacheInterceptoris used in . In the interceptor, the cache is mainly InternalCacheused abstractly through the interface dependency, and the method of how to delete, modify and check is defined inside the interface. CacheIt does not implement InternalCachethe interface, but uses a combination method, which is the specific implementation of CacheInterceptor``的构造方法中,新建一个InternalCache ,当作第一个代理,使用Cache```.

this.internalCache = new InternalCache() {
    @Nullable
    public Response get(Request request) throws IOException {
        return Cache.this.get(request);
    }

    @Nullable
    public CacheRequest put(Response response) throws IOException {
        return Cache.this.put(response);
    }

    public void remove(Request request) throws IOException {
        Cache.this.remove(request);
    }

    public void update(Response cached, Response network) {
        Cache.this.update(cached, network);
    }

    public void trackConditionalCacheHit() {
        Cache.this.trackConditionalCacheHit();
    }

    public void trackResponse(CacheStrategy cacheStrategy) {
        Cache.this.trackResponse(cacheStrategy);
    }
};
复制代码

We can implement a cache module by ourselves, and configure our own cache through OkHttpClientthe setInternalCache(InternalCache internalCache)method.

check

The query here has two levels. The first is a common query, which gets the cached information, and the second is to check the validity and whether the cache can be used. All belong to the "check" content.

get cache

final InternalCache cache;

Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;
复制代码

The acquisition of the cache is mainly obtained through the above code, and the get method of the cache is directly called, where the cache is directly the Response object. Passing in a Request object is the Request object created when we request the network. Let's look at Cache#getthe implementation.

@Nullable Response get(Request request) {
  String key = key(request.url());
  DiskLruCache.Snapshot snapshot;
  Entry entry;
  try {
    snapshot = cache.get(key);
    if (snapshot == null) {
      return null;
    }
  } catch (IOException e) {
    // Give up because the cache cannot be read.
    return null;
  }
  try {
    entry = new Entry(snapshot.getSource(ENTRY_METADATA));
  } catch (IOException e) {
    Util.closeQuietly(snapshot);
    return null;
  }
  Response response = entry.response(snapshot);
  if (!entry.matches(request, response)) {
    Util.closeQuietly(response.body());
    return null;
  }

  return response;
}
复制代码

Cache内部使用了LRU这种非常典型的数据结构来存储和清除数据,具体的实现是DiskLruCache,感兴趣的可以看看。这里的逻辑也比较简单。首先获取缓存的key,通过key()方法获取。

public static String key(HttpUrl url) {
  return ByteString.encodeUtf8(url.toString()).md5().hex();
}
复制代码

这里获取了请求url的me5并转成了16进制。拿到key后从缓存中获取,最后通过entry.matches()判断是否匹配,不匹配那么就返回null,表示当前没有缓存。

public boolean matches(Request request, Response response) {
  return url.equals(request.url().toString())
      && requestMethod.equals(request.method())
      && HttpHeaders.varyMatches(response, varyHeaders, request);
}
复制代码

这里过滤了三个条件,首先新请求的url要和缓存的Response的url一致。并且请求的方法要一致,get、post这些。
最后一个条件比较复杂,varyHeaders这里存储的响应中header字段在请求中也存在的字段,在提取响应时,这部分相同的header也要都包含在新的请求中。
满足了这三个条件,就可以正确的获取缓存的Response。

可用性检查

成功获取到缓存后,还要进行可用性的检查。这段逻辑在CacheInterceptor中,主要是用了策略类CacheStrategy,在调用完成get方法后,会执行这段逻辑。cacheCandidate存储了上面获取到的缓存。

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
。。。
复制代码

通过CacheStrategy的get的方法内部进行检查,输出了networkRequest和cacheResponse两个变量,表示缓存可用性的结果。主要使用通过null和非null来处理的,在分析CacheStrategy的内部逻辑前,先来分析下结果的意义。

networkRequest cacheResponse 结果
Null 非Null 缓存可用,直接返回缓存
Null Null 请求失败,直接构建一个504响应返回
非Null 非Null或Null 直接请求网络,调用下一个拦截链

发现如果内部的networkRequest如果非空,那么肯定是要请求网络了。如果为空,那么说明要使用缓存,这时候还要看内部的cacheResponse的情况,如果非Null,就直接使用,如果为Null,直接请求失败。
下面具体看下CacheStrategy中的实现。 缓存的策略的逻辑都在CacheStrategy中了。

CacheStrategy的创建

创建是通过一个内部类工厂进行创建的。通过类的名称--CacheStrategy。就可以知道他使用了策略模式。通过工厂的get方法获取不同的策略。

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);
      }
    }
  }
}
复制代码

上面的对CacheStrategy的创建,传入了缓存的cacheResponse、当前的时间、request请求对象。并从缓存的cacheResponse的header中获取几个和缓存有关的字段,在后面对缓存的处理中,都是非常有用的。

标题 用处
Date 报文创建的日期和时间
Expires 包含日期/时间, 表示在此时候之后,响应过期
Last-Modified 最后一次修改的时间
ETag 实体的标识符,类似hash的作用
Age 表示缓存存在的时间,一般是和Date的差值

CacheStrategy的策略获取

缓存策略的获取主要在工厂类的get方法中。
通过上面讲的输出结果,可以很好的理解下面每一个判断的意义。

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // 如果缓存不生效,但是必须使用缓存,请求失败,对应上面输出结果的双null。
    return new CacheStrategy(null, null);
  }

  return candidate;
}

复制代码

获取的缓存策略都在getCandidate()中,内部的辑比较长,我们逐一分析。
缓存策略在前端分为强缓存和协商缓存,如果触发了强缓存,那么就会直接返回缓存数据。如果没有触发强缓存,那么看可不可以走协商缓存,也就是和服务器沟通,看本地数据是否可用,不需要重复传递主体数据部分。CacheStrategy的处理也分成这两部分。只是在前面多了可用性检查。

没有缓存
if (cacheResponse == null) {
  return new CacheStrategy(request, null);
}
复制代码

如果没有拿到缓存,那么直接返回有带有request的策略,也就是直接请求网络。

https下的缓存
if (request.isHttps() && cacheResponse.handshake() == null) {
  return new CacheStrategy(request, null);
}
复制代码

如果新的请求是Https类型的,但是缓存中更没有SSL的握手的信息,那么直接请求网络。

不可缓存
if (!isCacheable(cacheResponse, request)) {
  // 不可缓存,直接请求网络
  return new CacheStrategy(request, null);
}

public static boolean isCacheable(Response response, Request request) {
  // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
  // This implementation doesn't support caching partial content.
  switch (response.code()) {
    case HTTP_OK: 200
    case HTTP_NOT_AUTHORITATIVE: 203
    case HTTP_NO_CONTENT: 204
    case HTTP_MULT_CHOICE:300
    case HTTP_MOVED_PERM: 301
    case HTTP_NOT_FOUND: 404
    case HTTP_BAD_METHOD: 405
    case HTTP_GONE: 410
    case HTTP_REQ_TOO_LONG: 414
    case HTTP_NOT_IMPLEMENTED: 510
    case StatusLine.HTTP_PERM_REDIRECT: 308
      // These codes can be cached unless headers forbid it.
      break;

    case HTTP_MOVED_TEMP: 302
    case StatusLine.HTTP_TEMP_REDIRECT: 307
      if (response.header("Expires") != null
          || response.cacheControl().maxAgeSeconds() != -1
          || response.cacheControl().isPublic()
          || response.cacheControl().isPrivate()) {
        break;
      }
      // Fall-through.

    default:
      return false;
  }

  return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
复制代码

上面的代码判断了能否缓存的特性,这在插入缓存时,也进行了判断。这里防治自定义的缓存的数据无效性,内部做了兜底的预防。
主要是通过状态码进行判断,在缓存是302和307的状态码下,需要至少满足4个条件中一个,才可以缓存。

  1. 缓存Header中有Expires,Expires上面也说过,包含日期/时间, 即在此时候之后,响应过期,有这个字段表示是可以缓存的。
  2. Header中cacheControl字段有public、private、maxAge等参数,前两个表示是否私有,maxAge表示最大的age值,也就是缓存已经存在时间的最大值。

如果上面的条件都满足,那么还要看请求和缓存响应的cacheControl是否有noStore字段,表示不允许缓存。 经过上面的方法,如果是不可以缓存的,那么也是直接请求网络。

CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
  return new CacheStrategy(request, null);
}
private static boolean hasConditions(Request request) {
  return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
}
复制代码

上面判断了请求的缓存控制字段,如果是noCache,也就是不使用缓存,那么直接请求网路。或者hasConditions返回true,hasConditions方法内部判断了两个字段

  1. If-Modified-Since:服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回
  2. If-None-Match,服务器上没有资源和上传的eTag一致时,才回正常响应,

上面这两个属性都是需要向服务器确认的,所以需要请求网络。

上面就是缓存策略的整体逻辑,下面介绍下,Http缓存的知识。

强缓存

强缓存通过缓存生存时间来判断的。也就是看本地取出的缓存死期到没到,如果到了就需要重新拿网络了。既然要算当前到没到死期,需要知道两个变量,第一个是已经的生存时间,第二个就是最大生存时间。 Http内部的缓存策略还有几个变量共同作用,这些都在OkHttp中CacheStrategy体现出来了。
分两部分分析,首先介绍用到的变量,再介绍具体介绍运算过程。

1. ageMillis 已经生存时间
通过```cacheResponseAge()```获取已经生存时间。
复制代码
private long cacheResponseAge() {
  long apparentReceivedAge = servedDate != null
      ? Math.max(0, receivedResponseMillis - servedDate.getTime())
      : 0;
  long receivedAge = ageSeconds != -1
      ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
      : apparentReceivedAge;
  long responseDuration = receivedResponseMillis - sentRequestMillis;
  long residentDuration = nowMillis - receivedResponseMillis;
  return receivedAge + responseDuration + residentDuration;
}
复制代码
字段值 意义
servedDate 取data字段,服务器在创建缓存响应创建时间
receivedResponseMillis OkHttp接收响应时的时间
ageSeconds 取age字段服务器计算的已经生存时间
sentRequestMillis OkHttp发送请求时的时间

一个响应的已经生存时间分为几个部分呢?首先是在服务端的生存时间,第二个是在传输中的时间,第三个是本地持续缓存的时间。这几个时间相加就是最终的生存时间。
服务端的生存时间:在服务器的生存时间有两个首部字段影响,第一个就是Date表示这个响应报文的创建时间,第二个时age,表示服务端计算的生存的时间。我们需要这个两个变量共同计算,取最大值。前端计算在服务器的生存时间,只能通过接收响应的时间减去创建时间,但是还要和服务器计算的生存时间进行对比。 传输时间:就是OkHttp前端记录的接收响应和发送请求的差值 本地持续缓存时间:当前的时间距离接收响应的时间
通过上面几个变量的整体运算,就可以得到已经生存时间了。

2. freshMillis 最大生存时间
private long computeFreshnessLifetime() {
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (responseCaching.maxAgeSeconds() != -1) {
    return SECONDS.toMillis(responseCaching.maxAgeSeconds());
  } else if (expires != null) {
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : receivedResponseMillis;
    long delta = expires.getTime() - servedMillis;
    return delta > 0 ? delta : 0;
  } else if (lastModified != null
      && cacheResponse.request().url().query() == null) {
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : sentRequestMillis;
    long delta = servedMillis - lastModified.getTime();
    return delta > 0 ? (delta / 10) : 0;
  }
  return 0;
}
复制代码

通过computeFreshnessLifetime()方法获取最大生存时间,主要通过Http首部字段的max-age和expires获取最大生存时间。

max-age是一个相对时间,也就是相对创建时间,最大可能生存多少时间。而expires是一个绝对时间,表示一个时间戳,过了这个时刻,缓存就失效了,但是因为前端本地可以修改时间,所以expires优先级低于max-age,设计max-age就是为了替代expires

如果这两个字段没获取到,那么根据 HTTP RFC 的建议并在 Firefox 中实现,文档的max age 应默认为文档提供时文档年龄的 10%。默认到期日日期不用于包含查询的 URI。

3. minFreshMillis

如果这个值是x,表示过了x,必须还是有效的。这个值和max-age有关,也就是过了x,他的生存时间还要在max-age以内。直接通过CacheControl#minFreshSeconds()获取。

4. maxStaleMillis

能容忍的最大过期时间,直接通过CacheControl#maxStaleSeconds()获取。

拿到了上面的4个时间,就可以进行强缓存的合法性判断了。

计算过程
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
  Response.Builder builder = cacheResponse.newBuilder();
  。。。
  return new CacheStrategy(null, builder.build());
}
复制代码

对应输出策略,如果数据合法,那么就直接使用缓存。
首先看了缓存响应的no-cache字段,这个值在请求中的意思是不使用缓存,但是在响应中的意义并不是不使用缓存,而是在使用之后,必须和服务器进行验证。
第一个条件如果设置了no-cache,那么肯定不能使用缓存了,需要请求服务器验证。 第二个条件中计算了各个值,ageMillis + minFreshMillis < freshMillis + maxStaleMillis,看似很复杂,但是结合上面的各个变量的详细分析,很容易确定内部的逻辑,正常的逻辑是已经生存时间<最大生存时间即可,即ageMillis < freshMillis,但是有两个维度扩展了判断的合法区间,分别是minFreshMillis和maxStaleMillis,minFreshMillis减少了合法区间,因为要求更高了。maxStaleMillis增大了合法区间,因为要求更小了。
如果上面两个条件都满足,那么就会创建一个使用缓存的策略。

以上就是强制缓存的逻辑,下面介绍协商缓存。

协商缓存

协商缓存也就是说需要和服务器确认缓存的有效性,不能自己做主了。分为两个维度

  1. 时间:判断修改时间,如果服务器的修改时间比本地存储的文件修改时间一致,本地缓存就是合法的。需要结合首部的If-Modified-Since字段进行判断。
  2. 文件:判断文件是否发生了变化,需要结合eTag和If-None-Match,如果服务端的文件eTag和本地的不一致,需要重新传输新的文件。

文件etag相比时间更高效,因为文件可能多次更改,变回之前的内容,判断文件更加高效。

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

// 执行协商缓存策略,带上协商的字段
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);
复制代码

在OkHttp中的实现和上面的逻辑一致,根据缓存的eTag或者lastModified的值,如果有,那么肯定希望可以尽量进行缓存,所以分别增加了"If-None-Match"和"If-Modified-Since"字段,并带着他们进行请求。
如果上面的字段都没有,那么就会直接构建一个请求网络的策略。

以上就是CacheStrategy#getCandidate()的全部逻辑,执行完后就判断了是否可以使用缓存的逻辑。 在执行完getCandidate还有一处判断需要注意下,如果策略的networkRequest不为空,也就是需要请求网络,但是当前的请求设置了onlyIfCached,也就是必须使用缓存,那么这时候这个请求就失败了,因为和请求冲突。

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    return new CacheStrategy(null, null);
  }
  return candidate;
}
复制代码

在获取完CacheStrategy缓存策略后,就根据内部的请求和响应字段Null和非Null来判断是否可以使用缓存。上面也说过了。

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

if (cache != null) {
  cache.trackResponse(strategy);
}

//cacheResponse为空,说明不能使用缓存
if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); 
}

// 两个都为Null,上面也遇到过,就是onlyIfCached情况下必须使用网络请求
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();
}

// networkRequest字段为空,直接使用缓存进行响应
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());
  }
}
复制代码

上面的代码逻辑上面讲到过,直接写在注释里了。 以上就是缓存的获取和可用性判断的所有逻辑。都在CacheStrategy这个策略类里面。

if (cache != null) {
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // 填充逻辑
    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.
    }
  }
}

public static boolean hasBody(Response response) {
 if (response.request().method().equals("HEAD")) {
    return false;
  }

  int responseCode = response.code();
  if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
      && responseCode != HTTP_NO_CONTENT
      && responseCode != HTTP_NOT_MODIFIED) {
    return true;
  }

  if (contentLength(response) != -1
      || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
    return true;
  }

  return false;
}

复制代码

缓存的插入增加是通过put方法实现的,在从网络获取完成请求后,如果当前有响应有body(通过hasBody判断),并且可以缓存(通过isCacheable判断)。如果都满足,那么就会插入缓存中。

  1. hasBody判断有没有body,也就是Http报文中的实体部分。如果请求方法为HEAD,肯定没有body的,因为HEAD只获取头部信息,并且状态码为204或304,并且在100到200之间都是没有body的。最后通过content相关的首部字段进行判断,必须有contentLength或者Transfer-Encoding为chunked(分快输出)。
  2. isCacheable上面分析过,主要是不能有no-cache字段。

删除通过cache的remove()移除的。

if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }
public static boolean invalidatesCache(String method) {
  return method.equals("POST")
      || method.equals("PATCH")
      || method.equals("PUT")
      || method.equals("DELETE")
      || method.equals("MOVE");     // WebDAV
}
复制代码

It is mainly judged by invalidatesCache, which means that POST/PATCH/PUT/DELETE/MOVE requests cannot be cached.

The logic of OKHttp is to store it first, then determine the request method and delete it. Can it be optimized?

change

The modification is updated through the update of the cache, mainly when the server returns 304, that is, when the data has not changed.

//执行网络请求后

if (cacheResponse != null) {
  if (networkResponse.code() == HTTP_NOT_MODIFIED (304) ) {
    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());
  }
}
复制代码

After performing a network request to obtain data, if CacheStrategythe cacheResponse is not empty, 304 is returned. This case corresponds to the case of negotiating the cache. If the data returned by the negotiation request has not changed, the local cache is still valid, and the local cache is currently updated. Including parts such as header and request/response time.

The above is all the logic of the cache center CacheInterceptor, which built a cache center by himself in the front end.

Next

In the next article, we will actually request the network to see how to request the network.

Guess you like

Origin juejin.im/post/7084839523204464648