OKHttp3 源码解析之拦截器(二)

在上一篇文章中,主要梳理了 OKHttp 请求的整体流程,了解到拦截器在其中有非常重要的意义,那么本篇文章就重点介绍一下 OKHttp 中的精髓 ---- 拦截器。本文中涉及到的自定义类的源码都在 Github 上的 OkHttpPractice 工程中。

  1. 拦截器的使用
  2. 源码中拦截器的应用及分析

1. 拦截器的使用

拦截器是 OKHttp 设计的精髓所在,每个拦截器负责不同的功能,使用责任链模式,通过链式调用执行所有的拦截器对象中的 Response intercept(Chain chain) 方法。拦截器在某种程度上也借鉴了网络协议中的分层思想,请求时从最上层到最下层,响应时从最下层到最上层。

一个拦截器可以拦截请求和响应,获取或修改其中的信息,这在编程中是非常有用的。不仅在源码中拦截器使用的很广泛,开发者也可以根据自己的需求自定义拦截器,并将其加入到 OkHttpClient 对象中。

1.1 自定义拦截器

拦截器的源码如下:

/**
 * Observes, modifies, and potentially short-circuits requests going out and the corresponding
 * responses coming back in. Typically interceptors add, remove, or transform headers on the request
 * or response.
 */
public interface Interceptor {
  Response intercept(Chain chain) throws IOException;

  interface Chain {
    Request request();

    Response proceed(Request request) throws IOException;

    /**
     * Returns the connection the request will be executed on. This is only available in the chains
     * of network interceptors; for application interceptors this is always null.
     */
    @Nullable Connection connection();
  }
}

其中最重要的方法便是 Response intercept(Chain chain) 方法。自定义拦截器只要实现 interceptor 接口,重写其中的 Response intercept(Chain chain) 方法便基本完成。

在开发中,经常想查看服务器响应中的内容,如果使用拦截器的话,则可以非常方便的实现,只要自定义拦截器并加入到 OKHttpClient 对象中,那么使用此 OKHttpClient 对象进行的网络请求都会将其响应信息打印出来。自定义的拦截器 LogInterceptor,源码如下所示:

public class LogInterceptor implements Interceptor {

    private static final Charset UTF8 = Charset.forName("UTF-8");

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response;
        long startTime = System.currentTimeMillis();
        response = chain.proceed(request);
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;
        BufferedSource source = response.body().source();
        source.request(Long.MAX_VALUE);
        Buffer buffer = source.buffer();
        String log = "\n================="
                .concat("\nnetwork code ==== " + response.code())
                .concat("\nnetwork url ===== " + request.url())
                .concat("\nduration ======== " + duration)
                .concat("\nrequest duration ============ " + (response.receivedResponseAtMillis() - response.sentRequestAtMillis()))
                .concat("\nrequest header == " + request.headers())
                .concat("\nrequest ========= " + bodyToString(request.body()))
                .concat("\nbody ============ " + buffer.clone().readString(UTF8));
        Log.i("lijk", "log is " + log);
        return response;
    }

    /**
     * 请求体转String
     *
     * @param request 请求体
     * @return String 类型的请求体
     */
    private static String bodyToString(final RequestBody request) {
        try {
            final Buffer buffer = new Buffer();
            request.writeTo(buffer);
            return buffer.readUtf8();
        } catch (final Exception e) {
            return "did not work";
        }
    }
}

1.2 注意事项

这里有个坑需要注意一下:response.body().string(); 方法只能被调用一次,如果多次调用 response.body().string(); 则会抛出如下异常:

exception.png

可以看到 string() 的源码如下所示:

  /**
   * Returns the response as a string decoded with the charset of the Content-Type header. If that
   * header is either absent or lacks a charset, this will attempt to decode the response body in
   * accordance to <a href="https://en.wikipedia.org/wiki/Byte_order_mark">its BOM</a> or UTF-8.
   * Closes {@link ResponseBody} automatically.
   *
   * <p>This method loads entire response body into memory. If the response body is very large this
   * may trigger an {@link OutOfMemoryError}. Prefer to stream the response body if this is a
   * possibility for your response.
   */
  public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
  }

因为在执行完读取数据之后,IO 流被关闭,如果再次调用此方法,就会抛出上面的异常。

而且从注释中可以看到,此方法将响应报文中的主体全部都读到了内存中,如果响应报文主体较大,可能会导致 OOM 异常。所以更推荐使用流的方式获取响应体的内容。如下所示:

  BufferedSource source = response.body().source();
  source.request(Long.MAX_VALUE);
  Buffer buffer = source.buffer();
  String response = buffer.clone().readString(UTF8);

2. 源码中拦截器的应用及分析

在上一篇分析整体流程的文章中,在 RealCall 中有一个方法非常重要,不论是异步请求还是同步请求,都是通过该方法获取服务器响应的,源码如下所示:

Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> 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);
  // 通过 RealInterceptorChain 对象链式的调用拦截器,从而得到响应。
  return chain.proceed(originalRequest);
}

其中的每个拦截器负责具体不同的功能,接下来就分析每个拦截器的功能,因为拦截器中最重要的方法便是 Response intercept(Chain chain),所以我们也重点分析 Response intercept(Chain chain) 方法的实现。

