拆輪子筆記 - OkHttp

前言

用了那麼久的OkHttp,解決了不少的聯網問題。對於熱門的好輪子,總不能一直停留在會用這個層面上吧,是時候動手拆拆輪子,學習一下其中的原理。本文主要記錄筆者通過各種網絡學習資源及[OkHttp源碼](“https://github.com/square/okhttp“)的過程,希望通過自身學習研究的過程,給其他同學提供一些參考與幫助,如有不足,懇請指教。
- 本文記錄基於 OkHttp 3.4.1 源碼的學習分析過程。
- 筆者水平有限,內容可能基於學習資源,當然也會有個人的一些見解加入其中,僅作個人筆記用途,同時也試圖探索學習如何入手拆輪子的好方法推薦給各位,如有侵權,馬上刪除。

學習資源

學習記錄

[跟隨Piasy 拆輪子(學習資源 - 第一篇)](“http://blog.piasy.com/2016/07/11/Understand-OkHttp/“)

閱讀心得:

  1. 從最實際的基本使用方法進行拓展、步步深入分析
  2. 沒有過多深入到細節進行解析,進階知識需要繼續鑽研
  3. 是帶領大家入手拆輪子的好文章。(強烈建議各位邊閱讀邊看源碼,更加有助於理解其中的實現方式!)
  4. 再次感謝 Piasy 大神

文章知識點:

  1. 關注 OkHttp 整體工作流程,結合源碼解析了“創建 OkHttpClient 對象”、“發起 HTTP 請求”、“同步網絡請求”、“異步網絡請求”等使用方法
  2. 詳解了其中應用的核心設計模式:責任鏈模式
  3. 分析了 OkHttp 如何“建立連接”、“發送和接收數據”、“發起異步網絡請求”、“獲取返回數據”、“Http緩存”

學習筆記:

這裏貼出筆者閱讀文章時,看源碼的順序與結合理解的註解。

  1. 從 OkHttp 創建對象使用方法入手
    OkHttpClient client = new OkHttpClient();
  2. 進入構造方法,發現內部會創建 Builder
//構造方法中已初始化 Builder
public OkHttpClient() {
  this(new Builder());
}
  1. 建造者模式?進入 OkHttpClient.Builder 構造方法一探究竟,直接創建的 OkHttpClient 會默認使用基本配置。
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;
}
  1. 接下來,看看發起 Http 請求 OkHttp 用法。
String run(String url) throws IOException {
  //構造請求體
  Request request = new Request.Builder()
      .url(url)
      .build(); 
  //發起請求核心代碼
  Response response = client.newCall(request).execute();
  return response.body().string();
}
  1. 方法解析:client.newCall(request) - 根據請求創建新的 Call 類
@Override public Call newCall(Request request) {
  //實際構造並返回 RealCall 對象
  return new RealCall(this, request);
}
  1. 方法解析:client.newCall(request).execute() - 執行請求
@Override 
public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

上述代碼主要做了4件事:
1. 判斷是否被執行 - 說明每個call只能被執行一次;另:可通過clone方法得到一個完全一樣的Call(該方法是 Object類的方法)
2. 利用client.dispatcher().executed(this)

 //dispatcher()方法返回 dispatcher ,異步http請求策略(內部使用 ExecutorService 實現)
 public Dispatcher dispatcher() {
    return dispatcher;
  }
  1. 調用 getResponseWithInterceptorChain() 獲取Http返回結果(InterceptorChain - 攔截鏈? 一系列攔截操作待分析)
//方法解析:構建一個完整的 interceptors List,最後利用該 list 構建 Interceptor.Chain
private Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //1.添加 client 攜帶的所有 interceptors (配置 OkHttpClient 時候用戶設置的 interceptors )
    interceptors.addAll(client.interceptors());
    //2.添加 retryAndFollowUpInterceptor (負責失敗重試以及重定性)
    interceptors.add(retryAndFollowUpInterceptor);
    //3.添加由 client.cookieJar() 構建的 BridgeInterceptor(負責把用戶構造的請求轉換爲發送到服務器的請求、把服務器返回的相應轉換爲用戶友好響應 - 即客戶端與服務器端溝通的橋樑)
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //4.添加由 client.internalCache() 構建的 CacheInterceptor (負責讀取緩存直接返回、更新緩存)
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //5.添加由 client 構建的 ConnectInterceptor (負責與服務器建立連接)
    interceptors.add(new ConnectInterceptor(client));
    //如果 forWebSocket 則添加 client 攜帶的所有 networkInterceptors(配置OkHttpClient 時候用戶設置的 networkInterceptors)
    if (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    //添加 CallServerInterceptor (負責向服務器發送給請求數據、從服務器讀取響應數據)
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));
    //構建 Interceptor.Chain ,最後調用 chain.proceed(originalRequest),第7點有解析
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }
  - **“責任鏈模式”科普(百度百科)**:在責任鏈模式裏,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,直到鏈上的某一個對象決定處理此請求。發出這個請求的客戶端並不知道鏈上的哪一個對象最終處理這個請求,這使得系統可以在不影響客戶端的情況下動態地重新組織和分配責任。
  - **“責任鏈模式”科普(維基百科)**:它包含了一些命令對象和一系列的處理對象,每一個處理對象決定它能處理哪些命令對象,它也知道如何將它不能處理的命令對象傳遞給該鏈中的下一個處理對象。該模式還描述了往該處理鏈的末尾添加新的處理對象的方法。
  - **總結**:攔截鏈中,每個Interceptor都可處理 Request,返回 Response。運行時,順着攔截鏈,讓每個Interceptor 自行決定是否處理以及怎麼處理(不處理則交給下一個Interceptor ),這樣,可將處理網絡請求從 RealCall 類中剝離,簡化了各自責任與邏輯
  - **另**:責任鏈模式 在 Android 有着許多典型應用,例:view的點擊事件分發(Android源碼設計模式一書中有提及)

