學會這篇OkHttp,花了我一個通宵,也是值了!

引子

OkHttp 知名第三方網絡框架SDK,使用簡單,性能優秀,但是內核並不簡單,此係列文章,專挑硬核知識點詳細講解。何爲硬核,就是要想深入研究,你絕對繞不過去的知識點。

TIPS:聲明:攔截器種細節太多,要一一講解不太現實,所以我挑了其中最實用的一些要點加以總結。

詳細講解 OKHttp的核心內容,攔截器。不過攔截器衆多,有系統自帶的,也有我們可以自己去自定義的。

大家可以先看首篇-你必須學會的OKHttp
順手留下GitHub鏈接,需要獲取相關面試或者面試寶典核心筆記PDF等內容的可以自己去找
https://github.com/xiangjiana/Android-MS

更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我聯繫我獲取


這是網絡請求執行的核心方法的起點,這裏涉及了衆多攔截器。

正文大綱

系統自帶攔截器

1 重試與重定向攔截器 RetryAndFollowUpInterceptor
2 橋接攔截器
3 緩存攔截器 CacheInterceptor
4 連接攔截器 ConnectInterceptor
5 服務調用攔截器 CallServerInterceptor

正文

在詳解攔截器之前,有必要先將 RealCallgetResponseWithInterceptorChain() 方法最後兩行展開說明:

  Interceptor.Chain chain = newRealInterceptorChain( interceptors, null, null, null, 0, originalRequest);
  return chain.proceed(originalRequest);

這裏最終返回 一個 Response,進入 chain.proceed方法,最終索引到 RealInterceptorChain的 proceed方法:

之後,我們追蹤這個 interceptor.intercept(next); ,發現是一個接口,找到實現類,有多個,進入其中的 RetryAndFollowUpInterceptor,發現:

它這裏又執行了 chain.proceed,於是又回到了 RealInterceptorChain.proceed()方法,但是此時,剛纔鏈條中的攔截器已經不再是原來的攔截器了,而是變成了第二個,因爲每一次都 index+1了(這裏比較繞,類似遞歸,需要反覆仔細體會),依次類推,直到所有攔截器的intercept方法都執行完畢,直到鏈條中沒有攔截器。就返回最後的 Response

這一段是 okhttp責任鏈模式的核心,應該好理解

系統自帶攔截器

1. 重試與重定向攔截器 RetryAndFollowUpInterceptor

先說結論吧:

