OkHttp源码学习之三 CacheInterceptor

在RetryAndFollowUpInterceptor之后是BridgeInterceptor。BridgeInterceptor主要是为我们的请求添加了一些头部的信息,处理GZIP压缩解压缩等,暂且略过。 然后在BridgeInterceptor的后面就是本篇的主题了—— CacheInterceptor。

BridgeInterceptor的intercept方法中调用了RealInterceptorChain的proceed方法,内部会调用CacheInterceptor的intercept方法。

先介绍一些关于响应缓存的知识点。当我们构建一个默认的OkHttpClient,默认是没有使用缓存的。OkHttpClient中的这两个成员都是为null。

final @Nullable Cache cache;
final @Nullable InternalCache internalCache;

为了缓存响应,你需要一个用于读取和写入的缓存目录,并且限制缓存的大小。缓存目录应该是私有的,不受信任的应用程序应该不可以读取该目录下的内容。

多个缓存实例同时访问同一个缓存目录是错误的。大多数应用程序应该只调用new OkHttpClient()一次,并配置好他们的缓存,然后在应用中使用唯一的OkHttpClient实例。否则的话,两个缓存实例会互相踩踏,破坏响应缓存,并可能致程序崩溃。

响应缓存使用HTTP头进行所有的配置。你可添加请求头,像Cache-Control: max-stale=3600,OkHttp的缓存会遵循它们。web服务器会在响应的头部配置响应应该被缓存多长时间,例如Cache-Control: max-age=9600。有缓存头可以强制缓存响应,强制网络响应,或者使用有条件的GET来验证网络响应。

配置OkHttpClient的缓存目录

private final OkHttpClient client;

  public CacheResponse(File cacheDirectory) throws Exception {
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(cacheDirectory, cacheSize);

    client = new OkHttpClient.Builder()
        .cache(cache)//构建OkHttpClient的时候,可以传递一个Cache实例,用来缓存响应。
        .build();
  }

  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();

    String response1Body;
    try (Response response1 = client.newCall(request).execute()) {
      if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);

      response1Body = response1.body().string();
      System.out.println("Response 1 response:          " + response1);
      System.out.println("Response 1 cache response:    " + response1.cacheResponse());
      System.out.println("Response 1 network response:  " + response1.networkResponse());
    }
    String response2Body;
    //注释1处,第二次请求的时候,就会从缓存里面取数据了。
    try (Response response2 = client.newCall(request).execute()) {
      if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);

      response2Body = response2.body().string();
      System.out.println("Response 2 response:          " + response2);
      System.out.println("Response 2 cache response:    " + response2.cacheResponse());
      System.out.println("Response 2 network response:  " + response2.networkResponse());
    }

    System.out.println("Response 2 equals Response 1? " +
     response1Body.equals(response2Body));
}

注释1处,第二次请求的时候,就会从缓存里面取数据了。

RealCall的getResponseWithInterceptorChain方法4处,添加了CacheInterceptor。

Response getResponseWithInterceptorChain() throws IOException {
    // 构建一整套拦截器
    List interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//1
    interceptors.add(retryAndFollowUpInterceptor);//2
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//3
    interceptors.add(new CacheInterceptor(client.internalCache()));//4
    interceptors.add(new ConnectInterceptor(client));//5
    //构建一个RealCall的时候我们传入的forWebSocket是false
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//6
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//7
    //构建拦截器链
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
    //拦截器链处理请求
    return chain.proceed(originalRequest);
}

OkHttpClient的internalCache方法

InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }

因为我们设置了cache,这里返回的是cache的internalCache。

okhttp3.Cache类部分代码。

public final class Cache implements Closeable, Flushable {
  private static final int VERSION = 201105;
  private static final int ENTRY_METADATA = 0;
  private static final int ENTRY_BODY = 1;
  private static final int ENTRY_COUNT = 2;

  //内部还是调用了Cache的方法。
  final InternalCache internalCache = new InternalCache() {
    @Override public Response get(Request request) throws IOException {
      return Cache.this.get(request);
    }

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

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

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

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

    @Override public void trackResponse(CacheStrategy cacheStrategy) {
      Cache.this.trackResponse(cacheStrategy);
    }
  };
  //...
}

接下来我们看CacheInterceptor是怎么工作的。

    @Override
    public Response intercept(Interceptor.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();
        //注释3处
        Request networkRequest = strategy.networkRequest;
        //如果返回的cacheResponse不为null,说明缓存是可用的。
        Response cacheResponse = strategy.cacheResponse;

        if (cache != null) {
            cache.trackResponse(strategy);
        }
        //注释4处,缓存不满足缓存策略,缓存不可用
        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }

        // 注释5处,如果禁止使用网络,并且缓存不满足,失败。
        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();
        }

        // 注释6处,缓存可用。返回缓存。
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                     //给返回的Response添加cacheResponse字段,cacheResponse字段是去掉了响应体的。
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        Response networkResponse = null;
        try {
            //注释7处,缓存不满足,交给后面的拦截器发起网络请求
            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());
            }
        }

        // 注释8处,condition get的情况If we have a cache response too, then we're doing a conditional get.
        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();
                //注释9处,更新缓存
                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)) {
                // 注释10处,添加缓存
                CacheRequest cacheRequest = cache.put(response);
                return cacheWritingResponse(cacheRequest, response);
            }

            if (HttpMethod.invalidatesCache(networkRequest.method())) {
                try {
                    //注释11处,缓存不合法了,移除缓存
                    cache.remove(networkRequest);
                } catch (IOException ignored) {
                    // The cache cannot be written.
                }
            }
        }
        //最终返回响应。
        return response;
    }