2.1 RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor 拦截器主要负责实现 HTTP 协议中的认证质询、重定向和超时重试等协议机制。

RetryAndFollowUpInterceptor 拦截器的主要功能如下所示:

  1. 初始化连接对象 StreamAllocation
  2. 通过 RealInterceptorChain 调用链对象得到响应
  3. 通过得到的响应,根据 HTTP 协议做认证质询、重定向和超时重试等处理,通过 followUpRequest() 方法创建后续新的请求
  4. 若没有后续请求,即 followUpRequest() 方法返回为 null,则说明当前请求结束,返回响应
    ,若后续请求不为空,则继续进行请求

源码如下:

@Override public Response intercept(Chain chain) throws IOException {
  Request request = chain.request();
  // 创建一个 StreamAllocation 对象
  streamAllocation = new StreamAllocation(
      client.connectionPool(), createAddress(request.url()), callStackTrace);

  int followUpCount = 0;
  Response priorResponse = null;
  // 启动一个 While 死循环
  while (true) {
    // 判断是否已经取消,若已取消则抛出 IO 异常
    if (canceled) {
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response = null;
    boolean releaseConnection = true;
    try {
      // 通过 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.
      // 如果进入 RouteException 路由异常,则尝试是否可以重新进行请求,若可以则从头开始新的请求
      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.
      // 若是进入 IOException IO异常,若可以重新尝试请求,则从头开始新的请求
      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.
    // 如果之前发生过重定向,并且 priorResponse 不为空,则创建新的 响应对象,并将其 body 置位空
    if (priorResponse != null) {
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
          .build();
    }

    // 添加认证需要的头部,处理重定向或超时重试,得到新的请求
    // followUpRequest() 方法很重要,涉及到 HTTP 中认证质询、重定向和重试等协议的实现
    Request followUp = followUpRequest(response);

    // 若 followUp 重试请求为空,则当前请求结束,并返回当前的响应
    if (followUp == null) {
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }

    // 关闭响应结果
    closeQuietly(response.body());

    // 若重定向、认证质询、重试次数超过 MAX_FOLLOW_UPS,则抛出 ProtocolException 异常
    if (++followUpCount > MAX_FOLLOW_UPS) {
      streamAllocation.release();
      throw new ProtocolException("Too many follow-up requests: " + followUpCount);
    }

    // 若请求中的主体为 UnrepeatableRequestBody 不可被重复使用的请求体类型,则抛出异常
    if (followUp.body() instanceof UnrepeatableRequestBody) {
      streamAllocation.release();
      throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
    }

    // 判断是否是相同的连接,若不相同则释放 streamAllocation,并重新创建新的 streamAllocation 对象
    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;
  }
}

2.2 BridgeInterceptor

BridgeInterceptor 拦截器主要功能是:

  • 设置一些请求和响应首部,如:Content-TypeContent-LengthHost 等常见的请求和响应首部。
  • 处理 HTTP 请求和响应中的 Cookie
  • 如果在请求中设置了编码,要从响应流中解码

源码如下:

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

  RequestBody body = userRequest.body();
  if (body != null) {
    // 在请求中设置实体首部 Content-Type
    MediaType contentType = body.contentType();
    if (contentType != null) {
      requestBuilder.header("Content-Type", contentType.toString());
    }

    // 在请求中设置实体首部 Content-Length 和 Transfer-Encoding
    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");
    }
  }

  // 设置请求首部 `Host`
  if (userRequest.header("Host") == null) {
    requestBuilder.header("Host", hostHeader(userRequest.url(), false));
  }

  // 设置请求首部 Connection,若 `Connection` 为空,则打开长连接
  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.
  // 如果在请求中设置了 "Accept-Encoding: gzip",要记得从响应流中解码
  boolean transparentGzip = false;
  if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }

  // 为请求添加 Cookie
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }

  // 设置请求首部 "User-Agent"
  if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
  }

  Response networkResponse = chain.proceed(requestBuilder.build());

  // 从响应中得到 Cookie,并交给传入的 CookieJar 对象处理
  HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

  Response.Builder responseBuilder = networkResponse.newBuilder()
      .request(userRequest);

  // 如果之前在请求中设置了 "Accept-Encoding: gzip" 编码,则需要对响应流进行解码操作并移除响应中的首部字段 “Content-Encoding” 和 “Content-Length”
  if (transparentGzip
      && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
      && HttpHeaders.hasBody(networkResponse)) {
    GzipSource responseBody = new GzipSource(networkResponse.body().source());
    Headers strippedHeaders = networkResponse.headers().newBuilder()
        .removeAll("Content-Encoding")
        .removeAll("Content-Length")
        .build();
    responseBuilder.headers(strippedHeaders);
    responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
  }

  return responseBuilder.build();
}

这里涉及到 OKHttp 中对 Cookie 的处理,其中有一个接口十分重要 ---- CookieJar,源码如下:

public interface CookieJar {
  /** A cookie jar that never accepts any cookies. */
  CookieJar NO_COOKIES = new CookieJar() {
    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
    }

