Okhttp解析(五)缓存的处理

大家好,之前我们讲解了Okhttp网络数据请求相关的内容,这一节我们讲讲数据缓存的处理。本节按以下内容讲解Okhttp缓存相关的内容。

  1. 缓存的优势
  2. HTTP的缓存机制
  3. Okhttp的缓存启用
  4. Okhttp的读取缓存流程
  5. Okhttp的存储缓存策略
  6. Okhttp的CacheControl和缓存策略介绍

缓存的优势

缓存的使用场景很多,通过它可以将数据通过一定的规则存储起来,再次请求数据的时候就可以快速从缓存中读取了,缓存有以下优势。

  1. 减少向服务器请求的次数,减轻服务器的负载。
  2. 加快了本地的响应速度,直接从缓存中取数据比从网络读取要快很多。
  3. 提供无网模式下的浏览体验,没有网络的情况下也能显示内容。

HTTP的缓存机制

HTTP本身提供了一套缓存相关的机制。这套机制定义了相关的字段和规则,用来客户端和服务端进行缓存相关的协商,如响应的数据是否需要缓存,缓存有效期,缓存是否有效,服务器端给出指示,而客户端则根据服务端的指示做具体的缓存更新和读取缓存工作。http缓存可以分为两类:

强制缓存

强制缓存,是直接向缓存数据库请求数据,如果找到了对应的缓存数据,并且是有效的,就直接返回缓存数据。如果没有找到或失效了,则向服务器请求数据,返回数据和缓存规则,同时将数据和缓存规则保存到缓存数据库中。

对比缓存

对比缓存,是先向缓存数据库获取缓存数据的标识,然后用该标识去服务器请求该标识对应的数据是否失效,如果没有失效,服务器会返回304未失效响应,则客户端使用该标识对应的缓存。如果失效了,服务器会返回最新的数据和缓存规则,客户端使用返回的最新数据,同时将数据和缓存规则保存到缓存数据库中。

强制缓存

强制缓存,在缓存数据未失效的情况下,可以直接使用缓存数据,有两个字段Expires和Cache-Control用于标明失效规则。

Expires

表示过期时间,由服务端返回。那么下次请求数据时,判断这个Expires过期时间是否已经过了,如果还没有到过期时间,则使用缓存,如果过了过期时间,则重新请求服务器的数据。Expires格式如下:

Expires: Sat, 11 Nov 2017 10:30:01 GMT

表示到期时间是2017年11月11日10点30分,在这个时间之前可以使用缓存,过了这个时间就要重新请求服务器数据了。

不过因为服务器和客户端的时间并不是同步的,用一个绝对时间作为过期的标记并不是很明智,所以HTTP1.1之后更多的是Cache-Control,它的控制更加灵活。

Cache-Control

表示缓存的控制,有服务端返回。它有以下几个取值:

public

表示数据内容都可以被储存起来,就连有密码保护的网页也储存,安全性很低

private

表示数据内容只能被储存到私有的cache,仅对某个用户有效,不能共享

no-cache

表示可以缓存,但是只有在跟WEB服务器验证了其有效后,才能返回给客户端,触发对比缓存

no-store

表示请求和响应都禁止被缓存,强制缓存,对比缓存都不会触发

max-age

表示返回数据的过期时间

默认情况下是private,也就是不能共享的。Cache-Control格式如下:

Cache-Control:public, max-age=31536000

表示可以被公共缓存,有效时间是1年,也就是说一年时间内,请求该数据时,直接使用缓存,而不用请求服务器了。

对比缓存

对比缓存,表示需要和服务端进行相关信息的对比,由服务器决定是使用缓存还是最新内容,如果服务器判定使用缓存,返回响应吗304,判定使用最新内容,则返回响应码200和最新数据。对比缓存的判定字段有两组:

ETag和If-None-Match

ETag表示资源的一种标识信息,用于标识某个资源,由服务端返回,优先级更高。格式如下:

Etag:"AFY10-6MddXmSerSiXP1ZTiU65VS"

表示该资源的标识是AFY10-6MddXmSerSiXP1ZTiU65VS

然后客户端再次请求时,加入字段If-None-Match,格式如下:

