Okhttp源码学习三(重试和重定向,桥接,缓存拦截器的内部原理)

OkHttp 内置了 5 个拦截器,在每一个拦截器里,分别对请求信息和响应值做了处理,每一层只做当前相关的操作,这五个拦截器分别是:

RetryAndFollowUpInterceptor,BridgeInterceptor,CacheInterceptor,ConnectInterceptor,CallServerInterceptor.

他们的作用分别如下:

  1. RetryAndFollowUpInterceptor:取消、失败重试、重定向
  2. BridgeInterceptor:把用户请求转换为 HTTP 请求;把 HTTP 响应转换为用户友好的响应
  3. CacheInterceptor:读写缓存、根据策略决定是否使用
  4. ConnectInterceptor:和服务器建立连接
  5. CallServerInterceptor:实现读写数据

RetryAndFollowUpInterceptor

通过前面对OkHttp拦截器拦截过程的学习,我们知道,在请求的时候,RetryAndFollowUpInterceptor是第一个会被调用的内置拦截器,拦截器的核心逻辑就在拦截方法:

@Override
public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  Call call = realChain.call();
  EventListener eventListener = realChain.eventListener();
  //注意:新创建了一个流分配管理类对象,这个类很重要
  StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
    createAddress(request.url()), call, eventListener, callStackTrace);
  this.streamAllocation = streamAllocation;

  int followUpCount = 0;    //重定向次数
  Response priorResponse = null;
  while (true) {            //while循环
    if (canceled) {        //检查当前请求是否被取消,如果这时请求被取消了,则会通StreamAllocation释放连接
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response;
    boolean releaseConnection = true;  
    try {
      response = realChain.proceed(request, streamAllocation, null, null);
      releaseConnection = false;  //请求过程中,只要发生未处理的异常,releaseConnection 就会  为true,一旦变为true,就会将StreamAllocation释放掉
    } catch (RouteException e) {
      // The attempt to connect via a route failed. The request will not have been sent.
    if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
      throw e.getLastConnectException();
    }
      releaseConnection = false;
      continue;        //发生异常就继续循环
    } catch (IOException e) {
      // An attempt to communicate with a server failed. The request may have been sent.
      boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
      if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
      releaseConnection = false;
      continue;      //继续循环
    } finally {
    // We're throwing an unchecked exception. Release any resources.
      if (releaseConnection) {      //releaseConnection一旦为true,就释放连接
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }

  // Attach the prior response if it exists. Such responses never have a body.
    if (priorResponse != null) {   
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
        .build();
    }
   //根据 code 和 method 判断是否需要重定向请求
    Request followUp = followUpRequest(response, streamAllocation.route());

    if (followUp == null) {    //不需要重定向时直接返回结果
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }

    closeQuietly(response.body());
    //重定向次数先+1,然后判断是否大于重定向次数的最大值20
    if (++followUpCount > MAX_FOLLOW_UPS) {
      streamAllocation.release();
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    if (followUp.body() instanceof UnrepeatableRequestBody) {
      streamAllocation.release();
      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
    }
    //是否可以复用当前连接,如果不能,则释放当前连接,重新建立连接
    if (!sameConnection(response, followUp.url())) {
      streamAllocation.release();
      streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(followUp.url()), call, eventListener, callStackTrace);
      this.streamAllocation = streamAllocation;
    } else if (streamAllocation.codec() != null) {
      throw new IllegalStateException("Closing the body of " + response
          + " didn't close its backing stream. Bad interceptor?");
    }

    request = followUp;
    priorResponse = response;
 }
}

可以看到RetryAndFollowUpInterceptor的拦截操作:

  1. 新建了一个流引用分配管理类 StreamAllocation对象,也就是说,每一次请求都会新建一个StreamAllocation,因为RetryAndFollowUpInterceptor是第一个拦截器
  2. 然后在一个 while 循环中调用拦截器链的 proceed() 方法,执行下一个拦截器
  3. 拿到响应的过程中如果出现路由异常、IO 异常,就 continue 请求(即失败重试)
  4. 根据拿到的响应结果, 在followUpRequest()方法中判断是否需要重定向,如果不需要直接返回响应结果
  5. 如果需要重定向,先判断重定向次数是否超过最大值,在判断重定向请求是否可以复用当前连接,如果不可以,则要释放当前连接并新建
  6. 重新设置request,并将当前响应赋值给priorResponse ,继续while循环

那么okhttp是怎么判断需要重定向的呢,看下followUpRequest()