顧名思義,retry 重試,FollowUp 重定向 。這個攔截器處在所有攔截器的第一個,它是用來判定要不要對當前請求進行重試和重定向的,
那麼我們應該關心的是: 什麼時候重試, 什麼時候重定向。並且,它會判斷用戶有沒有取消請求,因爲RealCall中有一個cancel方法,可以支持用戶 取消請求(不過這裏有兩種情況,在請求發出之 前取消,和 在之 後取消。如果是在請求之 前取消,那就直接不執行之後的過程,如果是在請求發出去之 後取消,那麼客戶端就會丟棄這一次的 response

重試
RetryAndFollowUpInterceptor的核心方法 interceptor() :

  @Override public Response intercept(Chain chain) throws IOException {
    ...省略
    while (true) {
      ...省略
      try {
        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.
        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.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      }
      ...省略
      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
      ...省略

    }
  }

上面的代碼中,我只保留了關鍵部分。其中有兩個continue,一個return.
當請求到達了這個攔截器,它會進入一個 while(true)循環,

當發生了 RouteException 異常(這是由於請求尚未發出去,路由異常,連接未成功),就會去判斷 recover方法的返回值,根據返回值決定要不要 continue.
當發生 IOException(請求已經發出去,但是和服務器通信失敗了)之後,同樣去判斷 recover方法的返回值,根據返回值決定要不要 continue.
如果這兩個 continue都沒有執行,就有可能走到最後的 returnresponse結束本次請求. 那麼 是不是要 重試,其判斷邏輯就在 recover()方法內部:

  private boolean recover(IOException e, StreamAllocation streamAllocation,
                            boolean requestSendStarted, Request userRequest) {
        streamAllocation.streamFailed(e);

        //todo 1、在配置OkhttpClient是設置了不允許重試(默認允許),則一旦發生請求失敗就不再重試
        //The application layer has forbidden retries.
        if (!client.retryOnConnectionFailure()) return false;

        //todo 2、由於requestSendStarted只在http2的io異常中爲false,http1則是 true,
        //在http1的情況下,需要判定 body有沒有實現UnrepeatableRequestBody接口,而body默認是沒有實現,所以後續instanceOf不成立,不會走return false.
        //We can't send the request body again.
        if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
            return false;

        //todo 3、判斷是不是屬於重試的異常
        //This exception is fatal.
        if (!isRecoverable(e, requestSendStarted)) return false;

        //todo 4、有沒有可以用來連接的路由路線
        //No more routes to attempt.
        if (!streamAllocation.hasMoreRoutes()) return false;

        // For failure recovery, use the same route selector with a new connection.
        return true;
    }

簡單解讀一下這個方法:

  • 如果okhttpClient已經set了不允許重試,那麼這裏就返回false,不再重試。
  • 如果requestSendStarted 只在http2.0的IO異常中是true,不過HTTP2.0還沒普及,先不管他,這裏默認通過。
  • 判斷是否是重試的異常,也就是說,是不是之前重試之後發生了異常。這裏解讀一下,之前重試發生過異常,拋出了Exception,這個 isRecoverable方法會根據這個異常去判定,是否還有必要去重試。
  • 協議異常,如果發生了協議異常,那麼沒必要重試了,你的請求或者服務器本身可能就存在問題,再重試也是白瞎。
  • 超時異常,只是超時而已,直接判定重試(這裏requestSendStartedhttp2纔會爲true,所以這裏默認就是false)
  • SSL異常,HTTPS證書出現問題,沒必要重試。
  • SSL握手未授權異常,也不必重試
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    // 出現協議異常,不能重試
    if (e instanceof ProtocolException) {
      return false;
    }

    // requestSendStarted認爲它一直爲false(不管http2),異常屬於socket超時異常,直接判定可以重試
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;    
    }

    // SSL握手異常中,證書出現問題,不能重試
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授權異常 不能重試
    if (e instanceof SSLPeerUnverifiedException) {
      return false;    }
    return true;
}

有沒有可以用來連接的路由路線,也就是說,如果當DNS解析域名的時候,返回了多個IP,那麼這裏可能一個一個去嘗試重試,直到沒有更多ip可用。

重定向
依然是 RetryAndFollowUpInterceptor的核心方法 interceptor() 方法,這次我截取後半段:

  public Response intercept(Chain chain) throws IOException {
     while (true) {
            ...省略前面的重試判定
            //todo 處理3和4xx的一些狀態碼,如301 302重定向
            Request followUp = followUpRequest(response, streamAllocation.route());
            if (followUp == null) {
                if (!forWebSocket) {
                    streamAllocation.release();
                }
                return response;
            }

            closeQuietly(response.body());

            //todo 限制最大 followup 次數爲20次
            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());
            }
            //todo 判斷是不是可以複用同一份連接
            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?");
            }
     }
 }