If-None-Match:"AFY10-6MddXmSerSiXP1ZTiU65VS"

服务端收到请求的该字段时(之前的Etag值),和资源的唯一标识进行对比,如果相同,说明没有改动,则返回状态码304,如果不同,说明资源被改过了,则返回状态码200和整个内容数据。

Last-Modified和If-Modified-Since

Last-Modified表示资源的最近修改时间,由服务端返回,优先级更低。格式如下:

Last-Modified: Sat, 11 Nov 2017 10:30:01 GMT

表示上次修改时间是2017年11月11日10点30分。

If-Modified-Since: Sat, 11 Nov 2017 10:30:01 GMT

客户端请求,表示我指定的这个2017年11月11日10点30分是不是你服务器最新的修改时间。

Last-Modified
由服务器返回,表示响应的数据最近修改的时间。


If-Modified-Since
由客户端请求,表示询问服务器这个时间是不是上次修改的时间。如果服务端该资源的修改时间小于等于If-Modified-Since指定的时间,说明资源没有改动,返回响应状态码304,可以使用缓存。如果服务端该资源的修改时间大于If-Modified-Since指定的时间,说明资源又有改动了,则返回响应状态码200和最新数据给客户端,客户端使用响应返回的最新数据。

Last-Modified字段的值(服务端返回的资源上次修改时间),常常被用于客户端下次请求时的If-Modified-Since字段中。

两种缓存的区别

强制缓存的情况下,如果缓存是有效的,则直接使用缓存,而对比缓存不管缓存是否有效,都需要先去和服务器对比是否有新的数据,没有新的数据才使用缓存数据。

两种缓存的使用情景

对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行对比缓存策略。

对于对比缓存,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。

HTTP的缓存规则总结

HTTP的缓存规则是优先考虑强制缓存,然后考虑对比缓存。

  1. 首先判断强制缓存中的数据的是否在有效期内。如果在有效期,则直接使用缓存。如果过了有效期,则进入对比缓存。
  2. 在对比缓存过程中,判断ETag是否有变动,如果服务端返回没有变动,说明资源未改变,使用缓存。如果有变动,判断Last-Modified。
  3. 判断Last-Modified,如果服务端对比资源的上次修改时间没有变化,则使用缓存,否则重新请求服务端的数据,并作缓存工作。
image

Okhttp缓存相关类

Okhttp缓存相关的类有如下:

CacheControl(HTTP中的Cache-Control和Pragma缓存控制)

CacheControl是用于描述HTTP的Cache-Control和Pragma字段的类,用于指定缓存的规则。

CacheStrategy(缓存策略类)

CacheStrategy是用于判定使用缓存数据还是网络请求的决策类。

Cache(缓存类)

对外开放的缓存类,提供了缓存的增删改查接口。

InternalCache(内部缓存类)

对内使用的缓存类接口,没有具体实现,只是封装了Cache的使用。

DiskLruCache(文件化的LRU缓存类)

这是真正实现缓存功能的类,将数据存储在文件中,并使用LRU规则(由LinkedHashMap实现),控制对缓存文件的增删改查。

Okhttp缓存的启用

要开启使用Okhttp的缓存其实很简单,只需要给OkHttpClient对象设置一个Cache对象即可,创建一个Cache时指定缓存保存的目录和缓存最大的大小即可。

//新建一个cache,指定目录为外部目录下的okhttp_cache目录,大小为100M
Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/okhttp_cache/"), 100 * 1024 * 1024);
将cache设置到OkHttpClient中,这样缓存就开始生效了。
OkHttpClient client = new OkHttpClient.Builder().cache(cache).build();

那么下面我们来看看Okhttp缓存执行的大概流程

Okhttp的缓存流程

Okhttp的缓存流程分为读取缓存和存储缓存两个过程,我们分别分析。

Okhttp读取缓存流程

