一、前言


看一下 RealCall 中的拦截器排序:

  Response getResponseWithInterceptorChain() throws IOException {    // Build a full stack of interceptors.    List interceptors = new ArrayList<>();    interceptors.addAll(client.interceptors());  //自定义的应用拦截器,是一开始添加的所以可以截断请求和一些初始化拦截    interceptors.add(retryAndFollowUpInterceptor);    interceptors.add(new BridgeInterceptor(client.cookieJar()));    interceptors.add(new CacheInterceptor(client.internalCache()));    interceptors.add(new ConnectInterceptor(client));    if (!forWebSocket) {      interceptors.addAll(client.networkInterceptors());  //自定义网络拦截器,在真正请求前调用,可以拦截改变一切默认的参数。    }    interceptors.add(new CallServerInterceptor(forWebSocket));  //真正请求的拦截器    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,        originalRequest, this, eventListener, client.connectTimeoutMillis(),        client.readTimeoutMillis(), client.writeTimeoutMillis());    return chain.proceed(originalRequest);  }

okhttp 中拦截器自定义拦截类型有两种:

Application Interceptor(应用拦截器):我们可以自定义设置 Okhttp 的拦截器之一。一次网络请求它只会执行一次拦截,而且它是第一个触发拦截的,这里拦截到的 url 请求的信息都是最原始的信息。所以我们可以在该拦截器中添加一些我们请求中需要的通用信息,打印一些我们需要的日志。当然我们可以定义多个这样的拦截器,一个处理 header 信息,一个处理接口请求的加解密 。

Netwrok Interceptor(网络拦截器):NetwrokInterceptor 也是我们可以自定义的拦截器之一。它位于倒数第二层,会经过 RetryAndFollowIntercptor 进行重定向并且也会通过 BridgeInterceptor 进行 request 请求头和响应 resposne 的处理,因此这里可以得到的是更多的信息。在打印结果可以看到它内部重定向操作和失败重试,这里会有比 Application Interceptor 更多的日志。

关于二者的区别可以看 Android OkHttp 官方 Wiki 之 Interceptors 拦截器。

自定义拦截器可以看 OkHttp 拦截器的一些骚操作。

 

二、拦截器


下面我们看看 Okhttp 中默认的 5 个拦截器。

2.1、RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 拦截器主要负责失败重连以及重定向,我们看下代码:

/** * This interceptor recovers from failures and follows redirects as necessary. It may throw an * {@link IOException} if the call was canceled. */public final class RetryAndFollowUpInterceptor implements Interceptor {    /**     * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,     * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.     */    //最大恢复追逐次数:    private static final int MAX_FOLLOW_UPS = 20;    public RetryAndFollowUpInterceptor(OkHttpClient client, boolean forWebSocket) {        this.client = client;        this.forWebSocket = forWebSocket;    }    @Override    public Response intercept(Chain chain) throws IOException {        Request request = chain.request();        // 三个参数分别对应:(1)全局的连接池,(2)连接线路Address, (3)堆栈对象        streamAllocation = new StreamAllocation(                client.connectionPool(), createAddress(request.url()), callStackTrace);        int followUpCount = 0;        Response priorResponse = null;        while (true) {            if (canceled) {                streamAllocation.release();                throw new IOException("Canceled");            }            Response response = null;            boolean releaseConnection = true;            try {                // 执行下一个拦截器,即BridgeInterceptor                // 这里有个很重的信息,即会将初始化好的连接对象传递给下一个拦截器,也是贯穿整个请求的连击对象,                // 上面我们说过,在拦截器执行过程中,RealInterceptorChain的几个属性字段会一步一步赋值                response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);                releaseConnection = false;            } catch (RouteException e) {                // The attempt to connect via a route failed. The request will not have been sent.                //  如果有异常,判断是否要恢复                if (!recover(e.getLastConnectException(), 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, requestSendStarted, request)) throw e;                releaseConnection = false;                continue;            } finally {                // We're throwing an unchecked exception. Release any resources.                if (releaseConnection) {                    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();            }            // 检查是否符合要求            Request followUp = followUpRequest(response);            if (followUp == null) {                if (!forWebSocket) {                    streamAllocation.release();                }                // 返回结果                return response;            }            //不符合,关闭响应流            closeQuietly(response.body());            // 是否超过最大限制            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()), callStackTrace);            } 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;        }    }

我们知道每个拦截器都实现了 interceptor 接口,interceptor.intercept() 方法就是子类用来处理自己的业务逻辑,所以我们仅仅需要分析这个方法即可。看源码我们得出了如下流程:

1 、根据 url 创建一个 Address 对象,初始化一个 Socket 连接对象,基于 Okio。

  private Address createAddress(HttpUrl url) {    SSLSocketFactory sslSocketFactory = null;    HostnameVerifier hostnameVerifier = null;    CertificatePinner certificatePinner = null;    if (url.isHttps()) {      sslSocketFactory = client.sslSocketFactory();      hostnameVerifier = client.hostnameVerifier();      certificatePinner = client.certificatePinner();    }    return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),        sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),        client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());  }

