開源框架之一第一篇:OKhttp解析,這些你又瞭解多少呢?

一、整體思路

從使用方法出發,首先是怎麼使用,其次是我們使用的功能在內部是如何實現的, 實現方案上有什麼技巧,有什麼範式。全文基本上是對 OkHttp 源碼的一個分析與 導讀,非常建議大家下載 OkHttp 源碼之後,跟着本文,過一遍源碼。對於技巧和 範式,由於目前我的功力還不到位,分析內容沒多少,歡迎大家和我一起討論。

本部分內容是關於Android進階的一些知識總結,涉及到的知識點比較雜,不過都 是面試中幾乎常問的知識點,也是加分的點。 關於這部分內容,可能需要有一些具體的項目實踐。在面試的過程中,結合具體自 身實踐經歷,才能更加深入透徹的描繪出來

相關內容後續GitHub更新,想衝擊金三銀四的小夥伴可以找找看看,歡迎star
順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我聯繫我獲取

首先放一張完整流程圖(看不懂沒關係,慢慢往後看):

二、基本用例

來自OKHTTP官方網站

2.1.創建 OkHttpClient 對象
  OkHttpClient client = new OkHttpClient();

咦,怎麼不見 builder?莫急,且看其構造函數:

  public OkHttpClient() { 
    this(new Builder()); 
  }

原來是方便我們使用,提供了一個“快捷操作”,全部使用了默認的配 置。 OkHttpClient.Builder 類成員很多,後面我們再慢慢分析,這裏先暫時略 過:

  public Builder() { 
    dispatcher = new Dispatcher(); 
    protocols = DEFAULT_PROTOCOLS; 
    connectionSpecs = DEFAULT_CONNECTION_SPECS; 
    proxySelector = ProxySelector.getDefault(); 
    cookieJar = CookieJar.NO_COOKIES; 
    socketFactory = SocketFactory.getDefault(); 
    hostnameVerifier = OkHostnameVerifier.INSTANCE; 
    certificatePinner = CertificatePinner.DEFAULT; 
    proxyAuthenticator = Authenticator.NONE; 
    authenticator = Authenticator.NONE; 
    connectionPool = new ConnectionPool(); 
    dns = Dns.SYSTEM; 
    followSslRedirects = true; 
    followRedirects = true; 
    retryOnConnectionFailure = true; 
    connectTimeout = 10_000; 
    readTimeout = 10_000; 
    writeTimeout = 10_000; 
  }
2.2.發起 HTTP 請求
  String run(String url) throws IOException { 
    Request request = new Request.Builder()
       .url(url) 
       .build(); 
  Response response = client.newCall(request).execute(); 
  return response.body().string(); 
  }

OkHttpClient 實現了 Call.Factory ,負責根據請求創建新的 Call 。
那我們現在就來看看它是如何創建 Call 的:

  /**
    * Prepares the {@code request} to be executed at some point in the future. 
    */ 
  @Override public Call newCall(Request request) { 
    return new RealCall(this, request); 
  }

如此看來功勞全在 RealCall 類了,下面我們一邊分析同步網絡請求的過程,一邊 瞭解 RealCall 的具體內容。

2.2.1.同步網絡請求

我們首先看 RealCall#execute

  @Override public Response execute() throws IOException { 
    synchronized (this) { 
      if (executed) throw new IllegalStateException("Already Execu ted"); 
          // (1) 
      executed = true; 
    }
    try { 
      client.dispatcher().executed(this); 
         // (2) 
      Response result = getResponseWithInterceptorChain(); 
        // (3) 
      if (result == null) throw new IOException("Canceled"); 
      return result; 
    } finally { 
    client.dispatcher().finished(this); 
       // (4) 
    } 
   }

這裏我們做了 4 件事:

  1. 檢查這個 call 是否已經被執行了,每個 call 只能被執行一次,如果想要一個完 全一樣的 call,可以利用 call#clone 方法進行克隆。
  2. 利用 client.dispatcher().executed(this) 來進行實際執 行 dispatcher 是剛纔看到的 OkHttpClient.Builder 的成員之一,它的文 檔說自己是異步 HTTP 請求的執行策略,現在看來,同步請求它也有摻和。
  3. 調用 getResponseWithInterceptorChain() 函數獲取 HTTP 返回結果,從 函數名可以看出,這一步還會進行一系列“攔截”操作。
  4. 最後還要通知 dispatcher 自己已經執行完畢。