4. dispatcher 如果try{}沒有拋出異常,並且 result != null(則不執行return,下面的finally才執行),最後還會通知 dispatcher 操作完成
7. 所以攔截鏈是如何工作的? 方法解析 - chain.proceed(originalRequest)

public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,
      Connection 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.httpStream != null && !sameConnection(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.httpStream != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }
    //然後再調用攔截鏈中的攔截器,最終得到 response
    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpStream, 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 (httpStream != 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");
    }
    //返回 response
    return response;
  }
  1. 明白攔截鏈的整體工作流程後,那麼 OkHttp 又如何與服務器進行實際通信的呢?這裏需要分析 CallServerInterceptor 攔截器。
//負責與目標服務器連接、將請求傳遞給下一個攔截器
/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;
  public ConnectInterceptor(OkHttpClient client) {
    this.client = client; 
  }
  //核心方法
  @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");
    //創建 HttpStream(接口) 對象這裏的實現類是 Http2xStream、Http1xStream 分別對應 HTTP/1.1 和 HTTP2 版本
    //兩者源碼有點長,需要交給讀者們自行深究,其中使用了 Okio 對 Socket 讀寫操作進行封裝
    //Okio 可暫時認爲是對 java.io、java.nio 進行封裝,提供更高效的IO操作
    HttpStream httpStream = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpStream, connection);
  }
}

另外,創建 HttpStream 對象還涉及 StreamAllocation、RealConnection 對象。
由於篇幅過長,這裏不貼出源碼,給出總體創建思路:找到可用的RealConnection,再利用 RealConnection 的輸入輸出(BufferedSource、BufferedSink)創建 HttpStream 對象。
9.接下來,來弄懂 OkHttp 如何發送、接收數據,需要分析攔截鏈中最後一個攔截器 CallServerInterceptor

//攔截鏈中最後一個攔截器,負責向服務器發送給請求數據、從服務器讀取響應數據
/** This is the last interceptor in the chain. It makes a network call to the server. */
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 {
    HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();
    long sentRequestMillis = System.currentTimeMillis();
    //1、寫入要發送的 Http Request Headers
    httpStream.writeRequestHeaders(request);
    //2、如果請求方法允許,且 request.body 不爲空,就加上一個body
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      //得到一個能傳輸 request body 的output stream
      Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
      //利用 Okio 將 requestBodyOut 寫入,得到 bufferedRequestBody
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      //將 request body 寫入到 bufferedRequestBody
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }
    //刷新 request 到 socket
    httpStream.finishRequest();
    //構造新的 Response 對象
    Response response = httpStream.readResponseHeaders()
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
    if (!forWebSocket || response.code() != 101) {
      response = response.newBuilder()
          .body(httpStream.openResponseBody(response))
          .build();
    }
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }
    int code = response.code();
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
  }
}
  • 核心工作基本由 HttpStream 完成(舊版本該類原名:HttpCodec ),利用了 Okio ,而 Okio 實際上還是使用了 Socket。
  • 分析(來源於Piasy 拆OkHttp):InterceptorChain 設計是一種分層思想,每層只關注自己的責任(單一責任原則),各層間通過約定的接口/協議進行合作,共同完成負責任務

    1. 初步學習了同步請求後,再從 OkHttp 異步網絡請求用法中入手 OkHttp 異步網絡請求的原理吧