2、用前面创建的 address 作为参数去实例化 StreamAllocation,PS:此处还没有真正的去建立连接,只是初始化一个连接对象。

3、开启一个 while(true) 循环。

4、如果取消,释放资源并抛出异常,结束流程。

5、执行下一个拦截器,一般是 BridgeInterceptor。

6、如果发生异常,走到 catch 里面,判断是否继续请求,不继续请求则退出。

7、如果 priorResponse 不为空,则说明前面已经获取到了响应,这里会结合当前获取的 Response 和先前的 Response。

8、调用 followUpRequest 查看响应是否需要重定向,如果不需要重定向则返回当前请求。

9、重定向次数+1,同时判断是否达到最大限制数量。是:退出。

10、检查是否有相同的链接,是:释放,重建创建。

11、重新设置 request,并把当前的 Response 保存到 priorResponse,继续 while 循环。

我们来看下重定向的判断 followUpRequest:

/**   * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will   * either add authentication headers, follow redirects or handle a client request timeout. If a   * follow-up is either unnecessary or not applicable, this returns null.   */  private Request followUpRequest(Response userResponse) throws IOException {    if (userResponse == null) throw new IllegalStateException();    Connection connection = streamAllocation.connection();    Route route = connection != null        ? connection.route()        : null;    int responseCode = userResponse.code();    final String method = userResponse.request().method();    switch (responseCode) {      case HTTP_PROXY_AUTH:        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:        return client.authenticator().authenticate(route, userResponse);      case HTTP_PERM_REDIRECT:      case HTTP_TEMP_REDIRECT:        // "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:      case HTTP_MOVED_PERM:      case HTTP_MOVED_TEMP:      case HTTP_SEE_OTHER:        // Does the client allow redirects?        if (!client.followRedirects()) return null;        String location = userResponse.header("Location");        if (location == null) return null;        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());        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'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 (userResponse.request().body() instanceof UnrepeatableRequestBody) {          return null;        }        return userResponse.request();      default:        return null;    }  }

这里主要是根据 响应码(code) 和 响应头(header) 查看是否需要重定向,并重新设置请求。当然,如果是正常响应则直接返回 Response 停止循环。

/**   * Report and attempt to recover from a failure to communicate with a server. Returns true if   * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only   * be recovered if the body is buffered or if the failure occurred before the request has been   * sent.   */  private boolean recover(IOException e, boolean requestSendStarted, Request userRequest) {    streamAllocation.streamFailed(e);     // 1. 应用层配置不在连接,默认为true    // The application layer has forbidden retries.    if (!client.retryOnConnectionFailure()) return false;     // 2. 请求Request出错不能继续使用    // We can't send the request body again.    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;    //  是否可以恢复的    // This exception is fatal.    if (!isRecoverable(e, requestSendStarted)) return false;    // 4. 没用更多线路可供选择    // No more routes to attempt.    if (!streamAllocation.hasMoreRoutes()) return false;    // For failure recovery, use the same route selector with a new connection.    return true;  }  private boolean isRecoverable(IOException e, boolean requestSendStarted) {    // If there was a protocol problem, don't recover.    if (e instanceof ProtocolException) {      return false;    }    // If there was an interruption don't recover, but if there was a timeout connecting to a route    // we should try the next route (if there is one).    if (e instanceof InterruptedIOException) {      return e instanceof SocketTimeoutException && !requestSendStarted;    }    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying    // again with a different route.    if (e instanceof SSLHandshakeException) {      // If the problem was a CertificateException from the X509TrustManager,      // do not retry.      if (e.getCause() instanceof CertificateException) {        return false;      }    }    if (e instanceof SSLPeerUnverifiedException) {      // e.g. a certificate pinning error.      return false;    }    // An example of one we might want to retry with a different route is a problem connecting to a    // proxy and would manifest as a standard IOException. Unless it is one we know we should not    // retry, we return true and try a new route.    return true;  }

看上面代码可以这样理解:判断是否可以恢复,如果下面几种条件符合,则返回 true,代表可以恢复,如果返回 false,代表不可恢复。

  1. 应用层配置不在连接(默认为 true ),则不可恢复
  2. 请求 Request 是不可重复使用的 Request,则不可恢复
  3. 根据 Exception 的类型判断是否可以恢复的 ( isRecoverable() 方法)
    3.1、如果是协议错误(ProtocolException)则不可恢复
    3.2、如果是中断异常(InterruptedIOException)则不可恢复
    3.3、如果是 SSL 握手错误(SSLHandshakeException && CertificateException)则不可恢复
    3.4、certificate pinning错误(SSLPeerUnverifiedException)则不可恢复
  4. 没用更多线路可供选择 则不可恢复
    如果上述条件都不满足,则这个 request 可以恢复

综上所述:一个循环来不停的获取 response。每循环一次都会获取下一个 request,如果没有,则返回 response,退出循环。而获取下一个 request 的逻辑,是根据上一个 response 返回的状态码,分别作处理。

 

2.2、BridgeInterceptor

负责对Request和Response报文进行加工具体如下:

  1. 请求从应用层数据类型类型转化为网络调用层的数据类型。
  2. 将网络层返回的数据类型 转化为 应用层数据类型。
  3. 补充:Keep-Alive 连接:

区别如下图:

Android OkHttp 源码解析 - 拦截器_第1张图片

我们看下它的 intercept() 方法 :

Android OkHttp 源码解析 - 拦截器_第2张图片

Android OkHttp 源码解析 - 拦截器_第3张图片

读了源码发现这个 interceptor 比较简单,可以分为发送请求和响应两个阶段来看:

1、在发送阶段 BridgeInterceptor 补全了一些 header 包括 Content-TypeContent-LengthTransfer-EncodingHostConnectionAccept-EncodingUser-Agent

2、如果需要 gzip 压缩则进行 gzip 压缩。

3、加载 Cookie。

4、随后创建新的 request 并交付给后续的 interceptor 来处理,以获取响应。

5、首先保存 Cookie。

6、如果服务器返回的响应 content 是以 gzip 压缩过的,则会先进行解压缩,移除响应中的 header Content-Encoding和Content-Length,构造新的响应返回。

7、否则直接返回 response。

其中* CookieJar来自 OkHttpClient*,他是OKHttp的Cookie管理类,负责Cookie的存取。

public interface CookieJar {  /** A cookie jar that never accepts any cookies. */  CookieJar NO_COOKIES = new CookieJar() {    @Override public void saveFromResponse(HttpUrl url, List cookies) {    }    @Override public List loadForRequest(HttpUrl url) {      return Collections.emptyList();    }  };  /**   * Saves {@code cookies} from an HTTP response to this store according to this jar's policy.   *   * 

Note that this method may be called a second time for a single HTTP response if the response * includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's * cookies. */ void saveFromResponse(HttpUrl url, List cookies); /** * Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly * empty list of cookies for the network request. * *

Simple implementations will return the accepted cookies that have not yet expired and that * {@linkplain Cookie#matches match} {@code url}. */ List loadForRequest(HttpUrl url);}

由于 OKHttpClient 默认的构造过程可以看到,OKHttp 默认是没有提供 Cookie 管理功能的,所以如果想增加 Cookie 管理需要重写里面的方法,PS:如果重写 CookieJar() 需要注意 loadForRequest() 方法的返回值不能为 null。

  public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {      // 没有配置,不解析    if (cookieJar == CookieJar.NO_COOKIES) return;    // 此处遍历,解析Set-Cookie的值,比如max-age    List cookies = Cookie.parseAll(url, headers);    if (cookies.isEmpty()) return;     // 然后保存,即自定义    cookieJar.saveFromResponse(url, cookies);  }

 

2.3、CacheInterceptor

CacheInterceptor 负责将请求和返回关联的保存到缓存中。客户端和服务器根据一定的机制(策略CacheStrategy ),在需要的时候使用缓存的数据作为网络响应,节省了时间和宽带。

老规矩上源码:

@Override public Response intercept(Chain chain) throws IOException {    //1、如果配置了缓存,则从缓存中取出(可能为null)    Response cacheCandidate = cache != null        ? cache.get(chain.request())        : null;    long now = System.currentTimeMillis();    //2、获取缓存的策略.    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();    Request networkRequest = strategy.networkRequest;    Response cacheResponse = strategy.cacheResponse;    //3、监测缓存    if (cache != null) {      cache.trackResponse(strategy);    }    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.      //4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回    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();    }    //5、如果缓存有效,使用网络,不使用网络    // If we don't need the network, we're done.    if (networkRequest == null) {      return cacheResponse.newBuilder()          .cacheResponse(stripBody(cacheResponse))          .build();    }    Response networkResponse = null;    try {     //6、如果缓存无效,执行下一个拦截器      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());      }    }    //7、本地有缓存、根据条件判断是使用缓存还是使用网络的response    // If we have a cache response too, then we're doing a conditional get.    if (cacheResponse != null) {      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 response = networkResponse.newBuilder()        .cacheResponse(stripBody(cacheResponse))        .networkResponse(stripBody(networkResponse))        .build();    //8、把response缓存到本地    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);      }      if (HttpMethod.invalidatesCache(networkRequest.method())) {        try {          cache.remove(networkRequest);        } catch (IOException ignored) {          // The cache cannot be written.        }      }    }    return response;  }

简单的说下上述流程:

1、如果配置了缓存,则从缓存中取出(可能为null)。
2、获取缓存的策略。
3、监测缓存。
4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回。
5、如果缓存有效,使用网络,不使用网络。
6、如果缓存无效,执行下一个拦截器。
7、本地有缓存、根据条件判断是使用缓存还是使用网络的response。
8、把response缓存到本地。

大体流程分析完,那么咱们再详细分析下。

1、原理
(1)、okhttp 的网络缓存是基于 http 协议。
(2)、使用 DiskLruCache 的缓存策略。
2、注意事项:
(1)、目前只支持 GET,其他请求方式需要自己实现。
(2)、需要服务器配合,通过 head 设置相关头来控制缓存。
(3)、创建 OkHttpClient 时候需要配置 Cache。

缓存实际上是一个比较复杂的逻辑,单独的功能块,实际上不属于 OKhttp 上的功能,实际上是通过是 http 协议和 DiskLruCache 做了处理。

 

2.4、ConnectInterceptor

顾名思义连接拦截器,这才是真行的开始向服务器发起器连接。看下这个类的代码:

/** Opens a connection to the target server and proceeds to the next interceptor. */public final class ConnectInterceptor implements Interceptor {  public final OkHttpClient client;  public ConnectInterceptor(OkHttpClient client) {    this.client = client;  }  @Override public Response intercept(Chain chain) throws IOException {    RealInterceptorChain realChain = (RealInterceptorChain) chain;    Request request = realChain.request();    StreamAllocation streamAllocation = realChain.streamAllocation();    // We need the network to satisfy this request. Possibly for validating a conditional GET.    boolean doExtensiveHealthChecks = !request.method().equals("GET");    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);    RealConnection connection = streamAllocation.connection();    return realChain.proceed(request, streamAllocation, httpCodec, connection);  }}

主要看下 ConnectInterceptor() 方法,里面代码已经很简单了,受限了通过 streamAllocation 的 newStream 方法获取一个流(HttpCodec 是个接口,根据协议的不同,由具体的子类的去实现),第二步就是获取对应的 RealConnection。StreamAllocation 的 newStream() 内部其实是通过 findHealthyConnection() 方法获取一个 RealConnection,而在 findHealthyConnection() 里面通过一个 while(true) 死循环不断去调用 findConnection() 方法去找 RealConnection。而在 findConnection() 里面其实是真正的寻找 RealConnection,而上面提到的 findHealthyConnection() 里面主要就是调用 findConnection() 然后去验证是否是"健康"的。在 findConnection() 里面主要是通过3重判断:

1、如果有已知连接且可用,则直接返回。

2、如果在连接池有对应address的连接,则返回。

3、切换路由再在连接池里面找下,如果有则返回。

如果上述三个条件都没有满足,则直接new一个RealConnection。然后开始握手,握手结束后,把连接加入连接池,如果在连接池有重复连接,和合并连接。至此 findHealthyConnection() 就分析完毕,给大家看下大缩减后的代码:

  //StreamAllocation.java  public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {    // 省略代码       RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);      HttpCodec resultCodec = resultConnection.newCodec(client, this);    // 省略代码   }  private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)      throws IOException {    while (true) {      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,          connectionRetryEnabled);      synchronized (connectionPool) {        if (candidate.successCount == 0) {          return candidate;        }      }      if (!candidate.isHealthy(doExtensiveHealthChecks)) {        noNewStreams();        continue;      }      return candidate;    }  }  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,      boolean connectionRetryEnabled) throws IOException {      //省略部分代码      //条件1如果有已知连接且可用,则直接返回      RealConnection allocatedConnection = this.connection;      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {        return allocatedConnection;      }      //条件2 如果在连接池有对应address的连接,则返回      Internal.instance.get(connectionPool, address, this, null);      if (connection != null) {        return connection;      }      selectedRoute = route;    }    // 条件3切换路由再在连接池里面找下,如果有则返回    if (selectedRoute == null) {      selectedRoute = routeSelector.next();    }    RealConnection result;    synchronized (connectionPool) {      if (canceled) throw new IOException("Canceled");      Internal.instance.get(connectionPool, address, this, selectedRoute);      if (connection != null) return connection;            route = selectedRoute;      refusedStreamCount = 0;      //以上条件都不满足则new一个      result = new RealConnection(connectionPool, selectedRoute);      acquire(result);    }    // 开始握手    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);    //计入数据库    routeDatabase().connected(result.route());    Socket socket = null;    synchronized (connectionPool) {      //加入连接池      Internal.instance.put(connectionPool, result);      // 如果是多路复用,则合并      if (result.isMultiplexed()) {        socket = Internal.instance.deduplicate(connectionPool, address, this);        result = connection;      }    }    closeQuietly(socket);    return result;  }

这里再简单的说下 RealConnection 的 connect(),因为这个方法也很重要。不过大家要注意 RealConnection 的 connect() 是 StreamAllocation 调用的。在 RealConnection 的 connect() 的方法里面也是一个 while(true) 的循环,里面判断是隧道连接还是普通连接,如果是隧道连接就走 connectTunnel(),如果是普通连接则走 connectSocket(),最后建立协议。connectSocket() 方法里面就是通过 okio 获取 source 与 sink。establishProtocol() 方法建立连接咱们说下,里面判断是是 HTTP/1.1 还是 HTTP/2.0。如果是 HTTP/2.0 则通过 Builder 来创建一个 Http2Connection 对象,并且调用 Http2Connection 对象的 start() 方法。所以判断一个 RealConnection 是否是 HTTP/2.0 其实很简单,判断 RealConnection 对象的 http2Connection 属性是否为 null 即可,因为只有 HTTP/2 的时候 http2Connection才会被赋值。

 

2.5、CallServerInterceptor

上面我们已经成功连接到服务器了,那接下来要做什么那?相信你已经猜到了, 那就是发送数据了。在 OkHttp 里面读取数据主要是通过以下四个步骤来实现的:

  1. 写入请求头
  2. 写入请求体
  3. 读取响应头
  4. 读取响应体

OkHttp 的流程是完全独立的。同样读写数据是交给相关的类来处理,就是 HttpCodec(解码器)来处理。

  @Override public Response intercept(Chain chain) throws IOException {    RealInterceptorChain realChain = (RealInterceptorChain) chain;    HttpCodec httpCodec = realChain.httpStream();    StreamAllocation streamAllocation = realChain.streamAllocation();    RealConnection connection = (RealConnection) realChain.connection();    Request request = realChain.request();        long sentRequestMillis = System.currentTimeMillis();    //写入请求头    httpCodec.writeRequestHeaders(request);    Response.Builder responseBuilder = null;    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100      // Continue" response before transmitting the request body. If we don't get that, return what      // we did get (such as a 4xx response) without ever transmitting the request body.      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {        httpCodec.flushRequest();        responseBuilder = httpCodec.readResponseHeaders(true);      }     //写入请求体      if (responseBuilder == null) {        // Write the request body if the "Expect: 100-continue" expectation was met.        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);        request.body().writeTo(bufferedRequestBody);        bufferedRequestBody.close();      } else if (!connection.isMultiplexed()) {        // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from        // being reused. Otherwise we're still obligated to transmit the request body to leave the        // connection in a consistent state.        streamAllocation.noNewStreams();      }    }    httpCodec.finishRequest();    //读取响应头    if (responseBuilder == null) {      responseBuilder = httpCodec.readResponseHeaders(false);    }    Response response = responseBuilder        .request(request)        .handshake(streamAllocation.connection().handshake())        .sentRequestAtMillis(sentRequestMillis)        .receivedResponseAtMillis(System.currentTimeMillis())        .build();    //读取响应体    int code = response.code();    if (forWebSocket && code == 101) {      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.      response = response.newBuilder()          .body(Util.EMPTY_RESPONSE)          .build();    } else {      response = response.newBuilder()          .body(httpCodec.openResponseBody(response))          .build();    }    if ("close".equalsIgnoreCase(response.request().header("Connection"))        || "close".equalsIgnoreCase(response.header("Connection"))) {      streamAllocation.noNewStreams();    }    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {      throw new ProtocolException(          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());    }    return response;  }

 

自此整个流程已经结束了。

 

三、连接机制


连接的创建是在 StreamAllocation 对象统筹下完成的,我们前面也说过它早在 RetryAndFollowUpInterceptor 就被创建了,StreamAllocation 对象主要用来管理两个关键角色:

  • RealConnection:真正建立连接的对象,利用 Socket 建立连接。
  • ConnectionPool:连接池,用来管理和复用连接。

在里初始化了一个 StreamAllocation 对象,我们说在这个 StreamAllocation 对象里初始化了一个 Socket 对象用来做连接,但是并没有。

3.1 创建连接

我们在前面的 ConnectInterceptor 分析中已经说过,ConnectInterceptor 用来完成连接。而真正的连接在 RealConnect 中实现,连接由连接池 ConnectPool 来管理,连接池最多保持 5 个地址的连接 keep-alive,每个 keep-alive 时长为 5 分钟,并有异步线程清理无效的连接。

主要由以下两个方法完成:

  1. HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
  2. RealConnection connection = streamAllocation.connection();

我们来具体的看一看。

StreamAllocation.newStream() 最终调动 findConnect() 方法来建立连接。

public final class StreamAllocation {          /**       * Returns a connection to host a new stream. This prefers the existing connection if it exists,       * then the pool, finally building a new connection.       */      private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,          boolean connectionRetryEnabled) throws IOException {        Route selectedRoute;        synchronized (connectionPool) {          if (released) throw new IllegalStateException("released");          if (codec != null) throw new IllegalStateException("codec != null");          if (canceled) throw new IOException("Canceled");              //1 查看是否有完好的连接          RealConnection allocatedConnection = this.connection;          if (allocatedConnection != null && !allocatedConnection.noNewStreams) {            return allocatedConnection;          }              //2 连接池中是否用可用的连接,有则使用          Internal.instance.get(connectionPool, address, this, null);          if (connection != null) {            return connection;          }              selectedRoute = route;        }            //线程的选择,多IP操作        if (selectedRoute == null) {          selectedRoute = routeSelector.next();        }            //3 如果没有可用连接,则自己创建一个        RealConnection result;        synchronized (connectionPool) {          if (canceled) throw new IOException("Canceled");              // Now that we have an IP address, make another attempt at getting a connection from the pool.          // This could match due to connection coalescing.          Internal.instance.get(connectionPool, address, this, selectedRoute);          if (connection != null) {            route = selectedRoute;            return connection;          }              // Create a connection and assign it to this allocation immediately. This makes it possible          // for an asynchronous cancel() to interrupt the handshake we're about to do.          route = selectedRoute;          refusedStreamCount = 0;          result = new RealConnection(connectionPool, selectedRoute);          acquire(result);        }            //4 开始TCP以及TLS握手操作        result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);        routeDatabase().connected(result.route());            //5 将新创建的连接,放在连接池中        Socket socket = null;        synchronized (connectionPool) {          // Pool the connection.          Internal.instance.put(connectionPool, result);              // If another multiplexed connection to the same address was created concurrently, then          // release this connection and acquire that one.          if (result.isMultiplexed()) {            socket = Internal.instance.deduplicate(connectionPool, address, this);            result = connection;          }        }        closeQuietly(socket);            return result;      }    }

整个流程如下:

1、查找是否有完整的连接可用:

  • Socket没有关闭
  • 输入流没有关闭
  • 输出流没有关闭
  • Http2连接没有关闭

2、连接池中是否有可用的连接,如果有则可用。

3、如果没有可用连接,则自己创建一个。

4、开始TCP连接以及TLS握手操作。

5、将新创建的连接加入连接池。

上述方法完成后会创建一个 RealConnection 对象,然后调用该方法的 connect() 方法建立连接,我们再来看看 RealConnection.connect() 方法的实现。

public final class RealConnection extends Http2Connection.Listener implements Connection {        public void connect(         int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {       if (protocol != null) throw new IllegalStateException("already connected");          //线路选择       RouteException routeException = null;       List connectionSpecs = route.address().connectionSpecs();       ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);          if (route.address().sslSocketFactory() == null) {         if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {           throw new RouteException(new UnknownServiceException(               "CLEARTEXT communication not enabled for client"));         }         String host = route.address().url().host();         if (!Platform.get().isCleartextTrafficPermitted(host)) {           throw new RouteException(new UnknownServiceException(               "CLEARTEXT communication to " + host + " not permitted by network security policy"));         }       }              //开始连接       while (true) {         try {            //如果是通道模式,则建立通道连接           if (route.requiresTunnel()) {             connectTunnel(connectTimeout, readTimeout, writeTimeout);           }            //否则进行Socket连接,一般都是属于这种情况           else {             connectSocket(connectTimeout, readTimeout);           }           //建立https连接           establishProtocol(connectionSpecSelector);           break;         } catch (IOException e) {           closeQuietly(socket);           closeQuietly(rawSocket);           socket = null;           rawSocket = null;           source = null;           sink = null;           handshake = null;           protocol = null;           http2Connection = null;              if (routeException == null) {             routeException = new RouteException(e);           } else {             routeException.addConnectException(e);           }              if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {             throw routeException;           }         }       }          if (http2Connection != null) {         synchronized (connectionPool) {           allocationLimit = http2Connection.maxConcurrentStreams();         }       }     }    /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */      private void connectSocket(int connectTimeout, int readTimeout) throws IOException {        Proxy proxy = route.proxy();        Address address = route.address();            //根据代理类型的不同处理Socket        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP            ? address.socketFactory().createSocket()            : new Socket(proxy);            rawSocket.setSoTimeout(readTimeout);        try {          //建立Socket连接          Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);        } catch (ConnectException e) {          ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());          ce.initCause(e);          throw ce;        }            // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0        // More details:        // https://github.com/square/okhttp/issues/3245        // https://android-review.googlesource.com/#/c/271775/        try {          //获取输入/输出流          source = Okio.buffer(Okio.source(rawSocket));          sink = Okio.buffer(Okio.sink(rawSocket));        } catch (NullPointerException npe) {          if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {            throw new IOException(npe);          }        }      }}

最终调用 Java 里的套接字 Socket 里的 connect() 方法。

3.2 连接池

我们知道在复杂的网络环境下,频繁的进行建立 Sokcet 连接(TCP 三次握手)和断开 Socket(TCP 四次分手)是非常消耗网络资源和浪费时间的,HTTP 中的 keepalive 连接对于降低延迟和提升速度有非常重要的作用。

复用连接就需要对连接进行管理,这里就引入了连接池的概念。Okhttp 支持 5 个并发 KeepAlive,默认链路生命为 5 分钟(链路空闲后,保持存活的时间),连接池由 ConectionPool 实现,对连接进行回收和管理。ConectionPool 在内部维护了一个线程池,来清理连接,如下所示:

public final class ConnectionPool {    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,            Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,            new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));    //清理连接,在线程池executor里调用。    private final Runnable cleanupRunnable = new Runnable() {        @Override        public void run() {            while (true) {                //执行清理,并返回下次需要清理的时间。                long waitNanos = cleanup(System.nanoTime());                if (waitNanos == -1) return;                if (waitNanos > 0) {                    long waitMillis = waitNanos / 1000000L;                    waitNanos -= (waitMillis * 1000000L);                    synchronized (ConnectionPool.this) {                        try {                            //在timeout时间内释放锁                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);                        } catch (InterruptedException ignored) {                        }                    }                }            }        }    };}

ConectionPool 在内部维护了一个线程池来清理链接,清理任务由 cleanup() 方法完成,它是一个阻塞操作,首先执行清理,并返回下次需要清理的间隔时间,调用调用 wait() 方法释放锁。等时间到了以后,再次进行清理,并返回下一次需要清理的时间,循环往复。

我们来看一看 cleanup() 方法的具体实现。

public final class ConnectionPool {          long cleanup(long now) {        int inUseConnectionCount = 0;        int idleConnectionCount = 0;        RealConnection longestIdleConnection = null;        long longestIdleDurationNs = Long.MIN_VALUE;                 synchronized (this) {            //遍历所有的连接,标记处不活跃的连接。          for (Iterator i = connections.iterator(); i.hasNext(); ) {            RealConnection connection = i.next();                //1. 查询此连接内部的StreanAllocation的引用数量。            if (pruneAndGetAllocationCount(connection, now) > 0) {              inUseConnectionCount++;              continue;            }                idleConnectionCount++;                //2. 标记空闲连接。            long idleDurationNs = now - connection.idleAtNanos;            if (idleDurationNs > longestIdleDurationNs) {              longestIdleDurationNs = idleDurationNs;              longestIdleConnection = connection;            }          }              if (longestIdleDurationNs >= this.keepAliveDurationNs              || idleConnectionCount > this.maxIdleConnections) {            //3. 如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。            connections.remove(longestIdleConnection);          } else if (idleConnectionCount > 0) {            //4. 返回此连接的到期时间,供下次进行清理。            return keepAliveDurationNs - longestIdleDurationNs;          } else if (inUseConnectionCount > 0) {            //5. 全部都是活跃连接,5分钟时候再进行清理。            return keepAliveDurationNs;          } else {            //6. 没有任何连接,跳出循环。            cleanupRunning = false;            return -1;          }        }            //7. 关闭连接,返回时间0,立即再次进行清理。        closeQuietly(longestIdleConnection.socket());        return 0;      }}

整个方法的流程如下所示:

  1. 查询此连接内部的 StreanAllocation 的引用数量。
  2. 标记空闲连接。
  3. 如果空闲连接超过 5 个或者 keepalive 时间大于 5 分钟,则将该连接清理掉。
  4. 返回此连接的到期时间,供下次进行清理。
  5. 全部都是活跃连接,5分钟时候再进行清理。
  6. 没有任何连接,跳出循环。
  7. 关闭连接,返回时间 0,立即再次进行清理。

在 RealConnection 里有个 StreamAllocation 虚引用列表,每创建一个 StreamAllocation,就会把它添加进该列表中,如果留关闭以后就将 StreamAllocation 对象从该列表中移除,正是利用这种引用计数的方式判定一个连接是否为空闲连接,

public final List> allocations = new ArrayList<>();

查找引用计数由 pruneAndGetAllocationCount() 方法实现,具体实现如下所示:

public final class ConnectionPool {         private int pruneAndGetAllocationCount(RealConnection connection, long now) {       //虚引用列表       List> references = connection.allocations;       //遍历虚引用列表       for (int i = 0; i < references.size(); ) {         Reference reference = references.get(i);         //如果虚引用StreamAllocation正在被使用,则跳过进行下一次循环,         if (reference.get() != null) {           //引用计数           i++;           continue;         }            // We've discovered a leaked allocation. This is an application bug.         StreamAllocation.StreamAllocationReference streamAllocRef =             (StreamAllocation.StreamAllocationReference) reference;         String message = "A connection to " + connection.route().address().url()             + " was leaked. Did you forget to close a response body?";         Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);            //否则移除该StreamAllocation引用         references.remove(i);         connection.noNewStreams = true;            // 如果所有的StreamAllocation引用都没有了,返回引用计数0         if (references.isEmpty()) {           connection.idleAtNanos = now - keepAliveDurationNs;           return 0;         }       }              //返回引用列表的大小,作为引用计数       return references.size();     } }

 

四、缓存机制


4.1 缓存策略

在分析 Okhttp 的缓存机制之前,我们先来回顾一下 HTTP 与缓存相关的理论知识,这是实现 Okhttp 机制的基础。HTTP 的缓存机制也是依赖于请求和响应 header 里的参数类实现的,最终响应式从缓存中去,还是从服务端重新拉取,HTTP 的缓存机制的流程如下所示:

Android OkHttp 源码解析 - 拦截器_第4张图片

HTTP 的缓存可以分为两种:

  • 强制缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
  • 对比缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存 标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。

强制缓存优先于对比缓存。

上面提到强制缓存使用的的两个标识:

  • Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
  • Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。

Cache-Control的取值有以下几种:

  • private: 客户端可以缓存。
  • public: 客户端和代理服务器都可缓存。
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache: 需要使用对比缓存来验证缓存数据。
  • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。

我们再来看看对比缓存的两个标识:

Last-Modified/If-Modified-Since

Last-Modified 表示资源上次修改的时间。

当客户端发送第一次请求时,服务端返回资源上次修改的时间:

Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT 

服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改, 则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。

上面是一种时间戳标记资源是否修改的方法,还有一种资源标识码ETag的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified。

Etag/If-None-Match

ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:

ETag: "5694c7ef-24dc"

客户端再次发送,会在header里携带上次服务端返回的资源标识码:

If-None-Match:"5694c7ef-24dc"

服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回 304,客户端可以继续使用缓存。

以上便是HTTP缓存策略的相关理论知识,我们来看看具体实现。

Okhttp的缓存策略就是根据上述流程图实现的,具体的实现类是CacheStrategy,CacheStrategy的构造函数里有两个参数:

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

这两个参数参数的含义如下:

  • networkRequest:网络请求。
  • cacheResponse:缓存响应,基于DiskLruCache实现的文件缓存,可以是请求中url的md5,value是文件中查询到的缓存,这个我们下面会说。

CacheStrategy就是利用这两个参数生成最终的策略,有点像map操作,将networkRequest与cacheResponse这两个值输入,处理之后再将这两个值输出,们的组合结果如下所示:

  • 如果networkRequest为null,cacheResponse为null:only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
  • 如果networkRequest为null,cacheResponse为non-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
  • 如果networkRequest为non-null,cacheResponse为null:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
  • 如果networkRequest为non-null,cacheResponse为non-null:Header中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。

那么这四种情况是如何判定的,我们来看一下。

CacheStrategy 是利用 Factory 模式进行构造的,CacheStrategy.Factory 对象构建以后,调用它的 get() 方法即可获得具体的 CacheStrategy,CacheStrategy.Factory.get() 方法内部调用的是 CacheStrategy.Factory.getCandidate() 方法,它是核心的实现。如下所示:

public static class Factory {            private CacheStrategy getCandidate() {          //1. 如果缓存没有命中,就直接进行网络请求。          if (cacheResponse == null) {            return new CacheStrategy(request, null);          }              //2. 如果TLS握手信息丢失,则返回直接进行连接。          if (request.isHttps() && cacheResponse.handshake() == null) {            return new CacheStrategy(request, null);          }          //3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。          if (!isCacheable(cacheResponse, request)) {            return new CacheStrategy(request, null);          }              //4. 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。          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);          }              //计算当前age的时间戳:now - sent + age          long ageMillis = cacheResponseAge();          //刷新时间,一般服务器设置为max-age          long freshMillis = computeFreshnessLifetime();              if (requestCaching.maxAgeSeconds() != -1) {            //一般取max-age            freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));          }              long minFreshMillis = 0;          if (requestCaching.minFreshSeconds() != -1) {            //一般取0            minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());          }              long maxStaleMillis = 0;          if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {            maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());          }              //5. 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。          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());          }              //6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求          //交给服务端判断处理          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);        }}

整个函数的逻辑就是按照上面那个HTTP缓存判定流程图来实现,具体流程如下所示:

  1. 如果缓存没有命中,就直接进行网络请求。
  2. 如果TLS握手信息丢失,则返回直接进行连接。
  3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问。
  4. 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
  5. 如果缓存在过期时间内则可以直接使用,则直接返回上次缓存。
  6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求交给服务端判断处理

整个流程就是这样,另外说一点,Okhttp的缓存是根据服务器header自动的完成的,整个流程也是根据RFC文档写死的,客户端不必要进行手动控制。理解了缓存策略,我们来看看缓存在磁盘上是如何被管理的。

4.2 缓存管理

这篇文章我们来分析Okhttp的缓存机制,缓存机制是基于DiskLruCache做的。Cache类封装了缓存的实现,实现了InternalCache接口。InternalCache接口如下所示:

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);  //跟踪一个满足缓存条件的GET请求  void trackConditionalCacheHit();  //跟踪满足缓存策略CacheStrategy的响应  void trackResponse(CacheStrategy cacheStrategy);}

我们接着来看看它的实现类。Cache 没有直接实现 InternalCache 这个接口,而是在其内部实现了 InternalCache 的匿名内部类,内部类的方法调用 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);    }};InternalCache internalCache() {    return cache != null ? cache.internalCache : internalCache;}

在Cache类里还定义一些内部类,这些类封装了请求与响应信息。

  • Cache.Entry:封装了请求与响应等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis与receivedResponseMillis。
  • Cache.CacheResponseBody:继承于ResponseBody,封装了缓存快照snapshot,响应体bodySource,内容类型contentType,内容长度contentLength。

除了两个类以外,Okhttp 还封装了一个文件系统类 FileSystem 类,这个类利用 Okio 这个库对 Java 的 File 操作进行了一层封装,简化了 IO 操作。理解了这些剩下的就是 DiskLruCahe 里的插入缓存 、获取缓存和删除缓存的操作。

更多相关文章

  1. android http-post方法简单实现
  2. Android 后台线程调用前台线程的几种方法
  3. Android WebView 使用方法,可以解决web视频播放层级问题。
  4. Android 实现分享功能的方法 分类: Android ...
  5. android 堆栈调试方法
  6. android获取短信方法1
  7. ch07 Android 日期与时间对话框
  8. android中drawable转bitmap的两种方法

随机推荐

  1. 无法从SQLite数据库获取最后一行
  2. java eclipse连接并且操作mysql数据库详
  3. 是否遇到过MySQL workbench text字段不能
  4. MySQL 5.7中的未知列
  5. 基本sql:输出用双引号括起来的标题
  6. 一条SQL语句实现添加不重复记录
  7. Sql server 2005中output用法解析
  8. 整取整存,尽可能的减少SQL连接数.
  9. SQL查找条件语句构造类
  10. Oracle:从SQL文件批量导入数据