dispatcher 這裏我們不過度關注,在同步執行的流程中,涉及到 dispatcher 的內容 只不過是告知它我們的執行狀態,比如開始執行了(調用 executed ),比如執行 完畢了(調用 finished ),在異步執行流程中它會有更多的參與。

真正發出網絡請求,解析返回結果的,還 是 getResponseWithInterceptorChain

   private Response getResponseWithInterceptorChain() throws IOExce ption { 
     // 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 (!retryAndFollowUpInterceptor.isForWebSocket()) { 
       interceptors.addAll(client.networkInterceptors()); 
     }
     interceptors.add(new CallServerInterceptor( 
         retryAndFollowUpInterceptor.isForWebSocket())); 
     Interceptor.Chain chain = new RealInterceptorChain( 
         interceptors, null, null, null, 0, originalRequest); 
     return chain.proceed(originalRequest); 
   }

InterceptorOkHttp 最核心的一個東西,不要誤以爲它只負責攔截請求 進行一些額外的處理(例如 cookie),實際上它把實際的網絡請求、緩存、透明壓 縮等功能都統一了起來,每一個功能都只是一個 Interceptor ,它們再連接成一 個 Interceptor.Chain ,環環相扣,最終圓滿完成一次網絡請求。

getResponseWithInterceptorChain 函數我們可以看 到 Interceptor.Chain 的分佈依次是:

  1. 在配置 OkHttpClient 時設置的 interceptors
  2. 負責失敗重試以及重定向的 RetryAndFollowUpInterceptor
  3. 負責把用戶構造的請求轉換爲發送到服務器的請求、把服務器返回的響應轉換 爲用戶友好的響應的 BridgeInterceptor
  4. 負責讀取緩存直接返回、更新緩存的 CacheInterceptor
  5. 負責和服務器建立連接的 ConnectInterceptor
  6. 配置 OkHttpClient 時設置的 networkInterceptors
  7. 負責向服務器發送請求數據、從服務器讀取響應數 據 CallServerInterceptor

在這裏,位置決定了功能,最後一個 Interceptor 一定是負責和服務器實際通訊的, 重定向、緩存等一定是在實際通訊之前的。 責任鏈模式在這個 Interceptor 鏈條中得到了很好的實踐。

它包含了一些命令對象和一系列的處理對象,每一個處理對象決定它能處理哪 些命令對象,它也知道如何將它不能處理的命令對象傳遞給該鏈中的下一個處 理對象。該模式還描述了往該處理鏈的末尾添加新的處理對象的方法。

對於把 Request 變成 Response 這件事來說,每個 Interceptor 都可能完成這 件事,所以我們循着鏈條讓每個 Interceptor 自行決定能否完成任務以及怎麼完 成任務(自力更生或者交給下一個 Interceptor )。這樣一來,完成網絡請求這 件事就徹底從 RealCall 類中剝離了出來,簡化了各自的責任和邏輯。兩個字:優 雅!

責任鏈模式在安卓系統中也有比較典型的實踐,例如 view 系統對點擊事件 (TouchEvent)的處理。

回到 OkHttp,在這裏我們先簡單分析一 下 ConnectInterceptorCallServerInterceptor ,看看 OkHttp 是怎麼進 行和服務器的實際通信的。

2.2.1.1.建立連接: ConnectInterceptor
  @Override public Response intercept(Chain chain) throws IOExcept ion { 
     RealInterceptorChain realChain = (RealInterceptorChain) chain; 
     Request request = realChain.request(); 
     StreamAllocation streamAllocation = realChain.streamAllocation ();
     // We need the network to satisfy this request. Possibly for v alidating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GE T");
     HttpCodec httpCodec = streamAllocation.newStream(client, doExt ensiveHealthChecks); 
     RealConnection connection = streamAllocation.connection(); 
     return realChain.proceed(request, streamAllocation, httpCodec, connection); 
   }