private Request followUpRequest(Response userResponse, Route route) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  //响应状态码
  int responseCode = userResponse.code();
  //请求method
  final String method = userResponse.request().method();
  switch (responseCode) {
  case HTTP_PROXY_AUTH:        //407代理服务器验证
    Proxy selectedProxy = route != null
        ? route.proxy()
        : client.proxy();
    if (selectedProxy.type() != Proxy.Type.HTTP) {
      throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
    }
    return client.proxyAuthenticator().authenticate(route, userResponse);

  case HTTP_UNAUTHORIZED:    //401,未验证
    return client.authenticator().authenticate(route, userResponse);

  case HTTP_PERM_REDIRECT:    //308
  case HTTP_TEMP_REDIRECT:    //307
    // "If the 307 or 308 status code is received in response to a request other than GET
    // or HEAD, the user agent MUST NOT automatically redirect the request"
    if (!method.equals("GET") && !method.equals("HEAD")) {
      return null;
    }
    // fall-through
  case HTTP_MULT_CHOICE:       //300
  case HTTP_MOVED_PERM:        //301
  case HTTP_MOVED_TEMP:        //302
  case HTTP_SEE_OTHER:         //303
    // Does the client allow redirects?
    //okHttpClient不允许重定向,返回null
    if (!client.followRedirects()) return null;

    String location = userResponse.header("Location");
    if (location == null) return null;
    //根据location拿到要重定向的url
    HttpUrl url = userResponse.request().url().resolve(location);

    // Don't follow redirects to unsupported protocols.
    if (url == null) return null;

    // If configured, don't follow redirects between SSL and non-SSL.
    boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
    //如果url不同,并且不允许ssl重定向,返回null
    if (!sameScheme && !client.followSslRedirects()) return null;

    // Most redirects don't include a request body.
    Request.Builder requestBuilder = userResponse.request().newBuilder();
    if (HttpMethod.permitsRequestBody(method)) {
      final boolean maintainBody = HttpMethod.redirectsWithBody(method);
      if (HttpMethod.redirectsToGet(method)) {
        requestBuilder.method("GET", null);
      } else {
        RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
        requestBuilder.method(method, requestBody);
      }
      if (!maintainBody) {
        requestBuilder.removeHeader("Transfer-Encoding");
        requestBuilder.removeHeader("Content-Length");
        requestBuilder.removeHeader("Content-Type");
      }
    }

    // When redirecting across hosts, drop all authentication headers. This
    // is potentially annoying to the application layer since they have no
    // way to retain them.
    if (!sameConnection(userResponse, url)) {
      requestBuilder.removeHeader("Authorization");
    }

    return requestBuilder.url(url).build();

  case HTTP_CLIENT_TIMEOUT:      //408
    // 408's are rare in practice, but some servers like HAProxy use this response code. The
    // spec says that we may repeat the request without modifications. Modern browsers also
    // repeat the request (even non-idempotent ones.)
    if (!client.retryOnConnectionFailure()) {
      // The application layer has directed us not to retry the request.
      return null;
    }

    if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
      return null;
    }
    //前一次响应也超时,就放弃重定向,返回null
    if (userResponse.priorResponse() != null
        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
      // We attempted to retry and got another timeout. Give up.
      return null;
    }

    if (retryAfter(userResponse, 0) > 0) {
      return null;
    }

    return userResponse.request();

  case HTTP_UNAVAILABLE:      //503
    //上一次响应状态也是503,返回null
    if (userResponse.priorResponse() != null
        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
      // We attempted to retry and got another timeout. Give up.
      return null;
    }
    //如果响应头Retry-After要求立即重试,就重新请求
    if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
      // specifically received an instruction to retry without delay
      return userResponse.request();
    }

    return null;

  default:
    return null;
}

}

可以看到重定向的判断依据就是是状态码和请求方法,总结一下这里的判断逻辑

  1. 状态码是407,先判断代理服务器的类型,如果不是HTTP代理就抛异常。如果是,会调用我们构造 OkHttpClient 时传入的 Authenticator,做鉴权处理操作,返回处理后的结果
  2. 状态码是401,和407一样,做认证操作,然后返回结果
  3. 状态码是308或者307重定向,且方法不是 GET 也不是 HEAD,就返回 null
  4. 状态码 是 300、301、302、303,且构造 OkHttpClient 时设置允许重定向,就从当前响应头中取出 Location 即新地址,然后构造一个新的 Request 再请求一次
  5. 状态码 是 408 超时,且上一次没有超时,就再请求一次
  6. 状态码 是 503 服务不可用,且上一次不是 503,响应头也要求立即重试,就重新请求一次

所以followUpRequest()的返回值不为null,就表示需要重定向,为null,表示不需要.

BridgeInterceptor