上面源碼中, followUpRequest() 方法中規定了哪些響應碼可以重定向:

  private Request followUpRequest(Response userResponse) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    Connection connection = streamAllocation.connection();
    Route route = connection != null
        ? connection.route()
        : null;
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      // 407 客戶端使用了HTTP代理服務器,在請求頭中添加 “Proxy-Authorization”,讓代理服務器授權
      case HTTP_PROXY_AUTH:
          Proxy selectedProxy = route != null
            ? route.proxy()
            : client.proxy();
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
        }
        return client.proxyAuthenticator().authenticate(route, userResponse);
      // 401 需要身份驗證 有些服務器接口需要驗證使用者身份 在請求頭中添加 “Authorization”
      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);
      // 308 永久重定向
      // 307 臨時重定向
      case HTTP_PERM_REDIRECT:
      case HTTP_TEMP_REDIRECT:
        // 如果請求方式不是GET或者HEAD,框架不會自動重定向請求
        if (!method.equals("GET") && !method.equals("HEAD")) {
          return null;
        }
      // 300 301 302 303
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // 如果用戶不允許重定向,那就返回null
        if (!client.followRedirects()) return null;
        // 從響應頭取出location
        String location = userResponse.header("Location");
        if (location == null) return null;
        // 根據location 配置新的請求 url
        HttpUrl url = userResponse.request().url().resolve(location);
        // 如果爲null,說明協議有問題,取不出來HttpUrl,那就返回null,不進行重定向
        if (url == null) return null;
        // 如果重定向在http到https之間切換,需要檢查用戶是不是允許(默認允許)
        boolean sameScheme =url.scheme().equals(userResponse.request().url().scheme());
        if (!sameScheme && !client.followSslRedirects()) return null;

        Request.Builder requestBuilder = userResponse.request().newBuilder();
        /**
         *  重定向請求中 只要不是 PROPFIND 請求,無論是POST還是其他的方法都要改爲GET請求方式,
         *  即只有 PROPFIND 請求才能有請求體
         */
        //請求不是get與head
        if (HttpMethod.permitsRequestBody(method)) {
          final boolean maintainBody = HttpMethod.redirectsWithBody(method);
           // 除了 PROPFIND 請求之外都改成GET請求
          if (HttpMethod.redirectsToGet(method)) {
            requestBuilder.method("GET", null);
          } else {
            RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
            requestBuilder.method(method, requestBody);
          }
          // 不是 PROPFIND 的請求,把請求頭中關於請求體的數據刪掉
          if (!maintainBody) {
            requestBuilder.removeHeader("Transfer-Encoding");
            requestBuilder.removeHeader("Content-Length");
            requestBuilder.removeHeader("Content-Type");
          }
        }

        // 在跨主機重定向時,刪除身份驗證請求頭
        if (!sameConnection(userResponse, url)) {
          requestBuilder.removeHeader("Authorization");
        }

        return requestBuilder.url(url).build();

      // 408 客戶端請求超時
      case HTTP_CLIENT_TIMEOUT:
        // 408 算是連接失敗了,所以判斷用戶是不是允許重試
           if (!client.retryOnConnectionFailure()) {
            return null;
        }
        // UnrepeatableRequestBody實際並沒發現有其他地方用到
        if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
            return null;
        }
        // 如果是本身這次的響應就是重新請求的產物同時上一次之所以重請求還是因爲408,那我們這次不再重請求了
        if (userResponse.priorResponse() != null
                       &&userResponse.priorResponse().code()==HTTP_CLIENT_TIMEOUT) {
            return null;
        }
        // 如果服務器告訴我們了 Retry-After 多久後重試,那框架不管了。
        if (retryAfter(userResponse, 0) > 0) {
            return null;
        }
        return userResponse.request();
       // 503 服務不可用 和408差不多,但是隻在服務器告訴你 Retry-After:0(意思就是立即重試) 才重請求
        case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
             return null;
         }

         if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
             return userResponse.request();
         }

         return null;
      default:
        return null;
    }
}

解讀一下這個方法,它根據拿到的response的內容,判斷他的響應碼,決定要不要返回一個新的request,如果返回了新的request,那麼外圍( 看RetryAndFollowUpInterceptorintercept方法)的 while(true)無限循環就會 使用新的request再次請求,完成重定向。細節上請查看上面代碼的註釋,來自一位高手,寫的很詳細。大概做個結論:

  • 響應碼 3XX 一般都會返回一個 新的Request,而另外的 return null就是不允許重定向。
  • followup最大發生20次

不過還是那句話,我們不是專門做網絡架構或者優化,瞭解到 這一個攔截器的基本作用,重要節點即可,真要摳細節,誰也記不了那麼清楚。

2. 橋接攔截器 BridgeInterceptor

這個可能是這5個當中最簡單的一個攔截器了,它從上一層RetryAndFollowUpInterceptor拿到 request之後,只做了一件事: 補全請求頭我們使用OkHttp發送網絡請求,一般只會 addHeader中寫上我們業務相關的一些參數,而 真正的請求頭遠遠沒有那麼簡單。服務器不只是要識別 業務參數,還要識別 請求類型,請求體的解析方式等,具體列舉如下:

它在補全了請求頭之後,交給下一個攔截器處理。在它得到響應之後,還會幹兩件事:
1、保存cookie,下一次同樣域名的請求就會帶上cookie到請求頭中,但是這個要求我們自己在okHttpClientCookieJar中實現具體過程。

如果使用gzip返回的數據,則使用GzipSource包裝便於解析。

3. 緩存攔截器 CacheInterceptor

本文只介紹他的作用,因爲內部邏輯太過複雜,必須單獨成文講解。

更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我聯繫我獲取

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章