實際上建立連接就是創建了一個 HttpCodec 對象,它將在後面的步驟中被使用, 那它又是何方神聖呢?它是對 HTTP 協議操作的抽象,有兩個實 現: Http1CodecHttp2Codec ,顧名思義,它們分別對應 HTTP/1.1 和 HTTP/2 版本的實現。

Http1Codec 中,它利用Okio對 Socket 的讀寫操作進行封裝,Okio 以後有機 會再進行分析,現在讓我們對它們保持一個簡單地認識:它 對 java.iojava.nio 進行了封裝,讓我們更便捷高效的進行 IO 操作。

而創建 HttpCodec 對象的過程涉及 到 StreamAllocationRealConnection ,代碼較長,這裏就不展開,這個過 程概括來說,就是找到一個可用的 RealConnection ,再利 用 RealConnection 的輸入輸出( BufferedSourceBufferedSink )創 建 HttpCodec 對象,供後續步驟使用。

2.2.1.2.發送和接收數據: CallServerInterceptor
  @Override public Response intercept(Chain chain) throws IOExcept ion {
  HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStrea m();
  StreamAllocation streamAllocation = ((RealInterceptorChain) ch ain).streamAllocation(); 
  Request request = chain.request();
  long sentRequestMillis = System.currentTimeMillis(); httpCodec.writeRequestHeaders(request);
  if (HttpMethod.permitsRequestBody(request.method()) && request .body() != null) {
    Sink requestBodyOut = httpCodec.createRequestBody(request, r equest.body().contentLength());
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody); 
    bufferedRequestBody.close();
  }
    ......
    // 省略部分檢查代碼
    return response; 
  }

我們抓住主幹部分:

  1. 向服務器發送 request header;
  2. 如果有 request body,就向服務器發送;
  3. 讀取 response header,先構造一個 Response 對象;
  4. 如果有 response body,就在 3 的基礎上加上 body 構造一個新 的 Response 對象;

這裏我們可以看到,核心工作都由 HttpCodec 對象完成,而 HttpCodec實際上 利用的是 Okio,而 Okio 實際上還是用的 Socket ,所以沒什麼神祕的,只不過一 層套一層,層數有點多。

其實 Interceptor 的設計也是一種分層的思想,每個 Interceptor 就是一層。 爲什麼要套這麼多層呢?分層的思想在 TCP/IP 協議中就體現得淋漓盡致,分層簡 化了每一層的邏輯,每層只需要關注自己的責任(單一原則思想也在此體現),而 各層之間通過約定的接口/協議進行合作(面向接口編程思想),共同完成複雜的任 務

簡單應該是我們的終極追求之一,儘管有時爲了達成目標不得不復雜,但如果有另 一種更簡單的方式,我想應該沒有人不願意替換。

2.2.2.發起異步網絡請求
  client.newCall(request).enqueue(new Callback() { 
     @Override 
     public void onFailure(Call call, IOException e) { 
     }
     @Override 
     public void onResponse(Call call, Response response) throws IOException { 
       System.out.println(response.body().string()); 
     } 
   }); 
   // RealCall#enqueue 
   @Override public void enqueue(Callback responseCallback) { 
       synchronized (this) { 
         if (executed) throw new IllegalStateException("Already Execu ted");
          executed = true; 
       }
       client.dispatcher().enqueue(new AsyncCall(responseCallback)); 
   }
   // Dispatcher#enqueue 
   synchronized void enqueue(AsyncCall call) { 
        if (runningAsyncCalls.size() < maxRequests && runningCallsForH ost(call) < maxRequestsPerHost) { 
          runningAsyncCalls.add(call); executorService().execute(call); 
        } else { 
          readyAsyncCalls.add(call); 
        } 
   }

這裏我們就能看到 dispatcher 在異步執行時發揮的作用了,如果當前還能執行一個 併發請求,那就立即執行,否則加入 readyAsyncCalls 隊列,而正在執行的請求 執行完畢之後,會調用 promoteCalls() 函數,來把 readyAsyncCalls 隊列中 的 AsyncCall “提升”爲 runningAsyncCalls ,並開始執行。

這裏的 AsyncCallRealCall 的一個內部類,它實現了 Runnable ,所以可 以被提交到 ExecutorService 上執行,而它在執行時會調 用 getResponseWithInterceptorChain() 函數,並把結果通 過 responseCallback 傳遞給上層使用者。

