okhttp面试题----拦截器interceptor

相信很多人面试都有遇到这个问题
"了解过okhttp吗,简单说一下okhttp的拦截器吧"

我曾经就经历过这样的场景,我只能挠着头说记不清楚了,其实压根没咋细看,面试成不成功的先放一边,面子问题都过不去
在这里插入图片描述
回到正题,okhttp在android三方库里面属于一个万金油级别的存在,

  1. 首先作为目前最主流的网络请求框架,强是一定的
  2. 其次在okhttp中使用了多种设计模式,比如单例,工程,观察者,装饰等经典设计模式,吃透了okhttp的细节也能提升对设计模式的理解
  3. okhttp的拦截器各个网络请求过程进行了封装,一方面装了解okhttp的过程中可以加深我们对于http请求的认识,另一方面通过链式结构也方便于我们自定义拦截器进行特定的工作
  4. 装okhttp4.0以后,官网源码采用了kotlin语言,没接触kotlin的小伙伴可以借此正好简单接触一下(毕竟官推),也算是让自己多掌握一项技术
  5. 如果面试时遇到面试官问了关于okhttp的东西,希望看完这篇文章后的你能跟面试官侃侃而谈,完全讲述okhttp的重要部分怎么也能讲上一二十分钟,将他带进你的节奏吧,okhttp是我的主场

**

先上一张okhttp的简略流程图**

在这里插入图片描述
简单的过程是我们通过构建okhttpClient,被转换成一个call也就是请求,然后通过调度器根据启动方式选择加入队列并开启,然后通过五个拦截器补全配置信息并实现网络请求,最终拿到我们所需要的response

拦截器

首先是拦截器的执行顺序
自定义拦截器 – 重定向拦截器 – 桥接(桥梁)连接器 – 缓存拦截器 – 连接拦截器 – 网络拦截器

接下来我会逐一简单讲一下每个拦截器的主要作用和关键代码

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) {
      if (canceled) {
        streamAllocation.release();
        throw new IOException("Canceled");
      }

      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.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(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        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) {
          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;
      try {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

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

interceptor方法是每一个拦截器的核心方法,可以看到的是,在这个重定向拦截器中,我们所填的东西在这里出现了一个streamAllocation ,这个类是一些我们http请求的组件和参数,源码量大就不放上来展示了 ,简单总结一下:

1.创建streamAllocation 并填充我们请求需要的一部分信息
2.重连,默认次数为20次,超过则抛出异常,连接成功则将请求和重连结果一并传给下一个拦截器
3.接收从下一个拦截器传回的response,处理并返回给上一层,也就是我们写的client

BridgeInterceptor - 桥接拦截器

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

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

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

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

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

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

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

    return responseBuilder.build();
  }

桥接拦截器更像是一个小小的分层,从源码中不难看出,在这一层中对我们所构建的request的请求头和请求体进行了设置,使得我们所需要发送的请求进一步完善

1.对请求头和请求体进行了配置参数的补充并将发起网络请求
2.对下一层适配器返回的response进行解压(gzip,简单来说就是将网络请求的数据解压成我们所需要的格式并塞进response的body里)处理并返回给上一层拦截器

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;

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

此部分的代码量较大,其实我想只截主要功能代码,但是想想还是全都放上来了,毕竟这个方法是拦截器最重要的方法,还是都看一下吧,这个拦截器的主要功能就和他的名字一样,主要是关于我们请求的缓存部分

1.cache若不为空则赋cacheCandidate对象
2.获取缓存策略,可以自己设置,默认为 CacheControl.FORCE_NETWORK(即强制使用网络请求)
CacheControl.FORCE_CACHE(即强制使用本地缓存,如果无可用缓存则返回一个code为504的响应)
3.在不为空且有缓存策略时,若返回304则直接响应缓存创建response返回
4.若无网络或无缓存时返回504
5.在有缓存策略无缓存时调用cache的put方法进行缓存
**

ConnectInterceptor - 连接拦截器

interceptor方法

@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, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }

newsteam方法

 public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);

      HttpCodec resultCodec;
      if (resultConnection.http2Connection != null) {
        resultCodec = new Http2Codec(client, this, resultConnection.http2Connection);
      } else {
        resultConnection.socket().setSoTimeout(readTimeout);
        resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
        resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
        resultCodec = new Http1Codec(
            client, this, resultConnection.source, resultConnection.sink);
      }

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