读取使用缓存的流程从HttpEngine的sendRequest发送请求开始。

  1. 首先获取OkHttpClient的Cache缓存对象,就是之前创建OkHttpClient时设置的Cache。
  2. 然后传入Request请求到Cache的get方法去查找缓存响应数据Response。
  3. 构造一个缓存策略,传入Request请求和缓存响应Response,然后调用它的get方法去决策使用网络请求还是缓存响应。
  4. 策略判定之后,如果是使用缓存,则它的cacheResponse不为空,networkRequest为空,如果使用请求,则相反。然后再将策略给出的这两个值,继续处理。
  5. 如果使用请求,但是之前又找到了缓存响应,则要关闭缓存响应资源。
  6. 如果策略得出缓存响应为空,网络请求也为空,则返回请求不合理的响应。(比如强制使用缓存,但是找不到缓存的情况下)
  7. 如果请求为空,缓存不为空,也就是使用缓存的情况,则使用缓存响应来构造返回的响应数据。
  8. 最后就是只使用网络请求的情况,走网络请求路线。
    总的来说就是,先查找是否有可用的Cache,然后通过Cache找到请求对应的缓存,然后将请求和缓存交给缓存策略去判断使用请求还是缓存,得出结果后,自己再判断使用缓存还是请求,如果使用缓存,用缓存构造响应直接返回,如果使用请求,那么开始网络请求流程。
public final class HttpEngine {

  //发送请求
  public void sendRequest() throws RequestException, RouteException, IOException {
    if (cacheStrategy != null) return; // Already sent.
    if (httpStream != null) throw new IllegalStateException();
    //根据用户请求得到实际的网络请求
    Request request = networkRequest(userRequest);
    //这里InternalCache就是对Cache的封装,它的实现在Cache的internalCache中。
    InternalCache responseCache = Internal.instance.internalCache(client);
    //通过Cache的get方法查找缓存响应
    Response cacheCandidate = responseCache != null
        ? responseCache.get(request)
        : null;

    long now = System.currentTimeMillis();
    //构造缓存策略,然后进行策略判断
    cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
    //策略判定后的网络请求和缓存响应
    networkRequest = cacheStrategy.networkRequest;
    cacheResponse = cacheStrategy.cacheResponse;
    
    if (responseCache != null) {
      //使用缓存响应的话,记录一下使用记录
      responseCache.trackResponse(cacheStrategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      //使用网络请求,但是之前又有缓存的话,要关闭缓存,释放资源
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      //强制使用缓存,又找不到缓存,就报不合理请求响应了
      userResponse = new Response.Builder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .build();
      return;
    }
    
    //上面情况处理之后,就是使用缓存返回,还是网络请求的情况了

    // If we don't need the network, we're done.
    if (networkRequest == null) {
      //使用缓存返回响应
      userResponse = cacheResponse.newBuilder()
          .request(userRequest)
          .priorResponse(stripBody(priorResponse))
          .cacheResponse(stripBody(cacheResponse))
          .build();
      userResponse = unzip(userResponse);
      return;
    }

    //使用网络请求
    //下面就是网络请求流程了,略
    ...
  }  
}

接下来我们分析

  1. Cache是如何获取缓存的。
  2. 缓存策略是如何判断的。

Cache获取缓存

从Cache的get方法开始。它按以下步骤进行。

  1. 计算request对应的key值,md5加密请求url得到。
  2. 根据key值去DiskLruCache查找是否存在缓存内容。
  3. 存在缓存的话,创建缓存Entry实体。ENTRY_METADATA代表响应头信息,ENTRY_BODY代表响应体信息。
  4. 然后根据缓存Entry实体得到响应,其中包含了缓存的响应头和响应体信息。
  5. 匹配这个缓存响应和请求的信息是否匹配,不匹配的话要关闭资源,匹配的话返回。
public final class Cache implements Closeable, Flushable {
  //获取缓存
  Response get(Request request) {
    //计算请求对应的key
    String key = urlToKey(request);
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      //这里从DiskLruCache中读取缓存信息
      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);
    //判断缓存响应和请求是否匹配,匹配url,method,和其他响应头信息
    if (!entry.matches(request, response)) {
      //不匹配的话,关闭响应体
      Util.closeQuietly(response.body());
      return null;
    }

    //返回缓存响应
    return response;
  }
  
  //这里md5加密url得到key值
  private static String urlToKey(Request request) {
    return Util.md5Hex(request.url().toString());
  }
  
}