這樣看來,同步請求和異步請求的原理是一樣的,都是 在 getResponseWithInterceptorChain() 函數中通過 Interceptor 鏈條來實 現的網絡請求邏輯,而異步則是通過 ExecutorService 實現。

2.3返回數據的獲取

在上述同步( Call#execute() 執行之後)或者異步 ( Callback#onResponse() 回調中)請求完成之後,我們就可以 從 Response 對象中獲取到響應數據了,包括 HTTP status code,status message,response header,response body 等。這裏 body 部分最爲特殊,因爲 服務器返回的數據可能非常大,所以必須通過數據流的方式來進行訪問(當然也提 供了諸如 string()bytes() 這樣的方法將流內的數據一次性讀取完畢),而 響應中其他部分則可以隨意獲取。

響應 body 被封裝到 ResponseBody 類中,該類主要有兩點需要注意:

  1. 每個 body 只能被消費一次,多次消費會拋出異常;
  2. body 必須被關閉,否則會發生資源泄漏;

在2.2.1.2.發送和接收數據:CallServerInterceptor小節中,我們就看過了 body 相 關的代碼:

  if (!forWebSocket || response.code() != 101) { 
     response = response.newBuilder() 
        .body(httpCodec.openResponseBody(response)) 
        .build(); 
  }

HttpCodec#openResponseBody 提供具體 HTTP 協議版本的響應 body, 而 HttpCodec 則是利用 Okio 實現具體的數據 IO 操作。

這裏有一點值得一提,OkHttp 對響應的校驗非常嚴格,HTTP status line 不能有任 何雜亂的數據,否則就會拋出異常,在我們公司項目的實踐中,由於服務器的問 題,偶爾 status line 會有額外數據,而服務端的問題也毫無頭緒,導致我們不得不 忍痛繼續使用 HttpUrlConnection,而後者在一些系統上又存在各種其他的問題, 例如魅族系統發送 multi-part form 的時候就會出現沒有響應的問題。

2.4.HTTP 緩存

在2.2.1.同步網絡請求小節中,我們已經看到了 Interceptor 的佈局,在建立連 接、和服務器通訊之前,就是 CacheInterceptor ,在建立連接之前,我們檢查 響應是否已經被緩存、緩存是否可用,如果是則直接返回緩存的數據,否則就進行 後面的流程,並在返回之前,把網絡的數據寫入緩存。 這塊代碼比較多,但也很直觀,主要涉及 HTTP 協議緩存細節的實現,而具體的緩 存邏輯 OkHttp 內置封裝了一個 Cache 類,它利用 DiskLruCache ,用磁盤上的 有限大小空間進行緩存,按照 LRU 算法進行緩存淘汰,這裏也不再展開。 我們可以在構造 OkHttpClient 時設置 Cache 對象,在其構造函數中我們可以指 定目錄和緩存大小:

  public Cache(File directory, long maxSize);

而如果我們對 OkHttp 內置的 Cache 類不滿意,我們可以自行實 現 InternalCache 接口,在構造 OkHttpClient 時進行設置,這樣就可以使用 我們自定義的緩存策略了。

三、總結 OkHttp

還有很多細節部分沒有在本文展開,例如 HTTP2/HTTPS 的支持等,但建 立一個清晰的概覽非常重要。對整體有了清晰認識之後,細節部分如有需要,再單 獨深入將更加容易。 在文章最後我們再來回顧一下完整的流程圖:

  • OkHttpClient 實現 Call.Factory ,負責爲 Request 創建 Call ;
  • RealCall 爲具體的 Call 實現,其 enqueue() 異步接口通 過 Dispatcher 利用 ExecutorService 實現,而最終進行網絡請求時和同 步 execute() 接口一致,都是通 過 getResponseWithInterceptorChain() 函數實現;
  • getResponseWithInterceptorChain() 中利用 Interceptor 鏈條,分層 實現緩存、透明壓縮、網絡 IO 等功能;

相關內容後續GitHub更新,想衝擊金三銀四的小夥伴可以找找看看,歡迎star
順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我聯繫我獲取

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