BridgeInterceptor主要负责对Request和Response报文进行加工,将用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应,直接看它的拦截方法:

@Override
public Response intercept(Chain chain) throws IOException {
  Request userRequest = chain.request();
  Request.Builder requestBuilder = userRequest.newBuilder();

  RequestBody body = userRequest.body();
  //以下是根据请求体,补全请求头
  if (body != null) {
    MediaType contentType = body.contentType();
    if (contentType != null) {
      requestBuilder.header("Content-Type", contentType.toString());
    }

    long contentLength = body.contentLength();
    if (contentLength != -1) {
      requestBuilder.header("Content-Length", Long.toString(contentLength));
      requestBuilder.removeHeader("Transfer-Encoding");
    } else {
      requestBuilder.header("Transfer-Encoding", "chunked");
      requestBuilder.removeHeader("Content-Length");
    }
  }

  if (userRequest.header("Host") == null) {
    requestBuilder.header("Host", hostHeader(userRequest.url(), false));
  }

  if (userRequest.header("Connection") == null) {
    requestBuilder.header("Connection", "Keep-Alive");
  }

// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
  boolean transparentGzip = false;
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }
  //加载cookie,cookieJar是在创建okhttpClient配置的,不配置就用默认的
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
    //将本地的cookie拼接成字符串,并设置给Cookie头
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }

  if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
  }

  Response networkResponse = chain.proceed(requestBuilder.build());
   //以下是对响应头的处理
  //解析并保存服务器返回的cookie,如果没有自定义cookie,不会解析
  HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

  Response.Builder responseBuilder = networkResponse.newBuilder()
      .request(userRequest);
  //如果响应内容是以gzip压缩过,就解压
  if (transparentGzip
      && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
      && HttpHeaders.hasBody(networkResponse)) {
    GzipSource responseBody = new GzipSource(networkResponse.body().source());
    //移除响应头Content-Encoding和Content-Length
    Headers strippedHeaders = networkResponse.headers().newBuilder()
        .removeAll("Content-Encoding")
        .removeAll("Content-Length")
        .build();
    //构建一个新的响应
    responseBuilder.headers(strippedHeaders);
    String contentType = networkResponse.header("Content-Type");
    responseBuilder.body(new RealResponseBody(contentType, -1L,   Okio.buffer(responseBody)));
  }
  //将新的响应返回
  return responseBuilder.build();
}

BridgeInterceptor做的主要工作是:

  1. 在发送请求前,根据body补全请求头,补全的请求头:Content-Type、Content-Length、Transfer-Encoding、Host、Connection、Cookie、Accept-Encoding、User-Agent
  2. 创建新的请求,执行下一个拦截器
  3. 如果响应内容以gzip压缩过,先解压,移除响应头Content-Encoding和Content-Length,再构建一个新的response返回
  4. 没有压缩过,直接返回response

CacheInterceptor

CacheInterceptor是根据HTTP协议的缓存机制来做缓存处理的,所以有必要先了解一下HTTP协议的缓存机制.

HTTP协议中缓存相关
缓存分类
 http请求有服务端和客户端之分。因此缓存也可以分为两个类型服务端侧和客户端侧。
服务端侧缓存
常见的服务端有Ngix和Apache。服务端缓存又分为代理服务器缓存和反向代理服务器缓存。常见  
的CDN就是服务器缓存。这个好理解,当浏览器重复访问一张图片地址时,CDN会判断这个请求 
有没有缓存,如果有的话就直接返回这个缓存的请求回复,而不再需要让请求到达真正的服务地 
址,这么做的目的是减轻服务端的运算压力。
客户端侧缓存
客户端主要指浏览器(如IE、Chrome等),当然包括我们的OKHTTPClient.客户端第一次请求网    
络时,服务器返回回复信息。如果数据正常的话,客户端缓存在本地的缓存目录。当客户端再次 
访问同一个地址时,客户端会检测本地有没有缓存,如果有缓存的话,数据是有没有过期,如果 
没有过期的话则直接运用缓存内容。
HTTP 缓存策略

在 HTTP 协议中,定义了一些与缓存相关的Header:

Cache-Control
Etag, If-None_match
LastModified, If-Modified-Since
Expired

Cache-Control

Cache-control 是HTTP协议中一个用来控制缓存的头部字段,既可以在请求头中使用,也可以在响应头中使用。它有不同的值,不同的值代表不同的缓存策略

Cache-control在请求头中使用的值有:

no-cache: 不使用缓存的数据,直接从服务器去取
only-if-cached: 表示直接获取缓存数据,若没有数据返回,则返回504(Gateway Timeout)
max-age: 表示可接受过期多久的缓存数据
max-stale:已过期的缓存在多少时间内仍可以继续使用(类似保质期),可以接受过去的对象,但是过期时间必须小于 max-stale 值
min-fresh:表示指定时间内的缓存数据仍有效,与缓存是否过期无关。如min-fresh: 60, 表示60s内的缓存数据都有效,60s之后的缓存数据将无效。

Cache-control在响应头中使用的值有:

public:可向任一方提供缓存数据
private:只向指定用户提供缓存数据
no-cache:缓存服务器不能对资源进行缓存
no-store:不缓存请求或响应的任何内容
max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;

Etag/If-None-Match
Etag:服务器响应请求时,告诉客户端当前资源在服务器的唯一标识(生成规则由服务器决定)

If-None-Match: 如果浏览器在Cache-Control:max-age=60设置的时间超时后,发现消息头中还设 
置了Etag值。然后,浏览器会再次向服务器请求数据并添加If-None-Match消息头,它的值就是之 
前Etag值。再次请求服务器时,客户端通过此字段通知服务器客户端缓存数据的唯一标识。服务    
器收到请求后发现有头部If-None-Match,则与被请求的资源的唯一标识进行对比,不同则说明源  
被改过,则响应整个内容,返回状态码是200,相同则说明资源没有被改动过,则响应状态码 
304,告知客户端可以使用缓存
LastModified, If-Modified-Since
Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的 
最后修改时间。
If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last- 
Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器 
收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间 
较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;若最后 
修改时间较旧,说明资源无需修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使 
用所保存的cache。
Expired
expires的效果等同于Cache-Control,不过它是Http 1.0的内容,它的作用是告诉浏览器缓存的过      
期时间,在此时间内浏览器不需要直接访问服务器地址直接用缓存内容就好了。 
expires最大的问题在于如果服务器时间和本地浏览器相差过大的问题。那样误差就很大。所以基 
本上用Cache-Control:max-age=多少秒的形式代替。

用一张图来表示HTTP的缓存机制就是:


HTTP缓存机制.png

概括起来就是:

  1. 首先根据 CacheControl 来判断是否使用缓存,如果使用缓存,就去判断当前缓存是否过期
  2. 如果缓存信息里有 Etag,就向服务器发送带 If-None-Match 的请求,服务器进行决策
  3. 如果没有 Etag 就看有没有 Last-Modified,有的话向服务器发送带 If-Modified-Since 的请求, 由服务器进行决策
  4. 服务器验证缓存有效性后,如果缓存仍可以使用,就返回 304;如果 code 不是 304,客户端就需要从响应里拿数据,同时更新缓存。

OkHttp的缓存拦截器实现

public final class CacheInterceptor implements Interceptor {
final InternalCache cache;

public CacheInterceptor(InternalCache cache) {
  this.cache = cache;
}

@Override
public Response intercept(Chain chain) throws IOException {
  //如果有缓存,取出缓存,cache是在OkHttpClient中设置的,如果不设置就为null
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

  long now = System.currentTimeMillis();
  //获取缓存策略对象
  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(),   cacheCandidate).get();
  //从缓存策略中拿到networkRequest ,networkRequest 不为null,表示要请求网络
  Request networkRequest = strategy.networkRequest;
 //缓存策略中拿到cacheResponse ,cacheResponse 不为null,表示缓存可用
  Response cacheResponse = strategy.cacheResponse;
  ....
}

首先是判断cache是否为null,如果不为null,就调用cache.get(chain.request())取出缓存响应,看一下这个cache:

public interface InternalCache {
  Response get(Request request) throws IOException;

  CacheRequest put(Response response) throws IOException;

  void remove(Request request) throws IOException;

  void update(Response cached, Response network);

  void trackConditionalCacheHit();

  void trackResponse(CacheStrategy cacheStrategy);
}

cache是InternalCache类型的接口,它唯一的实现在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;

//创建一个InternalCache 成员
 final InternalCache internalCache = new InternalCache() {
  @Override public Response get(Request request) throws IOException {
      //InternalCache的get方法其实调用的是Cache的get方法
      return Cache.this.get(request);
  }

  .......
 };

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

public static String key(HttpUrl url) {  //缓存的key值就是对url进行utf-8转码,并取md5
 return ByteString.encodeUtf8(url.toString()).md5().hex();
}

@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类可以看到OkHttp的缓存是用DiskLruCache 来存储的,存储的key就是请求的url的md5值.

继续回到CacheInterceptor的拦截方法,在拦截方法中:

//构建一个缓存策略对象
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(),     
cacheCandidate).get();
  //从缓存策略对象中拿到networkRequest 