连接拦截器的工作比较简单,也可以理解为在这一步,真正的网络请求才刚刚开始,之前的部分都属于准备工作,为请求填满所需要的配置信息,在这里拿到了从第一个拦截器就创建的streamAllocation,并生成了io,最后return的proceed的方法,这个方法是来自RealInterceptorChain(翻译:真实拦截器链)的一个方法,其返回值也是response,主要是发起网络请求的重要开端,代码不少,有兴趣的可以自己导个okhttp依赖点进去看一下,总结:

1.将url所在的StreamAllocaiton拿到并生成流(RealConnect)
2.将流和httpcode一并交给下一层拦截器进行请求,并返回response

CallServerInterceptor - 网络拦截器

public final class CallServerInterceptor implements Interceptor {
  private final boolean forWebSocket;

  public CallServerInterceptor(boolean forWebSocket) {
    this.forWebSocket = forWebSocket;
  }

  @Override public Response intercept(Chain chain) throws IOException {
1   HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();

    long sentRequestMillis = System.currentTimeMillis();
2   httpCodec.writeRequestHeaders(request);

3   if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }

4   httpCodec.finishRequest();

    Response response = httpCodec.readResponseHeaders()
        .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 {
5     response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }

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

最后一个拦截器也是最后真正的请求,十年磨一剑,这个拦截器主要的功能就是发起请求和拿到并处理请求结果

1.发起请求
2.完成读写
3.根据返回码处理请求结果
4.关闭连接

==============================================================================
以上便是五个拦截器以及他们的主要功能,源码量很大,我只截了比较重要的部分,对于我自己而言我也不敢保证自己吃透了okhttp,还有一些部分理解的不够深刻,有不对的地方也请指正,相互学习讨论
在这里插入图片描述
关于自定义拦截器,我手上没有现存的demo,简单的去说继承intercceptor类在里面怎样怎样谁都会说,所以我就不耽误大家了,毕竟网上也有很多大神写的自定义拦截器连源码带解析整的比我好太多,我没写不代表不重要,面试中也是很重要的一环,希望大家也再搜一下一定要看全面了

==============================================================================
我们刚才依次了解了五个拦截器的功能,接下来来看一下他们的存在方式和调用
在RealCall这个类里面有一行代码十分重要

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

不难看出,首先创建了一个集合用来存储适配器,,第一个addAll(client.interceptors())便是添加了我们的自定义适配器,后面是五个适配器依次加入,最终生成了一个Chain,也就是拦截器链
在这个方法的最后,调用了chain的proceed方法,不管是从英文水平上来说还是用脚指头想,这个方法肯定是要遍历轮询拦截器让他们干活了,我们再来看一下proceed方法

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpCodec, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
  }

我们在这一堆判断和处理挑出了两行行亮眼而又亮眼的代码

Interceptor interceptor = interceptors.get(index);
Response response = interceptor.intercept(next);

不是说client的一连串点方法叫链式结构,在这里链式结构被解释的淋漓尽致,通过获取下标的方式执行当前拦截器的interceptor方法并从其下一个拦截器中拿到返回回来的response,至此调用和轮询部分也结束了

再来说说response颠沛流离的一生

首先次对象在重定向拦截器总被创建但赋值为null

在经历缓存拦截器时会根据缓存策略,若缓存中存在则直接生成response并返回,若没有则继续向下
在网络拦截器中被请求并真正意义上的赋值,但请求的到的是io写下来的网络字节

然后response通过一层层拦截器传回到桥接拦截器进行gzip解压,这也是为什么桥接拦截器在缓存拦截器之前的原因之一(gzip不了解的小伙伴建议百度一下,多懂一些知识总没坏处)

最终经历了一整个拦截器链得到的一个带有我们所需要信息的response,我们再写一个线程调度将请求结果返给ui线程进行使用

关于okhttp的dispatcher部分后面我会再详细的写一篇,为什么不在这篇中一起讲解了呢?

原因简单而现实 因为我快下班了hhhhhhhhhhhh
在这里插入图片描述
当然我也会尽快抽时间将dispatcher的部分也做出来,讲当然是讲全套的,相信有耐心一点点看到这里的人个个都是彭于晏胡歌吴彦组吧,说不定里面可能还混着一两个范冰冰杨幂
在这里插入图片描述
那今天就先到这里啦

**

INTERESTING!!!

**

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值