如果存在缓存的话,在指定的缓存目录中,会有两个文件“****.0”和“****.1”,分别存储某个请求缓存的响应头和响应体信息。(“****”是url的md5加密值)对应的ENTRY_METADATA响应头和ENTRY_BODY响应体。缓存的读取其实是由DiskLruCache来读取的,DiskLruCache是支持Lru(最近最少访问)规则的用于磁盘存储的类,对应LruCache内存存储。它在存储的内容超过指定值之后,就会根据最近最少访问的规则,把最近最少访问的数据移除,以达到总大小不超过限制的目的。

接下来我们分析CacheStrategy缓存策略是怎么判定的。

CacheStrategy缓存策略

直接看CacheStrategy的get方法。缓存策略是由请求和缓存响应共同决定的。

  1. 如果缓存响应为空,则缓存策略为不使用缓存。
  2. 如果请求是https但是缓存响应没有握手信息,同上不使用缓存。
  3. 如果请求和缓存响应都是不可缓存的,同上不使用缓存。
  4. 如果请求是noCache,并且又包含If-Modified-Since或If-None-Match,同上不使用缓存。
  5. 然后计算请求有效时间是否符合响应的过期时间,如果响应在有效范围内,则缓存策略使用缓存。
  6. 否则创建一个新的有条件的请求,返回有条件的缓存策略。
  7. 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。
public final class CacheStrategy {

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      //网络请求和缓存响应
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        //找到缓存响应的响应头信息
        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 = HeaderParser.parseSeconds(value, -1);
          } else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
            sentRequestMillis = Long.parseLong(value);
          } else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
            receivedResponseMillis = Long.parseLong(value);
          }
        }
      }
    }

    public CacheStrategy get() {
      //获取判定的缓存策略
      CacheStrategy candidate = getCandidate();

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。
        return new CacheStrategy(null, null);
      }

      return candidate;
    }
    
    
    /** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
      // No cached response.
      //如果没有缓存响应,则返回没有缓存响应的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      //如果请求是https,而缓存响应的握手信息为空,则返回没有缓存响应的策略
      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);
      }

      //获取请求头中的CacheControl信息
      CacheControl requestCaching = request.cacheControl();
      //如果请求头中的CacheControl信息是不缓存的,则返回没有缓存响应的策略
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      //获取响应的年龄
      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信息
      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());
      }

      //构造一个新的有条件的Request,添加If-None-Match,If-Modified-Since等信息
      Request.Builder conditionalRequestBuilder = request.newBuilder();

      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }

      Request conditionalRequest = conditionalRequestBuilder.build();
      //根据是否有If-None-Match,If-Modified-Since信息,返回不同的缓存策略
      return hasConditions(conditionalRequest)
          ? new CacheStrategy(conditionalRequest, cacheResponse)
          : new CacheStrategy(conditionalRequest, null);
    }
    
    /**
     * Returns true if the request contains conditions that save the server from sending a response
     * that the client has locally. When a request is enqueued with its own conditions, the built-in
     * response cache won't be used.
     */
    private static boolean hasConditions(Request request) {
      return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
    }
}

接来下我们看看CacheControl类里有些什么。

CacheControl

public final class CacheControl {
  
  //表示这是一个优先使用网络验证,验证通过之后才可以使用缓存的缓存控制,设置了noCache
  public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();

  //表示这是一个优先先使用缓存的缓存控制,设置了onlyIfCached和maxStale的最大值
  public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();

  //以下的字段都是HTTP中Cache-Control字段相关的值
  private final boolean noCache;
  private final boolean noStore;
  private final int maxAgeSeconds;
  private final int sMaxAgeSeconds;
  private final boolean isPrivate;
  private final boolean isPublic;
  private final boolean mustRevalidate;
  private final int maxStaleSeconds;
  private final int minFreshSeconds;
  private final boolean onlyIfCached;
  private final boolean noTransform;
  
  //解析头文件中的相关字段,得到该缓存控制类
  public static CacheControl parse(Headers headers) {
    ...
  }
  
}

可以发现,它就是用于描述响应的缓存控制信息。

然后我们再看看Okhttp存储缓存是怎么进行的。

Okhttp存储缓存流程

存储缓存的流程从HttpEngine的readResponse发送请求开始的。

