面试题android

(面试必备-源码系列)OkHttp3

2019-01-18  本文已影响183人  蓝师傅_Android

一般面试的时候,只要简历写了开源库,面试官一般都会问源码,所以如何读源码,如何应对面试中的源码问题呢?
今天开始分析OkHttp3源码,希望对大家有所帮助。

首先,带着问题看源码

1.分析OkHttp请求流程
2.知道请求流程之后,拦截链的各个拦截器分析

一、请求流程之拦截链

okhttp发一个同步请求的流程

OkHttpClient client = new OkHttpClient();
Request.Builder builder = new Request.Builder().url("http:.//www.baidu.com");
Request request = builder.build();
//前面是参数构造,主要是下面这一句一句
Response response = client.newCall(request).execute();
RealCall:请求的真正执行者
// class:OkHttpClient
@Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
//class:RealCall
@Override public Response execute() throws IOException {
    ...
    try {
      //将RealCall加到队列 Deque<RealCall> runningSyncCalls
      client.dispatcher().executed(this);
      //通过一系列的拦截器请求处理和响应处理得到最终的返回结果
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    }
    ...
  }
  
  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //在配置 OkHttpClient 时设置的 interceptors,放在第一位
    interceptors.addAll(client.interceptors());
    // 负责失败重试以及重定向
    interceptors.add(retryAndFollowUpInterceptor);
    // 桥拦截器,添加一些Header和移除一些header,例如body的contentLength是-1,则移除Content-Length这个header
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    // 负责读取缓存直接返回、更新缓存
    interceptors.add(new CacheInterceptor(client.internalCache()));
    // 负责和服务器建立连接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    // 配置 OkHttpClient 时设置的 networkInterceptors
      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);
  }

可以看到这个调用链的流程是这样的,

  1. 自定义的 interceptor
  2. retryAndFollowUpInterceptor
  3. BridgeInterceptor(client.cookieJar())
  4. CacheInterceptor(client.internalCache())
  5. ConnectInterceptor(client)
  6. client.networkInterceptors() --如果有设置的话
  7. CallServerInterceptor(forWebSocket)

看下chain.proceed 的内部实现,

//class:RealInterceptorChain
...
//自定义拦截链intercept方法处理完后要调用chain.proceed(request)让拦截链继续下去
@Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }
  
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
//取出下一个拦截器,调用它的intercept方法
    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是一个负责管理拦截链的类,每个拦截器调用chain.proceed(request),就会走到下一个拦截器的 intercept方法

拦截链最底部的拦截器是 CallServerInterceptor(后面有分析),用okio请求网络返回了Response,然后Response往这个拦截链回传,上一个拦截器通过 response = chain.proceed(request);就获取到response了

小结:

Okhttp3通过拦截链的设计,每个拦截器各司其职,我们可以自定义拦截链,像打印日志,统一添加header等等,都是通过添加拦截器实现。

二 、分析下各个拦截器

1.RetryAndFollowUpInterceptor: 失败重试和重定向拦截器
public Response intercept(Chain chain) throws IOException {
    while (true) {
        ...
        response = realChain.proceed(request, streamAllocation, null, null);
        ...
        //followUpRequest 判断响应中是否有失败或者重定向(Location)标志,失败的话返回response.request,重定向的话构造新的Request返回
        Request followUp = followUpRequest(response, streamAllocation.route());
        if (followUp == null) {
            //如果没有失败或重定向,返回response
            return response;
        }
        ...
        //用新重定向的request继续while走拦截链
        request = followUp;
        priorResponse = response;
      }
        
    }
}
2. BridgeInterceptor 桥拦截器,比较简单,添加或者移除一些header
public Response intercept(Chain chain) throws IOException {
    ...
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        // contentType 不空就加Content-Type这个header
        requestBuilder.header("Content-Type", contentType.toString());
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        //contentLength 不是-1就加Content-Length这个header
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        //移除Content-Length 这个header
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }
    ...
    //后面还有响应的header
}
3. CacheInterceptor 缓存拦截器,根据缓存策略,判断是否返回缓存数据,响应的数据是否要缓存起来
public Response intercept(Chain chain) throws IOException {
    //缓存策略
    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.
    //如果被禁止试用网络,同时缓存为空,则构造一个失败的Response返回
    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);
    }
    //之后网络返回之后的策略
    // 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;
      }
    }
}
4. ConnectInterceptor 连接池

intercept只有几行代码,分析一下

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

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
  
  //
  public HttpCodec newStream(
     ...
    try {
    //顾名思义,就是找一个连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }
  
  //获取连接的方法
   private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
      boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          pingIntervalMillis, connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

总结一下,ConnectInterceptor主要是从连接池去取连接,http请求要先3次握手才能建立连接,复用连接可以免去握手的时间。

5. 最后一个 CallServerInterceptor 发送和接收数据
//class:CallServerInterceptor
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();

    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);
    
    ...
    //最终获取response方法 httpCodec.openResponseBody(response)
    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build
    //拦截链已经到底部,直接返回 response,将response回传给上一个拦截器
    return response;
}

最终获取response方法 httpCodec.openResponseBody(response),
httpCodec的实现有两个,Http1Codec 和 Http2Codec,看下Http1Codec,Http2Codec原理一样

//class:Http1Codec
public ResponseBody openResponseBody(Response response) throws IOException {
    ...
    //内部就是通过Okio.buffer去请求网络
    return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
  }

好了,以上就是OKHttp3的源码分析(各个拦截器点到为止,详细可以按照文中的分析流程,自己去看源码),如果文中有什么地方没有表达清楚可以跟我留言交流。

三、面试怎么答?

在面试中问到OKHttp3源码,我们可以这样回答:

OKHttp3通过拦截链的设计,让请求分成5个拦截器去处理,拦截器各司其职,扩展性非常高。拦截链是从自定义的拦截器开始,然后再到默认的5个拦截器。一般情况下我们想打印网络请求日志,所以可以自定义Log拦截器,如果要给所有请求添加Header,同样可以自定义Header拦截器。

面试官接着会问5个默认拦截器分别是什么,这时候我们就可以很轻易说出5个拦截器

1.失败重试、重定向拦截器。
2.桥拦截器:主要是添加和删除一些header
3.缓存拦截器:根据缓存策略,如果缓存可用,直接返回缓存数据。
4.连接池拦截器:连接池会缓存http链接,连接池的好处是复用连接,少了3次握手,所以请求会更快
5.真正访问网络的拦截器:内部使用okio去发请求

到这里,面试官可能问某一个拦截器的细节,例如延伸到okio的内部原理,就是NIO,跟IO之间的对比,这个自己再查下资料。

更多文章,敬请期待

上一篇 下一篇

猜你喜欢

热点阅读