OKHttp源码解析 4 - 1:拦截器源码分析、拦截器链

前言

看了我们这个系列文章的应该知道,前面我们多次提到拦截器链这个概念,然后说它是 OKHttp 当中非常重要的一个内容。那拦截器到底是个啥呢?官网给我们的解释是这样的:拦截器是OKHttp中提供的一种强大机制,它可以实现网络监听、请求以及响应重写、请求失败重试等功能。官网把拦截器分为两种:Application 拦截器、network拦截器,而 OKHttp 内部提供了一系列的拦截器给我们,这也是 OKHttp 拦截器的核心。

而拦截器链就是由一系列拦截器组成的,顺序调用的一个机制,简称拦截器链。另外很重要的一点是:拦截器链是不区分同步异步的,也就是说同步异步都会用到拦截器链。

拦截器链源码分析

在第二篇介绍异步请求时,我们提到过:在异步请求中,我们是通过拦截器链来获取 Response 的,也就是通过 getResponseWithInterceptorChain() 这个方法,当时没有仔细说明,现在我们来看一下这个方法的源码

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()));
    //连接拦截器:建立可用的连接,是CallServerInterceptor的基本
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    //将http请求写进io流当中,并且从io流中读取response
    interceptors.add(new CallServerInterceptor(forWebSocket));

    //初始化 RealInterceptorChain
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

源码中前面部分主要提到了五个拦截器,分别是:
- RetryAndFollowUpInterceptor:重试和失败重定向拦截器
- BridgeInterceptor:桥接拦截器,处理一些必须的请求头信息的拦截器
- CacheInterceptor:缓存拦截器,用于处理缓存
- ConnectInterceptor:连接拦截器,建立可用的连接,是CallServerInterceptor的基本
- CallServerInterceptor:请求服务器拦截器将 http 请求写进 IO 流当中,并且从 IO 流中读取响应 Response

后半部分很重要了,先是初始化 RealInterceptorChain,然后调用 proceed() 方法,我们跟踪这个方法,进入到 RealInterceptorChain 的源码中,最终定位到 RealInterceptorChain 中的 proceed() 方法

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {

    //......省略部分代码,只显示核心代码

    // Call the next interceptor in the chain.
    //调用拦截器链中的下一个拦截器,注意 index + 1 参数
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    //......省略部分代码,只显示核心代码

    return response;
  }

从 RealInterceptorChain 的 proceed() 方法的核心代码中,我们可以看到。在这个方法中,主要就是新建了一个新的 RealInterceptorChain,只是在第五个 index 参数中,传入了 index + 1,确保调用到的是下一个拦截器,这也是 OKhttp 中拦截器链的关键所在。然后获取当前的拦截器,调用 Interceptor 的 intercept() 方法。我们可以查看上面五个拦截器的 intercept() 方法,可以发现,这五个拦截器的 intercept() 方法中,无一例外都调用了 chain.proceed() 方法,然后 proceed() 方法又会去调用下一个拦截器的 intercept()。Response也是由这条链依次向上返回。由此就形成了一个完整的链条。

从这里我们可以总结得到:拦截器通过 intercept() 调用拦截器链的 proceed() 方法,由此创建下一个拦截器链,然后又调用下一个拦截器的 intercept() 方法。从而形成一个调用链,依次获取 Response 返回。看下面的调用流程图

拦截器链调用流程图

接下来我们就重点介绍一下 OKHttp 提供的五个拦截器

RetryAndFollowUpInterceptor 重试和重定向拦截器

从名字我们就能知道这个拦截器是用来做失败重连的,我们先看一下源码

@Override public Response intercept(Chain chain) throws IOException {

    //......

    //用于服务端数据传输的输入输出流,依次传到 ConnectInterceptor 用于 http 请求,本 intercepter 没有用到,核心点!!
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    //......

    while (true) {
      Response response;
      boolean releaseConnection = true;
      try {
        //调用拦截器链,执行下一个拦截器
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        //......
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        //......
        releaseConnection = false;
        continue;
      } finally {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

      //......

      //对重试次数进行判断,当重试次数大于20次时,释放掉streamAllocation
      if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      //......

    }
  }

这里只展示了部分核心代码,详细代码可查看源码。

前面我们提到,OKHttp 请求是通过 RealInterceptorChain 串联起所有的拦截器,然后依次往回返回 Response,只有所有拦截器正常返回,我们的请求才算成功。但是我们知道,网络在请求过程中是不稳定的,可能会存在不同程度的问题。这个时候 Response 返回码就不一定是200了,Response 也就不能用了。那我们这个拦截器就对这种情况进行了精密的部署,具体的逻辑在代码段的 while 循环中。我们只讲一下两处注释的地方。第一个注释位置:很明显,这里通过调用 chain.proceed() 方法去调用下一个拦截器,并获取 Response;第二次注释位置:这里对重试次数做了限制,避免无限制的重试,能跳出整个逻辑。

另外这段源码中,还创建了一个很重要的内容,那就是
StreamAllocation,它用于收集服务端数据传输时的输入输出流,在本拦截器中创建,但是会依次传到 ConnectInterceptor 用于 http 请求。

RetryAndFollowUpIntercepter 总结:
1. 创建StreamAllocation对象
2. 调用RealInterceptorChain.proceed(…)进行网络请求
3. 根据异常结果或者响应结果判断是否要进行重新请求
4. 调用下一个拦截器,对 Response 进行处理,返回给上一个拦截器

BridgeInterceptor 桥接拦截器

BridgeInterceptor 拦截器主要负责对头部信息进行添加(长度、编码方式、压缩等),看一下源码

    //省略部分源码
    //调用拦截器的 proceed() 方法,请求服务器,获取 Response。
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

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

    //判断 Response 是否进行了 Gzip 压缩,是则对 body 体进行解压。
    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);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }

前面我省略了一段源码,其实这一段主要就是对请求头进行了处理,大家可以去源码查看。我这里贴上的主要就是两段,一段就是前面一直提到的,调用拦截器的 proceed() 方法,执行下一个拦截器,获取 Response。另外一段就是判断 Response 是否进行了 Gzip 压缩,是则对 body 体进行解压。

BridgeInterceptor 总结:
1. 负责将用户构建的一个 Request 请求转化为能够进行网络访问的请求
2. 将这个符合网络请求的 Request 进行网络请求
3. 将网络请求回来的响应 Response 转化为用户可用的 Response

CacheInterceptor 缓存拦截器

首先我们在解析缓存拦截器源码前,我们先了解一下 OKHttp 缓存的使用方法。

public void cacheRequest() {
        //设置缓存目录、大小
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(new File("cache"), 24 * 1024 * 1024)).build();
        Request request = new Request.Builder().url("http://www.baidu.com").build();

        Call call = client.newCall(request);
        try {
            call.execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

这里我们看到了一个新建的 Cache 类,传入缓存目录和缓存大小参数。跟踪进去查看源码发现,这个 Cache 类其实本质就是 DiskLruCache,也就是说,OKHttp 的缓存策略其实就是用 DiskLruCache 实现的。在分析 CacheInterceptor 前,我们先主要讲一下这个 Cache 类,主要就是 Cache 类的 put() 和 get() 两个方法。

@Nullable CacheRequest put(Response response) {
    String requestMethod = response.request().method();

    //判断是否是无效的缓存
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        //是无效缓存则移除缓存
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    //不缓存非 GET 方法的响应
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    // Entry 类:封装了各种属性,用于数据缓存
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //先把 URL 经 MD5 加密,获取十六进制字符串,然后根据这个字符串创建 Editor
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      //把 Request 与 Response 的头部写入缓存
      entry.writeTo(editor);
      //返回 CacheRequestImpl,CacheRequestImpl 主要用于缓存 body 信息
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

put 方法总结:
- 判断是否是有效的缓存
- 判断是否是 GET 请求。注意:源码注释中提到,对非 GET 请求技术上能做到缓存,但是复杂性比较高,而且取得的收益很小,所以缓存只针对 GET 请求
- 使用 DiskLruCache.Editor 创建用于数据缓存的 Entry 类,缓存Request 和 Response 的头部信息,然后返回 CacheRequest 的实现类,用于缓存 body 信息。具体 body 缓存的实现位于 CacheIntercepor 中。

@Nullable Response get(Request request) {
    //对 URL 进行MD5加密,获取加密后的 16进制字符串
    String key = key(request.url());
    //缓存快照
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      //根据 key 值从缓存获取缓存快照
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      //赋值 Entry 对象
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    //从 Entry 中获取 Response
    Response response = entry.response(snapshot);
    //匹配 Request 和 Response,如果不匹配,返回 null
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

get方法总结:

  • 根据加密的 key 值,从缓存中获取缓存快照
  • 创建 Entry 对象,从 Entry 中获取 Response
  • 检测 Request 和 Response 是否匹配,不匹配返回 null,匹配返回 Response

在了解了 Cache 类的存、取方法后,我们再来分析 CacheInterceptor。先贴上源码:

@Override public Response intercept(Chain chain) throws IOException {
    //获取缓存,可能为空
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //获取缓存策略类,CacheStrategy 内部维护着一个网络请求 Request 和一个缓存 Response,目的是:判断是使用网络还是缓存,还是两者都用
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    //......

    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      //当 networkRequest 和 cacheResponse 都为空时,构建一个错误码为504的 Response
      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 {
      //获取 Response,交给下一个拦截器
      networkResponse = chain.proceed(networkRequest);
    } finally {
      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) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //当响应码为304,从缓存中获取数据
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        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) {
      //http 头部是否有响应体,并且缓存策略可以被缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.写入 Response 到缓存中
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //判断 Request 方法是否是有效的缓存,是的话,移除这个缓存。
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

详情查看上面源码的注释。从源码中我们可以发现,OKHttp 的缓存主要是根据 CacheStrategy 这个缓存策略类来处理的,它决定了是使用网络还是缓存,又或是两者都用。源码最后可以看到,当有响应体且可以缓存时,缓存当前数据。然后还判断了,当请求是无效缓存时,要清除这个缓存。CacheInterceptor 缓存拦截器的整个逻辑就是这样了,重点在 CacheStrategy 这个缓存策略类。

接下来还有两个拦截器没有分析,分别是 ConnectInterceptor 和 CallServerInterceptor 拦截器,因为篇幅过长,所以这两个拦截器将在下一篇文章中进行讲解。

猜你喜欢

转载自blog.csdn.net/qq_14904791/article/details/80224010

相关文章