zoukankan      html  css  js  c++  java
  • 吊打面试官——史上最详细【OkHttp】 三

     简介:大三学生党一枚!主攻Android开发,对于Web和后端均有了解。

    个人语录:取乎其上,得乎其中,取乎其中,得乎其下,以顶级态度写好一篇的博客。

    前面已经简单的介绍了拦截器的概念和每一种拦截器的作用,凭借这一点还不足以打动面试官,还需要对每一个拦截器的源码有所了解,才能够扛住面试官的各种问题!

    在这里插入图片描述

    @TOC

    1.RetryAndFollowUpInterceptor

    1.1 源码分析

    我们知道拦截器链执行procced方法执行拦截器链中的每一个拦截器,拦截器则调用自身的intercept方法执行,所以我们只需要对intercept进行分析就可以了。

    @Override 
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();//从拦截器链获得原始的request请求,请求信息包含了url,携带的请求体等,如果需要重定向,可能会对Request做修改更新
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Call call = realChain.call();//获得call请求的原始对象,下面会用到
        EventListener eventListener = realChain.eventListener();
        //创建StreamAllocation对象,他主要负责分配stream,这个对象在这里创建,却没有被使用
        //他在后面的拦截会被使用,很重要
        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(request.url()), call, eventListener, callStackTrace);
    	
        this.streamAllocation = streamAllocation;
    
        int followUpCount = 0;//重试次数,默认最大重试次数是20
        Response priorResponse = null;//这个priorResponse记录之前一次请求返回的Response
        while (true) {
          if (canceled) {
    	  	//如果已经取消该请求,立即释放streamAllocation,结束该请求,后面的拦截器都不会执行了
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    
          Response response;//这个Resonse记录每次请求返回的response,
          //就是根据response的返回结果判断是否需要重试的
          boolean releaseConnection = true;//是否要释放connection的标志位
          try {
            response = realChain.proceed(request, streamAllocation, null, null);//调用下一个拦截器获得Response
            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) {
    	  	//捕捉到IO异常
            // 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.
          //不保存响应体,所以body为null
          if (priorResponse != null) {
    	  	//保存最新的Response,第一次执行请求这个if判断不会被执行
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                        .body(null)
                        .build())
                .build();
          }
    
          Request followUp;//如果followUp为空表示没必要进行重试,直接返回response
          try {
          //followUpRequest是判断有没有必要进行重试的方法,他需要根据response进行判断得到followUp
            followUp = followUpRequest(response, streamAllocation.route());
          } catch (IOException e) {
            streamAllocation.release();
            throw e;
          }
    
          if (followUp == null) {
          //如果followUp为空表示没必要进行重试,直接返回response
            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())) {
    	  	//比较是否是同一个host,port,schema
            streamAllocation.release();//如果不是,先释放原来建立好的连接
            streamAllocation = new StreamAllocation(client.connectionPool(),
                createAddress(followUp.url()), call, eventListener, callStackTrace);
                //获取最新followUp请求中指定的url建立流
            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;//记录最新的Response
        }
      }

    1.2 工作原理

    这些分析需要看懂才能明白RetryAndFollowUpInterceptor拦截器的工作原理!

    看完源代码以后,我们来简单梳理一下RetryAndFollowUpInterceptor的工作原理。

    原理:当RetryAndFollowUpInterceptor拦截器指定intercept方法时,第一次执行时,会调用后面的拦截器链获得返回的Response,然后根据Response中的信息,最主要的就是状态码,重试次数,请求体是否允许重复请求,决定是否需要进行重新连接,既然要进行重新请求,那么有可能会对url进行改变,如果改变就不能使用之前建立好的stream,需要重新建立。根据状态码判断如果不需要重新连接,则该请求直接返回Response,工作结束!

    2.BridgeInterceptor

    当在RetryAndFollowUpInterceptor拦截器中调用拦截器链的执行方法时,将会被执行的就是BridgeInterceptor拦截器,先从源码分析再讲解原理.

    2.1 源码分析

    根据之前说的BridgeInterceptor的作用,就是把用户传来的Request转换成符合网络请求格式的Request,把网络返回的Response(可能被压缩过)转换成用户可以用的Response,所以该拦截器就是对RequestResponse做操作

    @Override 
     public Response intercept(Chain chain) throws IOException {
        Request userRequest = chain.request();
        Request.Builder requestBuilder = userRequest.newBuilder();
        //requestBuilder就是根据用户传来的Request创建一个请求构造,添加上一些请求字段
        RequestBody body = userRequest.body();//获取请求体,一般请求体不需要被改变
        if (body != null) {
          MediaType contentType = body.contentType();
          if (contentType != null) {
           //添加Content-type字段
            requestBuilder.header("Content-Type", contentType.toString());
          }
    
          long contentLength = body.contentLength();
          if (contentLength != -1) {
          //添加content-length字段
            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) {
        //添加host字段
          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; //是否支持Gzip压缩的标志位
        if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
        //如果用户的接受编码为空,也就是对接受Response的编码没有要求,则允许Gzip压缩编码
          transparentGzip = true;
          requestBuilder.header("Accept-Encoding", "gzip");
        }
    
        List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
        //支持cookies
        if (!cookies.isEmpty()) {
          requestBuilder.header("Cookie", cookieHeader(cookies));
        }
    
        if (userRequest.header("User-Agent") == null) {
        //指定发起请求的平台,引擎,版本号等信息
          requestBuilder.header("User-Agent", Version.userAgent());
        }
    
       //对Reuqest的设置好了,再接着调用下一个拦截器获得Response,然后对Response进行处理返回给用户
        Response networkResponse = chain.proceed(requestBuilder.build());
    
        HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    
    //着手根据网络返回的Response构建返回给用户的Response
        Response.Builder responseBuilder = networkResponse.newBuilder()
            .request(userRequest);
    
        if (transparentGzip
            && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
            && HttpHeaders.hasBody(networkResponse)) {
            //这是个复杂的判断逻辑,首先就是我们允许了Gzip编码,
           // 第二个是响应的内容编码确实Gzip编码,第三个就是Response有返回体,
           //任何一个条件不成立都没必要执行这一步
          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)));
        }
        //返回最后处理好的Response,retryAndFollowUpInterceptor就是根据此结果
        //判断是否要重试。
        return responseBuilder.build();
      }

    2.2 工作原理

    BridgeInterceptor原理:对用户传入的请求做处理,在程序员编写代码时只需要指定Url和请求体,但是事实上,一个完整的请求报文远不止这些信息,BridgeInterceptor帮我们做了这些事,添加各种请求报文所需要的字段,然后再传递给下一个拦截器去执行。对于得到的返回结果Response,其内容可能经过了Gzip压缩,所以BridgeInterceptor帮我们做了解压缩。这像极了爱情!!!

    3.CacheInterceptor

    CacheInterceptor的的作用就是缓存网络请求,注意只能缓存GET类型的请求。他是怎么实现缓存的呢?看一下源码一探究竟。

    3.1 源码解析

    @Override
     public Response intercept(Chain chain) throws IOException {
     //cache是构建缓存拦截器时指定的
     //根据Request的url判断是否有缓存
        Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
        long now = System.currentTimeMillis();
          //缓存策略,会有详细介绍
          //当有请求到达时,需要判断该请求是否有缓存,该缓存是否可用,依次构建策略
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        //如果networkRequest==null,就调用缓存
        Request networkRequest = strategy.networkRequest;
        //如果cacheResponse==null,就是用网络请求
        Response cacheResponse = strategy.cacheResponse;
        //如果两者都为null,则返回fail
        if (cache != null) {
        //这里的cache是在我们创建OkhttpClient时指定的
          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 {
        //调用下一个拦截器获取Response
          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 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);
            //这一步是真正的向缓存中添加Response
            return cacheWritingResponse(cacheRequest, response);
          }
    
          if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
              cache.remove(networkRequest);
            } catch (IOException ignored) {
              // The cache cannot be written.
            }
          }
        }
    
        return response;
      }

    CacheInterceptor的源码有点乱,需要多看几遍,这里在说一下缓存策略以及缓存的存取

    缓存策略作用的描述

    Given a request and cached response, 
    this figures out whether to use the network, the cache, or  both.

    给定一个请求和缓存的Response,他能够帮助我们决定是否使用网络,使用缓存或者都是。

    重要代码展示:如何通过传入请求和缓存得到策略的

    private CacheStrategy getCandidate() {
          // No cached response.
          if (cacheResponse == null) {
          //如果没有缓存,则进行网络请求
            return new CacheStrategy(request, null);
          }
    
          // Drop the cached response if it's missing a required handshake.
          if (request.isHttps() && cacheResponse.handshake() == null) {
          //如果是https请求并且握手信息丢失,也需要进行网络请求
            return new CacheStrategy(request, null);
          }
    
          // If this response shouldn't have been stored, it should never be used
          // as a response source. This check should be redundant as long as the
          // persistence store is well-behaved and the rules are constant.
          if (!isCacheable(cacheResponse, request)) {
          //判断请求是否可以被缓存,如果不可以,直接使用网络
            return new CacheStrategy(request, null);
          }
    
        CacheControl requestCaching = request.cacheControl();
          if (requestCaching.noCache() || hasConditions(request)) {
            return new CacheStrategy(request, null);
          }
    
          CacheControl responseCaching = cacheResponse.cacheControl();
    //从这里开始
          long ageMillis = cacheResponseAge();
          long freshMillis = computeFreshnessLifetime();
    
          if (requestCaching.maxAgeSeconds() != -1) {
            freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
          }
    
          long minFreshMillis = 0;
          if (requestCaching.minFreshSeconds() != -1) {
            minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
          }
    
          long maxStaleMillis = 0;
          if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
            maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
          }
    //到这里结束,就是为了判断缓存是否过期,具体细节无需要关注。
          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());
          }
    
          // Find a condition to add to the request. If the condition is satisfied, the response body
          // will not be transmitted.
          //流程走到这,说明缓存已经过期了
          //添加请求头:If-Modified-Since或者If-None-Match
          //etag与If-None-Match配合使用
          //lastModified与If-Modified-Since配合使用
          //前者和后者的值是相同的
          //区别在于前者是响应头,后者是请求头。
          //后者用于服务器进行资源比对,看看是资源是否改变了。
          // 如果没有,则本地的资源虽过期还是可以用的
          String conditionValue;
          if (etag != null) {
          //默认是null的
            conditionName = "If-None-Match";
            conditionValue = etag;
          } else if (lastModified != null) {
          //The last modified date of the cached response, if known.
            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);
        }

    缓存策略并不复杂,还有缓存的存取采用的是DiskLru,根据urlmd5hex值作为键去存取,想要深入的可以看一下DiskLruCahce的源码。

    3.2 工作原理

    原理:底层是基于DiskLruCache,对于一个请求,
    1.没有缓存,直接网络请求;
    2.如果是https,但没有握手,直接网络请求;
    3.不可缓存,直接网络请求;
    4.请求头nocache或者请求头包含If-Modified-Since或者If-None-Match,则需要服务器验证本地缓存是不是还能继续使用,直接网络请求;
    5.可缓存,并且ageMillis + minFreshMillis < freshMillis + maxStaleMillis(意味着虽过期,但可用,只是会在响应头添加warning),则使用缓存;
    6.缓存已经过期,添加请求头:If-Modified-Since或者If-None-Match,进行网络请求;

    4.总结

    剩下两个拦截器会在下一篇博客讲解,最后还会有个对Okhttp总体的整理和相关的面试题!

    相关阅读:

    抖音涨粉

    抖音怎么看访客记录

    抖音没有浏览量

  • 相关阅读:
    cf D. Vessels
    cf C. Hamburgers
    zoj 3758 Singles' Day
    zoj 3777 Problem Arrangement
    zoj 3778 Talented Chef
    hdu 5087 Revenge of LIS II
    zoj 3785 What day is that day?
    zoj 3787 Access System
    判断给定图是否存在合法拓扑排序
    树-堆结构练习——合并果子之哈夫曼树
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13309132.html
Copyright © 2011-2022 走看看