注释1处,从缓存中获取响应候选者。

注释2处,根据当前时间、请求、响应候选者构建缓存策略。

-------------------------- 跳过开始 --------------------------

CacheStrategy.Factory的构造函数。可以跳过,涉及到http一些知识,后面再复习研究。

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)) {
            //响应的过期时间,如果有设置了max age ,max age 优先。
            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.Factory的get方法。可以跳过,涉及到http一些知识,后面再复习研究。

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

可以跳过,涉及到http一些知识,后面再复习研究。

private CacheStrategy getCandidate() {
      // 没有缓存的响应
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 如果缺少所需的握手,请删除缓存的响应。
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //兜底策略,防止错误的存储了不应该被缓存的响应。
     //如果不应该存储此响应,则永远不要将其用作响应源。只要持久性存储行为良好且规则不变,该检查就应该是多余的。
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
        //存在缓存的响应且响应是不可变的
        return new CacheStrategy(null, cacheResponse);
      }
      //缓存响应的年龄
      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;
      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) {
          //添加响应不新鲜的head
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          //添加响应快要过期的head
          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);
    }

-------------------------- 跳过结束 --------------------------

继续CacheInterceptor的intercept方法

注释3处,CacheStrategy返回的networkRequest和cacheResponse。总之,返回的策略一共有四种。

  • 如果CacheStrategy的networkRequest, cacheResponse都为null,则不使用网络策略,也不使用缓存策略。
  • 如果CacheStrategy的networkRequest不为null, cacheResponse为null,使用网络策略。
  • 如果CacheStrategy的networkRequest为null, cacheResponse不为null,使用缓存策略
  • 如果CacheStrategy的networkRequest, cacheResponse都不为null,使用有条件的网络策略,如果请求条件验证通过的话,服务器只会发送响应头,用来更新缓存的响应head信息,缓存的响应的body还可以继续使用。

释4处,cacheResponse为null,说明缓存cacheCandidate不满足缓存策略,不可用。

注释5处,如果networkRequest为null,禁止发起网络请求,并且缓存不满足,失败。

注释6处,缓存可用。返回缓存。

注释7处,缓存不满足,交给后面的拦截器发起网络请求。

注释8处,condition get的情况。如果返回304,说明服务端的内容未修改,我们只需要更新本地缓存的头部信息和时间信息即可。

注释9处,更新缓存。

注释10处,添加缓存。

注释11处,缓存不合法了,移除缓存。

接下来,我们看一下okhttp3.Cache是怎么工作的。

Cache的构造函数创建了DiskLruCache对象。

final DiskLruCache cache;

public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

Cache的put方法。

    @Nullable
    CacheRequest put(Response response) {
        String requestMethod = response.request().method();
        //不合法的http方法 POST,PATCH,PUT,DELETE,MOVE,直接移除缓存。
        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 (HttpHeaders.hasVaryAll(response)) {
            return null;
        }
        //构建一个 Cache.Entry 对象
        Cache.Entry entry = new Cache.Entry(response);
        DiskLruCache.Editor editor = null;
        try {
            editor = cache.edit(key(response.request().url()));
            if (editor == null) {
                return null;
            }
            //写入缓存
            entry.writeTo(editor);
            return new Cache.CacheRequestImpl(editor);
        } catch (IOException e) {
            abortQuietly(editor);
            return null;
        }
    }

Cache的update方法。

  void update(Response cached, Response network) {
    Entry entry = new Entry(network);
    DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
    DiskLruCache.Editor editor = null;
    try {
      editor = snapshot.edit(); // Returns null if snapshot is not current.
      if (editor != null) {
        //更新缓存。
        entry.writeTo(editor);
        //commit的时候,会整理DiskLruCache的大小
        editor.commit();
      }
    } catch (IOException e) {
      abortQuietly(editor);
    }
  }

Cache的remove方法。

void remove(Request request) throws IOException {
    cache.remove(key(request.url()));
}

DiskLruCache 会在一些合适的时机,比如插入提交,更新提交,删除的时候会整理其使用的文件大小。

private final Runnable cleanupRunnable = new Runnable() {
    public void run() {
      //注意,这里加锁了。
      synchronized (DiskLruCache.this) {
        if (!initialized | closed) {
          return; // Nothing to do
        }

        try {
          //整理大小
          trimToSize();
        } catch (IOException ignored) {
          mostRecentTrimFailed = true;
        }

        try {
          if (journalRebuildRequired()) {
            rebuildJournal();
            redundantOpCount = 0;
          }
        } catch (IOException e) {
          mostRecentRebuildFailed = true;
          journalWriter = Okio.buffer(Okio.blackhole());
        }
      }
    }
  };
void trimToSize() throws IOException {
    while (size > maxSize) {
      Entry toEvict = lruEntries.values().iterator().next();
      removeEntry(toEvict);
    }
    mostRecentTrimFailed = false;
  }

就是循环移除Entry,直到满足大小限制。

总结:自己并没有去专门学习过HTTP方面的知识,所以有很多东西没法去更细的了解。等自己学习完HTTP相关的知识,在回来查漏补缺。DiskLruCache后面会单独写一篇文章。

  • okhttp源码分析(三)-CacheInterceptor过滤器
  • github-okhttp

你可能感兴趣的:(OkHttp源码学习之三 CacheInterceptor)