public final class HttpEngine {
  /**
   * Flushes the remaining request header and body, parses the HTTP response headers and starts
   * reading the HTTP response body if it exists.
   */
  public void readResponse() throws IOException {
    //读取响应,略
    ...

    // 判断响应信息中包含响应体
    if (hasBody(userResponse)) {
      // 如果缓存的话,缓存响应头信息
      maybeCache();
      //缓存响应体信息,同时zip解压缩响应数据
      userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
    }
  }
  
  // 如果缓存的话,缓存响应头信息
  private void maybeCache() throws IOException {
    InternalCache responseCache = Internal.instance.internalCache(client);
    if (responseCache == null) return;

    // Should we cache this response for this request?
    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          responseCache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
      return;
    }

    // Offer this request to the cache.
    //这里将响应头信息缓存到缓存文件中,对应缓存文件“\*\*\*\*.0”
    storeRequest = responseCache.put(stripBody(userResponse));
  }
  
  /**
   * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
   * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
   * may never exhaust the source stream and therefore not complete the cached response.
   */
  //缓存响应体信息
  private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
      throws IOException {
    // Some apps return a null body; for compatibility we treat that like a null cache request.
    if (cacheRequest == null) return response;
    Sink cacheBodyUnbuffered = cacheRequest.body();
    if (cacheBodyUnbuffered == null) return response;

    final BufferedSource source = response.body().source();
    final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

    Source cacheWritingSource = new Source() {
      boolean cacheRequestClosed;

      //这里就是从响应体体读取数据,保存到缓存文件中,对应缓存文件“\*\*\*\*.1”
      @Override public long read(Buffer sink, long byteCount) throws IOException {
        long bytesRead;
        try {
          bytesRead = source.read(sink, byteCount);
        } catch (IOException e) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheRequest.abort(); // Failed to write a complete cache response.
          }
          throw e;
        }

        if (bytesRead == -1) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true;
            cacheBody.close(); // The cache response is complete!
          }
          return -1;
        }

        sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
        cacheBody.emitCompleteSegments();
        return bytesRead;
      }

      @Override public Timeout timeout() {
        return source.timeout();
      }

      @Override public void close() throws IOException {
        if (!cacheRequestClosed
            && !discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
          cacheRequestClosed = true;
          cacheRequest.abort();
        }
        source.close();
      }
    };

    return response.newBuilder()
        .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
        .build();
  }
}

可以看到这里先通过maybeCache写入了响应头信息,再通过cacheWritingResponse写入了响应体信息。我们再进去看Cache的put方法实现。

private CacheRequest put(Response response) throws IOException {
    String requestMethod = response.request().method();

    // 响应的请求方法不支持缓存,只有GET方法支持缓存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    // 同样,请求只支持GET方法的缓存
    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 (OkHeaders.hasVaryAll(response)) {
      return null;
    }

    //开始缓存
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
}

我们继续看Cache的writeTo方法,可以看到是写入一些响应头信息。

public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url);
      sink.writeByte('\n');
      sink.writeUtf8(requestMethod);
      sink.writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size());
      sink.writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i));
        sink.writeUtf8(": ");
        sink.writeUtf8(varyHeaders.value(i));
        sink.writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString());
      sink.writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size());
      sink.writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i));
        sink.writeUtf8(": ");
        sink.writeUtf8(responseHeaders.value(i));
        sink.writeByte('\n');
      }

      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName());
        sink.writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        // The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
        if (handshake.tlsVersion() != null) {
          sink.writeUtf8(handshake.tlsVersion().javaName());
          sink.writeByte('\n');
        }
      }
      sink.close();
    }

到这里Okhttp缓存的读取和存储流程我们就清楚了。可以说,缓存的使用策略基本都是按照HTTP的缓存定义来实现的,所以对HTTP缓存相关字段的理解是很重要的。然后关于DiskLruCache是如何管理缓存文件的,这个其实也很好理解,首先的原则就是按照LRU这种最近最少使用删除的原则,当总的大小超过限定大小后,删除最近最少使用的缓存文件,它的LRU算法是使用LinkedHashMap进行维护的,这样来保证,保留的缓存文件都是更常使用的。具体实现大家可以分析DiskLruCache和LinkedHashMap的实现原理。

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

推荐阅读更多精彩内容