Request networkRequest = strategy.networkRequest;
 //缓存策略对象中拿到cacheResponse 
Response cacheResponse = strategy.cacheResponse;

调用了CacheStrategyFactory()get()

public final class CacheStrategy {

  public final @Nullable Request networkRequest;
  public final @Nullable Response cacheResponse;

  CacheStrategy(Request networkRequest, Response cacheResponse) {
    this.networkRequest = networkRequest;
    this.cacheResponse = cacheResponse;
  }

  .......
  public static class Factory {
    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();
        //获取缓存响应的header值
        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);
        }
      }
    }
  }

  public CacheStrategy get() {
    //获取当前的缓存策略
    CacheStrategy candidate = getCandidate();
    //如果是网络请求不为null并且请求里面的cacheControl是只用缓存
    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() {
    // No cached response.
    if (cacheResponse == null) {   //如果没有缓存响应,返回一个没有响应的策略
        return new CacheStrategy(request, null);
    }

  // Drop the cached response if it's missing a required handshake.
    if (request.isHttps() && cacheResponse.handshake() == null) {  //如果是https请求,并且握手丢失
        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)) {  //如果响应不能缓存(根据响应状态码,请求或响应头的Cache-control字段判断)
      return new CacheStrategy(request, null);
    }

    CacheControl requestCaching = request.cacheControl();
    //请求cacheControl设置为不缓存,或者请求头里面有If-Modified-Since或If-None-Match头部字段
    if (requestCaching.noCache() || hasConditions(request)) {  
        return new CacheStrategy(request, null);
    }

    CacheControl responseCaching = cacheResponse.cacheControl();
    //如果cache-Control的值是immutable
    if (responseCaching.immutable()) {
      return new CacheStrategy(null, cacheResponse);
    }
     //获取缓存响应的年龄
    long ageMillis = cacheResponseAge();
    //获取缓存响应的过期时间
    long freshMillis = computeFreshnessLifetime();
    //如果请求cache-control里面有过期时间max-age,则比较请求和响应的过期时间,取最小值
    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) {
      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);
  }
  .......
}

可以看到CacheStrategy内部的networkRequestcacheResponse是通过HTTP协议缓存机制,也就是根据用户对当前请求设置的 CacheControl 的值,缓存响应的时间、ETag 、 LastModified 或者 ServedDate 等 Header的值 进行不断的判断得出来的.拿到两个值之后,缓存拦截器CacheInterceptor 就根据这两个值的情况决定是请求网络还是直接返回缓存数据

@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;
    //缓存是否不为null(okhttpClient.builder可以设置,不设置默认就为null)
    if (cache != null) {
      cache.trackResponse(strategy);
    }
   //有缓存,但是缓存不可用,
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }

    //networkRequest和cacheResponse都为null,表示禁止使用网络请求,但是缓存又不可用,返回504错误
    // 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();
    }

    //networkRequest为null,cacheResponse不为null,表示不使用网络请求,并且缓存有效,直接返回缓存
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //networkRequest 不为null,就走到这里(networkRequest 不为null,表示需要请求网络)
      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());
      }
     }

    // If we have a cache response too, then we're doing a conditional get.
    //networkRequest不为null,cacheResponse也不为null
    if (cacheResponse != null) {
      //如果请求网络响应码为304(缓存还可以用)
      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());
      }
    }
    //networkRequest不为null,cacheResponse为null,
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    //有设置缓存目录
    if (cache != null) {
       //有响应体,并且能够缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response,     networkRequest)) {
        // Offer this request to the cache.
        //将从网络请求到的响应存到本地缓存目录
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //判断缓存是否可用(只有 GET 请求的响应能被缓存,否则就移除缓存)
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        cache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
  }

  return response;
}

CacheInterceptor的工作原理总结:

  1. 查看本地是否有缓存,本地缓存是通过DiskLruCache 来存储的,存储的目录是在OkHttpClient的builder.cache(Cache)中设置的,存储的key为请求URL的MD5值
  2. 根据HTTP的缓存机制,也就是根据之前缓存的结果与当前将要发送Request的header值,得出是进行请求还是使用缓存,并用networkRequestcacheResponse标识,同时根据这个两个值创建缓存策略对象CacheStrategy
  3. 根据缓存策略对象CacheStrategynetworkRequestcacheResponse标识,判断是请求还是使用缓存响应
  4. 如果当前请求是请求网络数据,并且没有缓存,在请求到数据后,存到本地缓存目录

所以如果要使用OkHttp自带的缓存,就需要后端人员配合,或者是自己使用网络拦截器在请求到数据后,自己添加跟缓存相关的响应Header。

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

推荐阅读更多精彩内容