    @Override public List<Cookie> loadForRequest(HttpUrl url) {
      return Collections.emptyList();
    }
  };

  // 从响应中获取 Cookie
  void saveFromResponse(HttpUrl url, List<Cookie> cookies);

  // 为请求添加 Cookie
  List<Cookie> loadForRequest(HttpUrl url);
}

OKHttp 对 Cookie 的管理还是十分方便简洁的。创建一个类实现 Cookiejar 接口,并将其对象设置给 OKHttpClient,那么使用此 OKHttpClient 对象进行的网络请求都会自动处理 Cookie。这里有一个我实现的自动管理 Cookie 的类 CustomCookieManager,可以实现 Cookie 的持久化,Github 上还有其他很好的实现在 OKHttp 中管理 Cookie 的类,也可以参考。

2.3 CacheInterceptor

CacheInterceptor 实现了 HTTP 协议中的缓存机制,其主要功能如下:

  1. 从缓存中读取缓存,并创建缓存策略对象
  2. 根据创建的缓存策略对象,从缓存、网络获取响应并生成最终的响应对象
  3. 更新缓存内容,并返回响应对象

源码如下:

public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    // 从 OKHttpClient 中传入的 InternalCache(内部缓存)对象
    this.cache = cache;
  }

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

    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.
    // 如果不走网络进行请求,并且缓存响应为空,则创建状态码为 504 的响应并返回
    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();
    }

    // If we don't need the network, we're done.
    // 如果不走网络进行请求,并且缓存响应不为空,则返回从缓存中获取的响应
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      // 如果前面的条件都不满足,从拦截器链中进行网络请求并得到响应
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    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());
      }
    }

    // 根据网络请求响应生成最终的响应
    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);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

  ......

}

2.4 ConnectInterceptor

ConnectInterceptor 拦截器的主要功能是:

  • 得到从 RetryAndFollowUpInterceptor 中创建的 StreamAllocation 对象
  • 通过 StreamAllocation 对象创建 HttpCodec 对象
  • 通过 StreamAllocation 对象创建 RealConnection 对象
  • 最终通过 RealInterceptorChainproceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection) 方法得到响应对象并返回。

源码如下:

/** 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 类,此类创建了 HttpCodecRealConnection 对象,这两个类在网络请求中都是非常重要的类,会在后面文章中详细分析

2.5 CallServerInterceptor

CallServerInterceptor 拦截器中最主要的功能就是:

  • 遵循 HTTP 协议规范,通过 HttpCodec 对象写入请求头、请求主体、读取响应头和响应主体
  • 生成最初的响应对象并返回

源码如下所示:

@Override public Response intercept(Chain chain) throws IOException {
  // 得到 httpCodec、streamAllocation、connection 和 request 对象
  RealInterceptorChain realChain = (RealInterceptorChain) chain;
  HttpCodec httpCodec = realChain.httpStream();
  StreamAllocation streamAllocation = realChain.streamAllocation();
  RealConnection connection = (RealConnection) realChain.connection();
  Request request = realChain.request();

  // 向 HttpCodec 对象中写入请求头部信息
  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.
    // 若在请求头部中存在 ”Expect: 100-continue“,先不发送请求主体,只有收到 ”100-continue“ 响应报文才会将请求主体发送出去。
    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();
  }

  // 如果响应的状态码为 204 和 205 并且响应体不为空,则抛出异常
  if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
    throw new ProtocolException(
        "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
  }

  return response;
}

在此拦截器中非常重要的一个对象是 HttpCodec,它是一个接口,具体的实现类有 Http1Codec 和 Http2Codec 两个类,分别对应着 HTTP1.1 和 HTTP2。在 HttpCodec 内部是通过 sinksource 来实现的。

2.6 注意

关于 OKHttp 的拦截器需要注意的一点是,从 RealCallgetResponseWithInterceptorChain() 方法中可以看到开发自定义的拦截器有两种类型的

Response getResponseWithInterceptorChain() throws IOException {
  // Build a full stack of interceptors.
  List<Interceptor> 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);
  return chain.proceed(originalRequest);
}

一种是在最开始,通过 client.interceptors() 方法添加的普通拦截器;另一种是通过 client.networkInterceptors() 方法添加的网络请求拦截器,两者的区别如下:

  • 普通拦截器:对发出去的请求做最初的处理,对最终得到的响应做处理
  • 网络拦截器:对发出去的请求做最后的处理,对收到的响应做最初的处理

两种拦截器都可以通过在初始化 OKHttpClient 对象的时候设置,如下所示:

OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(new LogInterceptor())
                .addNetworkInterceptor(new LogInterceptor())
                .build();

虽然都是添加了 LogInterceptor 拦截器给 OKHttpClient 对象,但是从 RealCallgetResponseWithInterceptorChain() 方法中可以看出两个拦截器调用的时机不同。


本文中涉及到的自定义类的源码都在 Github 上的 OkHttpPractice 工程中。


参考资料:

OkHttp源码解析 -- 俞其荣

OkHttp源码解析 -- 高沛

OKhttp源码学习(十)——写在最后 -- 小禤大叔

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

推荐阅读更多精彩内容