//核心方法 - enqueue
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());
    }
});
  1. 進一步研究 RealCall.enqueue 方法
//源碼 - RealCall.enqueue
@Override 
public void enqueue(Callback responseCallback) {
  //同步鎖,如果已經執行會拋出異常
  synchronized (this) {
    if (executed) throw new IllegalStateException("Already Executed");
    executed = true;
  }
  //關鍵調用 - Dispatcher.enqueue
  client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
  1. 繼續深入,方法解析 - Dispatcher.enqueue
//概述:同步方法,如果當前還能執行一個併發請求,則加入 runningAsyncCalls ,立即執行,否則加入 readyAsyncCalls 隊列
synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
    runningAsyncCalls.add(call);
    executorService().execute(call);
  } else {
    readyAsyncCalls.add(call);
  }
}
  • 方法中涉及 AsyncCall 類 - RealCall 的一個內部類,它實現了 Runnable,因此可以提交到 ExecutorService 上執行
  • 它在執行時會調用 getResponseWithInterceptorChain() 函數,並把結果通過 responseCallback 傳遞給上層使用者
  • 總結:同步請求跟異步請求的原理基本一致,最後都是調用 getResponseWithInterceptorChain() 函數,利用攔截鏈來實現的網絡請求邏輯,只是實現方式不同,異步請求需要通過 ExecutorService 來調用getResponseWithInterceptorChain。
    1. 原來同步、異步請求有着異曲同工之妙,探究完 OkHttp 請求發送,當然要繼續探究下返回數據的獲取啦。
  • 完成同步或是異步的請求後,我們就可以從 Response 對象中獲取到相應數據了,而其中值得注意的,也是最重要的,便是 body 部分了,因爲一般服務器返回的數據較大,必須通過數據流的方式來訪問。
  • 響應 body 被封裝到 ResponseBody 類中,需要注意兩點:
    • 每個 body 只能被消費一次,多次消費會出現異常
    • body 必須被關閉,否則會資源泄漏
      1. 最後再來看看 Http 緩存,需要探究 CacheInterceptor 攔截器
//在 ConnectInterceptor 之前添加的一個攔截器,也就是說,在建立連接之前需要看看是否有可用緩存,如果可以則直接返回緩存,否則就繼續建立網絡連接等操作
//代碼較長、這裏貼出核心部分(OkHttp 緩存處理邏輯)
@Override 
public Response intercept(Chain chain) throws IOException {
   ...
    //無可用緩存,放棄
    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(EMPTY_BODY)
          .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 {
      //不管成不成功,都要記得關閉 cache body,避免內存泄漏
      // 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 (validate(cacheResponse, networkResponse)) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .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());
      }
    }
    ...
    return response;
  }
  1. 關於 OkHttp 內部的緩存實際實現?
    • 實現方式:主要涉及 HTTP 協議緩存細節的實現,而具體的緩存邏輯 OkHttp 內置封裝了一個 Cache 類,它利用 DiskLruCache,用磁盤上的有限大小空間進行緩存,按照 LRU 算法進行緩存淘汰。(源碼略長,需要各位自行查看鑽研)
    • InternalCache(接口),我們可以實現該接口,使用我們自定義的緩存策略

知識總結 - For 跟隨Piasy 拆輪子

最後,再回頭看看 Piasy 畫的流程圖,將知識串起來

Piasy - OkHttp 整體流程圖

  • 核心方法:getResponseWithInterceptorChain - 攔截鏈模式(《Android 源碼設計模式》 一書中有講解),層層分明,單一責任
  • 同步、異步請求差異?(異步通過提交到 ExecutorService 來實現,最終還是離不開 getResponseWithInterceptorChain 方法)
  • 其中的提及到的重點攔截器:
    • retryAndFollowUpInterceptor(負責失敗重試以及重定向)
    • BridgeInterceptor(負責把用戶構造的請求轉換爲發送到服務器的請求、把服務器返回的相應轉換爲用戶友好響應)
    • CacheInterceptor(負責讀取緩存直接返回、更新緩存)
    • ConnectInterceptor(負責與服務器建立連接)
    • CallServerInterceptor(負責向服務器發送給請求數據、從服務器讀取響應數據)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章