Android開源框架源碼鑑賞:Okhttp

關於做者java

郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。android

文章目錄git

  • 一 請求與響應流程
    • 1.1 請求的封裝
    • 1.2 請求的發送
    • 1.3 請求的調度
  • 二 攔截器
    • 2.1 RetryAndFollowUpInterceptor
    • 2.2 BridgeInterceptor
    • 2.3 CacheInterceptor
    • 2.4 ConnectInterceptor
    • 2.5 CallServerInterceptor
  • 三 鏈接機制
    • 3.1 創建鏈接
    • 3.2 鏈接池
  • 四 緩存機制
    • 4.1 緩存策略
    • 4.2 緩存管理

更多Android開源框架源碼分析文章請參見Android open framework analysis程序員

在Android刀耕火種的哪一個年代,咱們作網絡請求一般會選用HttpURLConnection或者Apache HTTP Client,這二者均支持HTTPS、流的上傳和下載、配置超時和鏈接池等特性,但隨着業務場景的負責化以及 對流量消耗的優化需求,Okhttp應運而生,自誕生起,口碑就一直很好。github

可是,你們都說好,好在哪裏?既然這麼好,它的設計理念和實現思路有哪些值得咱們學習的地方?🤔編程

今天就帶着這些問題,一探究竟。緩存

An HTTP+HTTP/2 client for Android and Java applications.安全

官方網站:https://github.com/square/okhttp服務器

源碼版本:3.9.1cookie

在正式分析源碼以前,咱們先來看個簡單的小例子,從例子入手,逐步分析Okhttp的實現。

👉 舉例

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .build();
Request request = new Request.Builder()
        .url(url)
        .build();
okHttpClient.newCall(request).enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

    }
});
複製代碼

在上面的例子中,咱們構建了一個客戶端OkHttpClient和一個請求Request,而後調用newCall()方法將請求發送了出去。從這個小例子中,咱們能夠發現 OkHttpClient至關因而個上下文或者說是大管家,它接到咱們給的任務之後,將具體的工做分發到各個子系統中去完成。

Okhttp的子系統層級結構圖以下所示:

👉 點擊圖片查看大圖

  • 網絡配置層:利用Builder模式配置各類參數,例如:超時時間、攔截器等,這些參數都會由Okhttp分發給各個須要的子系統。
  • 重定向層:負責重定向。
  • Header拼接層:負責把用戶構造的請求轉換爲發送給服務器的請求,把服務器返回的響應轉換爲對用戶友好的響應。
  • HTTP緩存層:負責讀取緩存以及更新緩存。
  • 鏈接層:鏈接層是一個比較複雜的層級,它實現了網絡協議、內部的攔截器、安全性認證,鏈接與鏈接池等功能,但這一層尚未發起真正的鏈接,它只是作了鏈接器一些參數的處理。
  • 數據響應層:負責從服務器讀取響應的數據。

在整個Okhttp的系統中,咱們還要理解如下幾個關鍵角色:

  • OkHttpClient:通訊的客戶端,用來統一管理髮起請求與解析響應。
  • Call:Call是一個接口,它是HTTP請求的抽象描述,具體實現類是RealCall,它由CallFactory建立。
  • Request:請求,封裝請求的具體信息,例如:url、header等。
  • RequestBody:請求體,用來提交流、表單等請求信息。
  • Response:HTTP請求的響應,獲取響應信息,例如:響應header等。
  • ResponseBody:HTTP請求的響應體,被讀取一次之後就會關閉,因此咱們重複調用responseBody.string()獲取請求結果是會報錯的。
  • Interceptor:Interceptor是請求攔截器,負責攔截並處理請求,它將網絡請求、緩存、透明壓縮等功能都統一塊兒來,每一個功能都是一個Interceptor,全部的Interceptor最 終鏈接成一個Interceptor.Chain。典型的責任鏈模式實現。
  • StreamAllocation:用來控制Connections與Streas的資源分配與釋放。
  • RouteSelector:選擇路線與自動重連。
  • RouteDatabase:記錄鏈接失敗的Route黑名單。

咱們首先來分析鏈接的請求與響應流程,這樣咱們就能夠對整個Okhttp系統有一個總體的認識。

一 請求與響應流程

Okhttp的整個請求與響應的流程就是Dispatcher不斷從Request Queue裏取出請求(Call),根據是否已經存存緩存,從內存緩存或者服務器獲取請求的數據,請求分爲同步和異步兩種,同步請求經過 調用Call.exectute()方法直接返回當前請求的Response,異步請求調用Call.enqueue()方法將請求(AsyncCall)添加到請求隊列中去,並經過回調(Callback)獲取服務器返回的結果。

一圖勝千言,咱們來看一下整個的流程圖,以下所示:

👉 點擊圖片查看大圖

讀者仔細看一下這個流程圖,是否是很像計算機網絡的OSI七層模型,Okhttp正式採用這種思路,利用攔截器Interceptor將整套框架縱向分層,簡化了設計邏輯,提高了框架擴展性。

經過上面的流程圖,咱們能夠知道在整個請求與響應流程中,如下幾點是咱們須要重點關注的:

  • Dispatcher是如何進行請求調度的?
  • 各個攔截器是如何實現的?
  • 鏈接與鏈接池是如何創建和維護的?

帶着以上問題,咱們去源碼中一探究竟。

咱們先來看一下具體的函數調用鏈,請求與響應的序列圖以下所示:

👉 點擊圖片查看大圖

上述序列圖能夠幫助咱們理解整個請求與響應流程的具體細節,咱們首先來看一下一個請求和如何被封裝併發出的。

1.1 請求的封裝

請求是由Okhttp發出,真正的請求都被封裝了在了接口Call的實現類RealCall中,以下所示:

Call接口以下所示:

public interface Call extends Cloneable {
    
  //返回當前請求
  Request request();

  //同步請求方法,此方法會阻塞當前線程知道請求結果放回
  Response execute() throws IOException;

  //異步請求方法,此方法會將請求添加到隊列中,而後等待請求返回
  void enqueue(Callback responseCallback);

  //取消請求
  void cancel();

  //請求是否在執行,當execute()或者enqueue(Callback responseCallback)執行後該方法返回true
  boolean isExecuted();

  //請求是否被取消
  boolean isCanceled();

  //建立一個新的如出一轍的請求
  Call clone();

  interface Factory {
    Call newCall(Request request);
  }
}
複製代碼

RealCall的構造方法以下所示:

final class RealCall implements Call {
    
  private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    //咱們構建的OkHttpClient,用來傳遞參數
    this.client = client;
    this.originalRequest = originalRequest;
    //是否是WebSocket請求,WebSocket是用來創建長鏈接的,後面咱們會說。
    this.forWebSocket = forWebSocket;
    //構建RetryAndFollowUpInterceptor攔截器
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }
}
複製代碼

RealCall實現了Call接口,它封裝了請求的調用,這個構造函數的邏輯也很簡單:賦值外部傳入的OkHttpClient、Request與forWebSocket,並 建立了重試與重定向攔截器RetryAndFollowUpInterceptor。

1.2 請求的發送

RealCall將請求分爲兩種:

  • 同步請求
  • 異步請求

異步請求只是比同步請求多了個Callback,分別調用的方法以下所示:

異步請求

final class RealCall implements Call {
    
      @Override public void enqueue(Callback responseCallback) {
        synchronized (this) {
          if (executed) throw new IllegalStateException("Already Executed");
          executed = true;
        }
        captureCallStackTrace();
        client.dispatcher().enqueue(new AsyncCall(responseCallback));
      }
}
複製代碼

同步請求

final class RealCall implements Call {
    @Override public Response execute() throws IOException {
      synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
      }
      captureCallStackTrace();
      try {
        client.dispatcher().executed(this);
        Response result = getResponseWithInterceptorChain();
        if (result == null) throw new IOException("Canceled");
        return result;
      } finally {
        client.dispatcher().finished(this);
      }
    }
}
複製代碼

從上面實現能夠看出,無論是同步請求仍是異步請求都是Dispatcher在處理:

  • 同步請求:直接執行,並返回請求結果
  • 異步請求:構造一個AsyncCall,並將本身加入處理隊列中。

AsyncCall本質上是一個Runable,Dispatcher會調度ExecutorService來執行這些Runable。

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl());
      this.responseCallback = responseCallback;
    }

    String host() {
      return originalRequest.url().host();
    }

    Request request() {
      return originalRequest;
    }

    RealCall get() {
      return RealCall.this;
    }

    @Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

複製代碼

從上面代碼能夠看出,無論是同步請求仍是異步請求最後都會經過getResponseWithInterceptorChain()獲取Response,只不過異步請求多了個線程調度,異步 執行的過程。

咱們先來來看看Dispatcher裏的實現。

1.3 請求的調度

public final class Dispatcher {
    
      private int maxRequests = 64;
      private int maxRequestsPerHost = 5;
    
      /** Ready async calls in the order they'll be run. */
      private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
    
      /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
      private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    
      /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
      private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
      
      /** Used by {@code Call#execute} to signal it is in-flight. */
      synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
      }

      synchronized void enqueue(AsyncCall call) {
      //正在運行的異步請求不得超過64,同一個host下的異步請求不得超過5個
      if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
      } else {
        readyAsyncCalls.add(call);
      }
    }
}
複製代碼

Dispatcher是一個任務調度器,它內部維護了三個雙端隊列:

  • readyAsyncCalls:準備運行的異步請求
  • runningAsyncCalls:正在運行的異步請求
  • runningSyncCalls:正在運行的同步請求

記得異步請求與同步騎牛,並利用ExecutorService來調度執行AsyncCall。

同步請求就直接把請求添加到正在運行的同步請求隊列runningSyncCalls中,異步請求會作個判斷:

若是正在運行的異步請求不超過64,並且同一個host下的異步請求不得超過5個則將請求添加到正在運行的同步請求隊列中runningAsyncCalls並開始 執行請求,不然就添加到readyAsyncCalls繼續等待。

講完Dispatcher裏的實現,咱們繼續來看getResponseWithInterceptorChain()的實現,這個方法纔是真正發起請求並處理請求的地方。

1.4 請求的處理

final class RealCall implements Call {
      Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //這裏能夠看出,咱們自定義的Interceptor會被優先執行
        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 (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));
    
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);
      }
}
複製代碼

短短几行代碼,完成了對請求的全部處理過程,Interceptor將網絡請求、緩存、透明壓縮等功能統一了起來,它的實現採用責任鏈模式,各司其職, 每一個功能都是一個Interceptor,上一級處理完成之後傳遞給下一級,它們最後鏈接成了一個Interceptor.Chain。它們的功能以下:

  • RetryAndFollowUpInterceptor:負責重定向。
  • BridgeInterceptor:負責把用戶構造的請求轉換爲發送給服務器的請求,把服務器返回的響應轉換爲對用戶友好的響應。
  • CacheInterceptor:負責讀取緩存以及更新緩存。
  • ConnectInterceptor:負責與服務器創建鏈接。
  • CallServerInterceptor:負責從服務器讀取響應的數據。

位置決定功能,位置靠前的先執行,最後一個則複製與服務器通信,請求從RetryAndFollowUpInterceptor開始層層傳遞到CallServerInterceptor,每一層 都對請求作相應的處理,處理的結構再從CallServerInterceptor層層返回給RetryAndFollowUpInterceptor,最紅請求的發起者得到了服務器返回的結果。

以上即是Okhttp整個請求與響應的具體流程,能夠發現攔截器纔是Okhttp核心功能所在,咱們來逐一分析每一個攔截器的實現。

二 攔截器

從上面的流程能夠看出,各個環節都是由相應的攔截器進行處理,全部的攔截器(包括咱們自定義的)都實現了Interceptor接口,以下所示:

public interface Interceptor {
  Response intercept(Chain chain) throws IOException;

  interface Chain {
    Request request();

    Response proceed(Request request) throws IOException;
    
    //返回Request執行後返回的鏈接
    @Nullable Connection connection();
  }
}

複製代碼

Okhttp內置的攔截器以下所示:

  • RetryAndFollowUpInterceptor:負責失敗重試以及重定向。
  • BridgeInterceptor:負責把用戶構造的請求轉換爲發送給服務器的請求,把服務器返回的響應轉換爲對用戶友好的響應。
  • CacheInterceptor:負責讀取緩存以及更新緩存。
  • ConnectInterceptor:負責與服務器創建鏈接。
  • CallServerInterceptor:負責從服務器讀取響應的數據。

咱們繼續來看看RealInterceptorChain裏是怎麼一級級處理的。

public final class RealInterceptorChain implements Interceptor.Chain {
    
     public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection 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.httpCodec != null && !this.connection.supportsUrl(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.httpCodec != null && calls > 1) {
          throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
              + " must call proceed() exactly once");
        }
    
        // Call the next interceptor in the chain.
        RealInterceptorChain next = new RealInterceptorChain(
            interceptors, streamAllocation, httpCodec, 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 (httpCodec != 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");
        }
    
        return response;
      }
}
複製代碼

這個方法比較有意思,在調用proceed方法以後,會繼續構建一個新的RealInterceptorChain對象,調用下一個interceptor來繼續請求,直到全部interceptor都處理完畢,將 獲得的response返回。

每一個攔截器的方法都遵循這樣的規則:

@Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    //1 Request階段,該攔截器在Request階段負責作的事情

    //2 調用RealInterceptorChain.proceed(),實際上是在遞歸調用下一個攔截器的intercept()方法
    response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);

    //3 Response階段,完成了該攔截器在Response階段負責作的事情,而後返回到上一層的攔截器。
    return response;     
    }
  }
複製代碼

從上面的描述可知,Request是按照interpretor的順序正向處理,而Response是逆向處理的。這參考了OSI七層模型的原理。上面咱們也提到過。CallServerInterceptor至關於最底層的物理層, 請求從上到逐層包裝下發,響應從下到上再逐層包裝返回。很漂亮的設計。

interceptor的執行順序:RetryAndFollowUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor。

2.1 RetryAndFollowUpInterceptor

RetryAndFollowUpInterceptor負責失敗重試以及重定向。

public final class RetryAndFollowUpInterceptor implements Interceptor {
    
    private static final int MAX_FOLLOW_UPS = 20;
    
     @Override public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
    
        //1. 構建一個StreamAllocation對象,StreamAllocation至關因而個管理類,維護了
        //Connections、Streams和Calls之間的管理,該類初始化一個Socket鏈接對象,獲取輸入/輸出流對象。
        streamAllocation = new StreamAllocation(
            client.connectionPool(), createAddress(request.url()), callStackTrace);
    
        //重定向次數
        int followUpCount = 0;
        Response priorResponse = null;
        while (true) {
          if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
          }
    
          Response response = null;
          boolean releaseConnection = true;
          try {
            //2. 繼續執行下一個Interceptor,即BridgeInterceptor
            response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
            releaseConnection = false;
          } catch (RouteException e) {
            //3. 拋出異常,則檢測鏈接是否還能夠繼續。
            if (!recover(e.getLastConnectException(), false, request)) {
              throw e.getLastConnectException();
            }
            releaseConnection = false;
            continue;
          } catch (IOException e) {
            // 和服務端創建鏈接失敗
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, requestSendStarted, request)) throw e;
            releaseConnection = false;
            continue;
          } finally {
            //檢測到其餘未知異常,則釋放鏈接和資源
            if (releaseConnection) {
              streamAllocation.streamFailed(null);
              streamAllocation.release();
            }
          }
    
          //構建響應體,這個響應體的body爲空。
          if (priorResponse != null) {
            response = response.newBuilder()
                .priorResponse(priorResponse.newBuilder()
                        .body(null)
                        .build())
                .build();
          }
    
          //4。根據響應碼處理請求,返回Request不爲空時則進行重定向處理。
          Request followUp = followUpRequest(response);
    
          if (followUp == null) {
            if (!forWebSocket) {
              streamAllocation.release();
            }
            return response;
          }
    
          closeQuietly(response.body());
    
          //重定向的次數不能超過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());
          }
    
          if (!sameConnection(response, followUp.url())) {
            streamAllocation.release();
            streamAllocation = new StreamAllocation(
                client.connectionPool(), createAddress(followUp.url()), callStackTrace);
          } 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;
        }
      }
      
    
 
}
複製代碼

咱們先來講說StreamAllocation這個類的做用,這個類協調了三個實體類的關係:

  • Connections:鏈接到遠程服務器的物理套接字,這個套接字鏈接可能比較慢,因此它有一套取消機制。
  • Streams:定義了邏輯上的HTTP請求/響應對,每一個鏈接都定義了它們能夠攜帶的最大併發流,HTTP/1.x每次只能夠攜帶一個,HTTP/2每次能夠攜帶多個。
  • Calls:定義了流的邏輯序列,這個序列一般是一個初始請求以及它的重定向請求,對於同一個鏈接,咱們一般將全部流都放在一個調用中,以此來統一它們的行爲。

咱們再來看看整個方法的流程:

  1. 構建一個StreamAllocation對象,StreamAllocation至關因而個管理類,維護了Connections、Streams和Calls之間的管理,該類初始化一個Socket鏈接對象,獲取輸入/輸出流對象。
  2. 繼續執行下一個Interceptor,即BridgeInterceptor
  3. 拋出異常,則檢測鏈接是否還能夠繼續,如下狀況不會重試:
  • 客戶端配置出錯再也不重試
  • 出錯後,request body不能再次發送
  • 發生如下Exception也沒法恢復鏈接:
    • ProtocolException:協議異常
    • InterruptedIOException:中斷異常
    • SSLHandshakeException:SSL握手異常
    • SSLPeerUnverifiedException:SSL握手未受權異常
  • 沒有更多線路能夠選擇 4。根據響應碼處理請求,返回Request不爲空時則進行重定向處理,重定向的次數不能超過20次。

最後是根據響應碼來處理請求頭,由followUpRequest()方法完成,具體以下所示:

public final class RetryAndFollowUpInterceptor implements Interceptor {
      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,代理認證
          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,未經認證
          case HTTP_UNAUTHORIZED:
            return client.authenticator().authenticate(route, userResponse);
          //307,308
          case HTTP_PERM_REDIRECT:
          case HTTP_TEMP_REDIRECT:
            // "If the 307 or 308 status code is received in response to a request other than GET
            // or HEAD, the user agent MUST NOT automatically redirect the request"
            if (!method.equals("GET") && !method.equals("HEAD")) {
              return null;
            }
            // fall-through
          //300,301,302,303
          case HTTP_MULT_CHOICE:
          case HTTP_MOVED_PERM:
          case HTTP_MOVED_TEMP:
          case HTTP_SEE_OTHER:
              
            //客戶端在配置中是否容許重定向
            if (!client.followRedirects()) return null;
    
            String location = userResponse.header("Location");
            if (location == null) return null;
            HttpUrl url = userResponse.request().url().resolve(location);
    
            // url爲null,不容許重定向
            if (url == null) return null;
    
            //查詢是否存在http與https之間的重定向
            boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
            if (!sameScheme && !client.followSslRedirects()) return null;
    
            // Most redirects don't include a request body.
            Request.Builder requestBuilder = userResponse.request().newBuilder();
            if (HttpMethod.permitsRequestBody(method)) {
              final boolean maintainBody = HttpMethod.redirectsWithBody(method);
              if (HttpMethod.redirectsToGet(method)) {
                requestBuilder.method("GET", null);
              } else {
                RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                requestBuilder.method(method, requestBody);
              }
              if (!maintainBody) {
                requestBuilder.removeHeader("Transfer-Encoding");
                requestBuilder.removeHeader("Content-Length");
                requestBuilder.removeHeader("Content-Type");
              }
            }
    
            // When redirecting across hosts, drop all authentication headers. This
            // is potentially annoying to the application layer since they have no
            // way to retain them.
            if (!sameConnection(userResponse, url)) {
              requestBuilder.removeHeader("Authorization");
            }
    
            return requestBuilder.url(url).build();
          //408,超時
          case HTTP_CLIENT_TIMEOUT:
            // 408's are rare in practice, but some servers like HAProxy use this response code. The
            // spec says that we may repeat the request without modifications. Modern browsers also
            // repeat the request (even non-idempotent ones.)
            if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
              return null;
            }
    
            return userResponse.request();
    
          default:
            return null;
        }
      }    
}
複製代碼

重定向會涉及到一些網絡編程的知識,這裏若是沒有完成理解,你只要知道RetryAndFollowUpInterceptor的做用就是處理了一些鏈接異常以及重定向就能夠了。咱們接着來看看下一個BridgeInterceptor。

2.2 BridgeInterceptor

BridgeInterceptor就跟它的名字那樣,它是一個鏈接橋,它負責把用戶構造的請求轉換爲發送給服務器的請求,把服務器返回的響應轉換爲對用戶友好的響應。 轉換的過程就是添加一些服務端須要的header信息。

public final class BridgeInterceptor implements Interceptor {
    @Override public Response intercept(Chain chain) throws IOException {
        Request userRequest = chain.request();
        Request.Builder requestBuilder = userRequest.newBuilder();
    
        RequestBody body = userRequest.body();
        if (body != null) {
          //1 進行Header的包裝
          MediaType contentType = body.contentType();
          if (contentType != null) {
            requestBuilder.header("Content-Type", contentType.toString());
          }
    
          long contentLength = body.contentLength();
          if (contentLength != -1) {
            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) {
          requestBuilder.header("Host", hostHeader(userRequest.url(), false));
        }
    
        if (userRequest.header("Connection") == null) {
          requestBuilder.header("Connection", "Keep-Alive");
        }
    
        //這裏有個坑:若是你在請求的時候主動添加了"Accept-Encoding: gzip" ,transparentGzip=false,那你就要本身解壓,若是
        // 你沒有吹解壓,或致使response.string()亂碼。
        // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
        // the transfer stream.
        boolean transparentGzip = false;
        if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
          transparentGzip = true;
          requestBuilder.header("Accept-Encoding", "gzip");
        }
    
        //建立OkhttpClient配置的cookieJar
        List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
        if (!cookies.isEmpty()) {
          requestBuilder.header("Cookie", cookieHeader(cookies));
        }
    
        if (userRequest.header("User-Agent") == null) {
          requestBuilder.header("User-Agent", Version.userAgent());
        }
    
        Response networkResponse = chain.proceed(requestBuilder.build());
    
        //解析服務器返回的Header,若是沒有這事cookie,則不進行解析
        HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    
        Response.Builder responseBuilder = networkResponse.newBuilder()
            .request(userRequest);
    
        //判斷服務器是否支持gzip壓縮,若是支持,則將壓縮提交給Okio庫來處理
        if (transparentGzip
            && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
            && HttpHeaders.hasBody(networkResponse)) {
          GzipSource responseBody = new GzipSource(networkResponse.body().source());
          Headers strippedHeaders = networkResponse.headers().newBuilder()
              .removeAll("Content-Encoding")
              .removeAll("Content-Length")
              .build();
          responseBuilder.headers(strippedHeaders);
          responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
        }
    
        return responseBuilder.build();
      }
}
複製代碼

就跟它的名字描述的那樣,它是一個橋樑,負責把用戶構造的請求轉換爲發送給服務器的請求,把服務器返回的響應轉換爲對用戶友好的響應。 在Request階段配置用戶信息,並添加一些請求頭。在Response階段,進行gzip解壓。

這個方法主要是針對Header作了一些處理,這裏主要提一下"Accept-Encoding", "gzip",關於它有如下幾點須要注意:

  • 開發者沒有添加Accept-Encoding時,自動添加Accept-Encoding: gzip
  • 自動添加Accept-Encoding,會對request,response進行自動解壓
  • 手動添加Accept-Encoding,不負責解壓縮
  • 自動解壓時移除Content-Length,因此上層Java代碼想要contentLength時爲-1
  • 自動解壓時移除 Content-Encoding
  • 自動解壓時,若是是分塊傳輸編碼,Transfer-Encoding: chunked不受影響。

BridgeInterceptor主要就是針對Header作了一些處理,咱們接着來看CacheInterceptor。

2.3 CacheInterceptor

咱們知道爲了節省流量和提升響應速度,Okhttp是有本身的一套緩存機制的,CacheInterceptor就是用來負責讀取緩存以及更新緩存的。

public final class CacheInterceptor implements Interceptor {
    
     @Override public Response intercept(Chain chain) throws IOException {
         
        //1. 讀取候選緩存,具體如何讀取的咱們下面會講。
        Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
    
        long now = System.currentTimeMillis();
    
        //2. 建立緩存策略,強制緩存、對比緩存等,關於緩存策略咱們下面也會講。
        CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;
    
        if (cache != null) {
          cache.trackResponse(strategy);
        }
    
        if (cacheCandidate != null && cacheResponse == null) {
          closeQuietly(cacheCandidate.body());
        }
    
        //3. 根據策略,不使用網絡,又沒有緩存的直接報錯,並返回錯誤碼504。
        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();
        }
    
        //4. 根據策略,不使用網絡,有緩存的直接返回。
        if (networkRequest == null) {
          return cacheResponse.newBuilder()
              .cacheResponse(stripBody(cacheResponse))
              .build();
        }
    
        Response networkResponse = null;
        try {
          //5. 前面兩個都沒有返回,繼續執行下一個Interceptor,即ConnectInterceptor。
          networkResponse = chain.proceed(networkRequest);
        } finally {
          //若是發生IO異常,則釋放掉緩存
          if (networkResponse == null && cacheCandidate != null) {
            closeQuietly(cacheCandidate.body());
          }
        }
    
        //6. 接收到網絡結果,若是響應code式304,則使用緩存,返回緩存結果。
        if (cacheResponse != null) {
          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();
    
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            return response;
          } else {
            closeQuietly(cacheResponse.body());
          }
        }
    
        //7. 讀取網絡結果。
        Response response = networkResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    
        //8. 對數據進行緩存。
        if (cache != null) {
          if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // Offer this request to the cache.
            CacheRequest cacheRequest = cache.put(response);
            return cacheWritingResponse(cacheRequest, response);
          }
    
          if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
              cache.remove(networkRequest);
            } catch (IOException ignored) {
              // The cache cannot be written.
            }
          }
        }
    
        //9. 返回網絡讀取的結果。
        return response;
      }
}
複製代碼

整個方法的流程以下所示:

  1. 讀取候選緩存,具體如何讀取的咱們下面會講。
  2. 建立緩存策略,強制緩存、對比緩存等,關於緩存策略咱們下面也會講。
  3. 根據策略,不使用網絡,又沒有緩存的直接報錯,並返回錯誤碼504。
  4. 根據策略,不使用網絡,有緩存的直接返回。
  5. 前面兩個都沒有返回,繼續執行下一個Interceptor,即ConnectInterceptor。
  6. 接收到網絡結果,若是響應code式304,則使用緩存,返回緩存結果。
  7. 讀取網絡結果。
  8. 對數據進行緩存。
  9. 返回網絡讀取的結果。

咱們再接着來看ConnectInterceptor。

2.4 ConnectInterceptor

在RetryAndFollowUpInterceptor裏初始化了一個StreamAllocation對象,咱們說在這個StreamAllocation對象裏初始化了一個Socket對象用來作鏈接,可是並無 真正的鏈接,等處處理完hader和緩存信息以後,才調用ConnectInterceptor來進行真正的鏈接

public final class ConnectInterceptor implements Interceptor {
    
      @Override public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Request request = realChain.request();
        StreamAllocation streamAllocation = realChain.streamAllocation();
    
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        //建立輸出流
        HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
        //創建鏈接
        RealConnection connection = streamAllocation.connection();
    
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
      }
}
複製代碼

ConnectInterceptor在Request階段創建鏈接,處理方式也很簡單,建立了兩個對象:

  • HttpCodec:用來編碼HTTP requests和解碼HTTP responses
  • RealConnection:鏈接對象,負責發起與服務器的鏈接。

這裏事實上包含了鏈接、鏈接池等一整套的Okhttp的鏈接機制,咱們放在下面單獨講,先來繼續看最後一個Interceptor:CallServerInterceptor。

2.5 CallServerInterceptor

CallServerInterceptor負責從服務器讀取響應的數據。

public final class CallServerInterceptor implements Interceptor {
    
    @Override public Response intercept(Chain chain) throws IOException {
        
        //這些對象在前面的Interceptor都已經建立完畢
        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();
        //1. 寫入請求頭 
        httpCodec.writeRequestHeaders(request);
    
        Response.Builder responseBuilder = null;
        if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
          // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
          // Continue" response before transmitting the request body. If we don't get that, return what
          // we did get (such as a 4xx response) without ever transmitting the request body.
          if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            httpCodec.flushRequest();
            responseBuilder = httpCodec.readResponseHeaders(true);
          }
    
          //2 寫入請求體
          if (responseBuilder == null) {
            // Write the request body if the "Expect: 100-continue" expectation was met.
            Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
            BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
            request.body().writeTo(bufferedRequestBody);
            bufferedRequestBody.close();
          } else if (!connection.isMultiplexed()) {
            // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from
            // being reused. Otherwise we're still obligated to transmit the request body to leave the
            // connection in a consistent state.
            streamAllocation.noNewStreams();
          }
        }
    
        httpCodec.finishRequest();
    
        //3 讀取響應頭
        if (responseBuilder == null) {
          responseBuilder = httpCodec.readResponseHeaders(false);
        }
    
        Response response = responseBuilder
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    
        //4 讀取響應體
        int code = response.code();
        if (forWebSocket && code == 101) {
          // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
          response = response.newBuilder()
              .body(Util.EMPTY_RESPONSE)
              .build();
        } else {
          response = response.newBuilder()
              .body(httpCodec.openResponseBody(response))
              .build();
        }
    
        if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
          streamAllocation.noNewStreams();
        }
    
        if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
          throw new ProtocolException(
              "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
        }
    
        return response;
      }
}
複製代碼

咱們經過ConnectInterceptor已經鏈接到服務器了,接下來咱們就是寫入請求數據以及讀出返回數據了。整個流程:

  1. 寫入請求頭
  2. 寫入請求體
  3. 讀取響應頭
  4. 讀取響應體

這篇文章就到這裏,後續的文章咱們會來分析Okhttp的緩存機制、鏈接機制、編輯嗎機制等實現。

三 鏈接機制

鏈接的建立是在StreamAllocation對象統籌下完成的,咱們前面也說過它早在RetryAndFollowUpInterceptor就被建立了,StreamAllocation對象 主要用來管理兩個關鍵角色:

  • RealConnection:真正創建鏈接的對象,利用Socket創建鏈接。
  • ConnectionPool:鏈接池,用來管理和複用鏈接。

在裏初始化了一個StreamAllocation對象,咱們說在這個StreamAllocation對象裏初始化了一個Socket對象用來作鏈接,可是並無

3.1 建立鏈接

咱們在前面的ConnectInterceptor分析中已經說過,onnectInterceptor用來完成鏈接。而真正的鏈接在RealConnect中實現,鏈接由鏈接池ConnectPool來管理,鏈接池最多保 持5個地址的鏈接keep-alive,每一個keep-alive時長爲5分鐘,並有異步線程清理無效的鏈接。

主要由如下兩個方法完成:

  1. HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
  2. RealConnection connection = streamAllocation.connection();

咱們來具體的看一看。

StreamAllocation.newStream()最終調動findConnect()方法來創建鏈接。

public final class StreamAllocation {
    
      /** * Returns a connection to host a new stream. This prefers the existing connection if it exists, * then the pool, finally building a new connection. */
      private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
        Route selectedRoute;
        synchronized (connectionPool) {
          if (released) throw new IllegalStateException("released");
          if (codec != null) throw new IllegalStateException("codec != null");
          if (canceled) throw new IOException("Canceled");
    
          //1 查看是否有無缺的鏈接
          RealConnection allocatedConnection = this.connection;
          if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
          }
    
          //2 鏈接池中是否用可用的鏈接,有則使用
          Internal.instance.get(connectionPool, address, this, null);
          if (connection != null) {
            return connection;
          }
    
          selectedRoute = route;
        }
    
        //線程的選擇,多IP操做
        if (selectedRoute == null) {
          selectedRoute = routeSelector.next();
        }
    
        //3 若是沒有可用鏈接,則本身建立一個
        RealConnection result;
        synchronized (connectionPool) {
          if (canceled) throw new IOException("Canceled");
    
          // Now that we have an IP address, make another attempt at getting a connection from the pool.
          // This could match due to connection coalescing.
          Internal.instance.get(connectionPool, address, this, selectedRoute);
          if (connection != null) {
            route = selectedRoute;
            return connection;
          }
    
          // Create a connection and assign it to this allocation immediately. This makes it possible
          // for an asynchronous cancel() to interrupt the handshake we're about to do.
          route = selectedRoute;
          refusedStreamCount = 0;
          result = new RealConnection(connectionPool, selectedRoute);
          acquire(result);
        }
    
        //4 開始TCP以及TLS握手操做
        result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
        routeDatabase().connected(result.route());
    
        //5 將新建立的鏈接,放在鏈接池中
        Socket socket = null;
        synchronized (connectionPool) {
          // Pool the connection.
          Internal.instance.put(connectionPool, result);
    
          // If another multiplexed connection to the same address was created concurrently, then
          // release this connection and acquire that one.
          if (result.isMultiplexed()) {
            socket = Internal.instance.deduplicate(connectionPool, address, this);
            result = connection;
          }
        }
        closeQuietly(socket);
    
        return result;
      }    
}
複製代碼

整個流程以下:

  1. 查找是否有完整的鏈接可用:
  • Socket沒有關閉
  • 輸入流沒有關閉
  • 輸出流沒有關閉
  • Http2鏈接沒有關閉
  1. 鏈接池中是否有可用的鏈接,若是有則可用。
  2. 若是沒有可用鏈接,則本身建立一個。
  3. 開始TCP鏈接以及TLS握手操做。
  4. 將新建立的鏈接加入鏈接池。

上述方法完成後會建立一個RealConnection對象,而後調用該方法的connect()方法創建鏈接,咱們再來看看RealConnection.connect()方法的實現。

public final class RealConnection extends Http2Connection.Listener implements Connection {
    
    public void connect( int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
       if (protocol != null) throw new IllegalStateException("already connected");
   
       //線路選擇
       RouteException routeException = null;
       List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
       ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
   
       if (route.address().sslSocketFactory() == null) {
         if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
           throw new RouteException(new UnknownServiceException(
               "CLEARTEXT communication not enabled for client"));
         }
         String host = route.address().url().host();
         if (!Platform.get().isCleartextTrafficPermitted(host)) {
           throw new RouteException(new UnknownServiceException(
               "CLEARTEXT communication to " + host + " not permitted by network security policy"));
         }
       }
       
       //開始鏈接
       while (true) {
         try {
            //若是是通道模式,則創建通道鏈接
           if (route.requiresTunnel()) {
             connectTunnel(connectTimeout, readTimeout, writeTimeout);
           } 
           //不然進行Socket鏈接,通常都是屬於這種狀況
           else {
             connectSocket(connectTimeout, readTimeout);
           }
           //創建https鏈接
           establishProtocol(connectionSpecSelector);
           break;
         } catch (IOException e) {
           closeQuietly(socket);
           closeQuietly(rawSocket);
           socket = null;
           rawSocket = null;
           source = null;
           sink = null;
           handshake = null;
           protocol = null;
           http2Connection = null;
   
           if (routeException == null) {
             routeException = new RouteException(e);
           } else {
             routeException.addConnectException(e);
           }
   
           if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
             throw routeException;
           }
         }
       }
   
       if (http2Connection != null) {
         synchronized (connectionPool) {
           allocationLimit = http2Connection.maxConcurrentStreams();
         }
       }
     }

    /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
      private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
        Proxy proxy = route.proxy();
        Address address = route.address();
    
        //根據代理類型的不一樣處理Socket
        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
    
        rawSocket.setSoTimeout(readTimeout);
        try {
          //創建Socket鏈接
          Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
        } catch (ConnectException e) {
          ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
          ce.initCause(e);
          throw ce;
        }
    
        // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
        // More details:
        // https://github.com/square/okhttp/issues/3245
        // https://android-review.googlesource.com/#/c/271775/
        try {
          //獲取輸入/輸出流
          source = Okio.buffer(Okio.source(rawSocket));
          sink = Okio.buffer(Okio.sink(rawSocket));
        } catch (NullPointerException npe) {
          if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
            throw new IOException(npe);
          }
        }
      }
}
複製代碼

最終調用Java裏的套接字Socket裏的connect()方法。

3.2 鏈接池

咱們知道在負責的網絡環境下,頻繁的進行創建Sokcet鏈接(TCP三次握手)和斷開Socket(TCP四次分手)是很是消耗網絡資源和浪費時間的,HTTP中的keepalive鏈接對於 下降延遲和提高速度有很是重要的做用。

複用鏈接就須要對鏈接進行管理,這裏就引入了鏈接池的概念。

Okhttp支持5個併發KeepAlive,默認鏈路生命爲5分鐘(鏈路空閒後,保持存活的時間),鏈接池有ConectionPool實現,對鏈接進行回收和管理。

ConectionPool在內部維護了一個線程池,來清理鏈接,以下所示:

public final class ConnectionPool {
    
        private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
          Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
      
        //清理鏈接,在線程池executor裏調用。
        private final Runnable cleanupRunnable = new Runnable() {
          @Override public void run() {
            while (true) {
              //執行清理,並返回下次須要清理的時間。
              long waitNanos = cleanup(System.nanoTime());
              if (waitNanos == -1) return;
              if (waitNanos > 0) {
                long waitMillis = waitNanos / 1000000L;
                waitNanos -= (waitMillis * 1000000L);
                synchronized (ConnectionPool.this) {
                  try {
                    //在timeout時間內釋放鎖
                    ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                  } catch (InterruptedException ignored) {
                  }
                }
              }
            }
          }
        };
}
複製代碼

ConectionPool在內部維護了一個線程池,來清理連,清理任務由cleanup()方法完成,它是一個阻塞操做,首先執行清理,並返回下次須要清理的間隔時間,調用調用wait() 方法釋放鎖。等時間到了之後,再次進行清理,並返回下一次須要清理的時間,循環往復。

咱們來看一看cleanup()方法的具體實現。

public final class ConnectionPool {
    
      long cleanup(long now) {
        int inUseConnectionCount = 0;
        int idleConnectionCount = 0;
        RealConnection longestIdleConnection = null;
        long longestIdleDurationNs = Long.MIN_VALUE;
    
     
        synchronized (this) {
            //遍歷全部的鏈接,標記處不活躍的鏈接。
          for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();
    
            //1. 查詢此鏈接內部的StreanAllocation的引用數量。
            if (pruneAndGetAllocationCount(connection, now) > 0) {
              inUseConnectionCount++;
              continue;
            }
    
            idleConnectionCount++;
    
            //2. 標記空閒鏈接。
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
              longestIdleDurationNs = idleDurationNs;
              longestIdleConnection = connection;
            }
          }
    
          if (longestIdleDurationNs >= this.keepAliveDurationNs
              || idleConnectionCount > this.maxIdleConnections) {
            //3. 若是空閒鏈接超過5個或者keepalive時間大於5分鐘,則將該鏈接清理掉。
            connections.remove(longestIdleConnection);
          } else if (idleConnectionCount > 0) {
            //4. 返回此鏈接的到期時間,供下次進行清理。
            return keepAliveDurationNs - longestIdleDurationNs;
          } else if (inUseConnectionCount > 0) {
            //5. 所有都是活躍鏈接,5分鐘時候再進行清理。
            return keepAliveDurationNs;
          } else {
            //6. 沒有任何鏈接,跳出循環。
            cleanupRunning = false;
            return -1;
          }
        }
    
        //7. 關閉鏈接,返回時間0,當即再次進行清理。
        closeQuietly(longestIdleConnection.socket());
        return 0;
      }
}
複製代碼

整個方法的流程以下所示:

  1. 查詢此鏈接內部的StreanAllocation的引用數量。
  2. 標記空閒鏈接。
  3. 若是空閒鏈接超過5個或者keepalive時間大於5分鐘,則將該鏈接清理掉。
  4. 返回此鏈接的到期時間,供下次進行清理。
  5. 所有都是活躍鏈接,5分鐘時候再進行清理。
  6. 沒有任何鏈接,跳出循環。
  7. 關閉鏈接,返回時間0,當即再次進行清理。

在RealConnection裏有個StreamAllocation虛引用列表,每建立一個StreamAllocation,就會把它添加進該列表中,若是留關閉之後就將StreamAllocation 對象從該列表中移除,正是利用利用這種引用計數的方式斷定一個鏈接是否爲空閒鏈接,

public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
複製代碼

查找引用計數由pruneAndGetAllocationCount()方法實現,具體實現以下所示:

public final class ConnectionPool {
    
     private int pruneAndGetAllocationCount(RealConnection connection, long now) {
       //虛引用列表
       List<Reference<StreamAllocation>> references = connection.allocations;
       //遍歷虛引用列表
       for (int i = 0; i < references.size(); ) {
         Reference<StreamAllocation> reference = references.get(i);
         //若是虛引用StreamAllocation正在被使用,則跳過進行下一次循環,
         if (reference.get() != null) {
           //引用計數
           i++;
           continue;
         }
   
         // We've discovered a leaked allocation. This is an application bug.
         StreamAllocation.StreamAllocationReference streamAllocRef =
             (StreamAllocation.StreamAllocationReference) reference;
         String message = "A connection to " + connection.route().address().url()
             + " was leaked. Did you forget to close a response body?";
         Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
   
         //不然移除該StreamAllocation引用
         references.remove(i);
         connection.noNewStreams = true;
   
         // 若是全部的StreamAllocation引用都沒有了,返回引用計數0
         if (references.isEmpty()) {
           connection.idleAtNanos = now - keepAliveDurationNs;
           return 0;
         }
       }
       
       //返回引用列表的大小,做爲引用計數
       return references.size();
     } 
}
複製代碼

四 緩存機制

3.1 緩存策略

在分析Okhttp的緩存機制以前,咱們先來回顧一下HTTP與緩存相關的理論知識,這是實現Okhttp機制的基礎。

HTTP的緩存機制也是依賴於請求和響應header裏的參數類實現的,最終響應式從緩存中去,仍是從服務端從新拉取,HTTP的緩存機制的流程以下所示:

👉 點擊圖片查看大圖

HTTP的緩存能夠分爲兩種:

  • 強制緩存:須要服務端參與判斷是否繼續使用緩存,當客戶端第一次請求數據是,服務端返回了緩存的過時時間(Expires與Cache-Control),沒有過時就能夠繼續使用緩存,不然則不適用,無需再向服務端詢問。
  • 對比緩存:須要服務端參與判斷是否繼續使用緩存,當客戶端第一次請求數據時,服務端會將緩存標識(Last-Modified/If-Modified-Since與Etag/If-None-Match)與數據一塊兒返回給客戶端,客戶端將二者都備份到緩存中 ,再次請求數據時,客戶端將上次備份的緩存 標識發送給服務端,服務端根據緩存標識進行判斷,若是返回304,則表示通知客戶端能夠繼續使用緩存。

強制緩存優先於對比緩存。

上面提到強制緩存使用的的兩個標識:

  • Expires:Expires的值爲服務端返回的到期時間,即下一次請求時,請求時間小於服務端返回的到期時間,直接使用緩存數據。到期時間是服務端生成的,客戶端和服務端的時間可能有偏差。
  • Cache-Control:Expires有個時間校驗的問題,全部HTTP1.1採用Cache-Control替代Expires。

Cache-Control的取值有如下幾種:

  • private: 客戶端能夠緩存。
  • public: 客戶端和代理服務器均可緩存。
  • max-age=xxx: 緩存的內容將在 xxx 秒後失效
  • no-cache: 須要使用對比緩存來驗證緩存數據。
  • no-store: 全部內容都不會緩存,強制緩存,對比緩存都不會觸發。

咱們再來看看對比緩存的兩個標識:

Last-Modified/If-Modified-Since

Last-Modified 表示資源上次修改的時間。

當客戶端發送第一次請求時,服務端返回資源上次修改的時間:

Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT
複製代碼

客戶端再次發送,會在header裏攜帶If-Modified-Since。將上次服務端返回的資源時間上傳給服務端。

If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT 
複製代碼

服務端接收到客戶端發來的資源修改時間,與本身當前的資源修改時間進行對比,若是本身的資源修改時間大於客戶端發來的資源修改時間,則說明資源作過修改, 則返回200表示須要從新請求資源,不然返回304表示資源沒有被修改,能夠繼續使用緩存。

上面是一種時間戳標記資源是否修改的方法,還有一種資源標識碼ETag的方式來標記是否修改,若是標識碼發生改變,則說明資源已經被修改,ETag優先級高於Last-Modified。

Etag/If-None-Match

ETag是資源文件的一種標識碼,當客戶端發送第一次請求時,服務端會返回當前資源的標識碼:

ETag: "5694c7ef-24dc"
複製代碼

客戶端再次發送,會在header裏攜帶上次服務端返回的資源標識碼:

If-None-Match:"5694c7ef-24dc"
複製代碼

服務端接收到客戶端發來的資源標識碼,則會與本身當前的資源嗎進行比較,若是不一樣,則說明資源已經被修改,則返回200,若是相同則說明資源沒有被修改,返回 304,客戶端能夠繼續使用緩存。

以上即是HTTP緩存策略的相關理論知識,咱們來看看具體實現。

Okhttp的緩存策略就是根據上述流程圖實現的,具體的實現類是CacheStrategy,CacheStrategy的構造函數裏有兩個參數:

CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
}
複製代碼

這兩個參數參數的含義以下:

  • networkRequest:網絡請求。
  • cacheResponse:緩存響應,基於DiskLruCache實現的文件緩存,能夠是請求中url的md5,value是文件中查詢到的緩存,這個咱們下面會說。

CacheStrategy就是利用這兩個參數生成最終的策略,有點像map操做,將networkRequest與cacheResponse這兩個值輸入,處理以後再將這兩個值輸出,們的組合結果以下所示:

  • 若是networkRequest爲null,cacheResponse爲null:only-if-cached(代表不進行網絡請求,且緩存不存在或者過時,必定會返回503錯誤)。
  • 若是networkRequest爲null,cacheResponse爲non-null:不進行網絡請求,並且緩存可使用,直接返回緩存,不用請求網絡。
  • 若是networkRequest爲non-null,cacheResponse爲null:須要進行網絡請求,並且緩存不存在或者過時,直接訪問網絡。
  • 若是networkRequest爲non-null,cacheResponse爲non-null:Header中含有ETag/Last-Modified標籤,須要在條件請求下使用,仍是須要訪問網絡。

那麼這四種狀況是如何斷定的,咱們來看一下。

CacheStrategy是利用Factory模式進行構造的,CacheStrategy.Factory對象構建之後,調用它的get()方法便可得到具體的CacheStrategy,CacheStrategy.Factory.get()方法內部 調用的是CacheStrategy.Factory.getCandidate()方法,它是核心的實現。

以下所示:

public static class Factory {
    
        private CacheStrategy getCandidate() {
          //1. 若是緩存沒有命中,就直接進行網絡請求。
          if (cacheResponse == null) {
            return new CacheStrategy(request, null);
          }
    
          //2. 若是TLS握手信息丟失,則返回直接進行鏈接。
          if (request.isHttps() && cacheResponse.handshake() == null) {
            return new CacheStrategy(request, null);
          }

          //3. 根據response狀態碼,Expired時間和是否有no-cache標籤就行判斷是否進行直接訪問。
          if (!isCacheable(cacheResponse, request)) {
            return new CacheStrategy(request, null);
          }
    
          //4. 若是請求header裏有"no-cache"或者右條件GET請求(header裏帶有ETag/Since標籤),則直接鏈接。
          CacheControl requestCaching = request.cacheControl();
          if (requestCaching.noCache() || hasConditions(request)) {
            return new CacheStrategy(request, null);
          }
    
          CacheControl responseCaching = cacheResponse.cacheControl();
          if (responseCaching.immutable()) {
            return new CacheStrategy(null, cacheResponse);
          }
    
          //計算當前age的時間戳:now - sent + age
          long ageMillis = cacheResponseAge();
          //刷新時間,通常服務器設置爲max-age
          long freshMillis = computeFreshnessLifetime();
    
          if (requestCaching.maxAgeSeconds() != -1) {
            //通常取max-age
            freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
          }
    
          long minFreshMillis = 0;
          if (requestCaching.minFreshSeconds() != -1) {
            //通常取0
            minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
          }
    
          long maxStaleMillis = 0;
          if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
            maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
          }
    
          //5. 若是緩存在過時時間內則能夠直接使用,則直接返回上次緩存。
          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());
          }
    
          //6. 若是緩存過時,且有ETag等信息,則發送If-None-Match、If-Modified-Since、If-Modified-Since等條件請求
          //交給服務端判斷處理
          String conditionName;
          String conditionValue;
          if (etag != null) {
            conditionName = "If-None-Match";
            conditionValue = etag;
          } else if (lastModified != null) {
            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);
        }
}
複製代碼

整個函數的邏輯就是按照上面那個HTTP緩存斷定流程圖來實現,具體流程以下所示:

  1. 若是緩存沒有命中,就直接進行網絡請求。
  2. 若是TLS握手信息丟失,則返回直接進行鏈接。
  3. 根據response狀態碼,Expired時間和是否有no-cache標籤就行判斷是否進行直接訪問。
  4. 若是請求header裏有"no-cache"或者右條件GET請求(header裏帶有ETag/Since標籤),則直接鏈接。
  5. 若是緩存在過時時間內則能夠直接使用,則直接返回上次緩存。
  6. 若是緩存過時,且有ETag等信息,則發送If-None-Match、If-Modified-Since、If-Modified-Since等條件請求交給服務端判斷處理

整個流程就是這樣,另外說一點,Okhttp的緩存是根據服務器header自動的完成的,整個流程也是根據RFC文檔寫死的,客戶端沒必要要進行手動控制。

理解了緩存策略,咱們來看看緩存在磁盤上是如何被管理的。

3.2 緩存管理

這篇文章咱們來分析Okhttp的緩存機制,緩存機制是基於DiskLruCache作的。Cache類封裝了緩存的實現,實現了InternalCache接口。

InternalCache接口以下所示:

InternalCache

public interface InternalCache {
  //獲取緩存
  Response get(Request request) throws IOException;
  //存入緩存
  CacheRequest put(Response response) throws IOException;
  //移除緩存
  void remove(Request request) throws IOException;
  //更新緩存
  void update(Response cached, Response network);
  //跟蹤一個知足緩存條件的GET請求
  void trackConditionalCacheHit();
  //跟蹤知足緩存策略CacheStrategy的響應
  void trackResponse(CacheStrategy cacheStrategy);
}
複製代碼

咱們接着來看看它的實現類。

Cache沒有直接實現InternalCache這個接口,而是在其內部實現了InternalCache的匿名內部類,內部類的方法調用Cache對應的方法,以下所示:

final InternalCache internalCache = new InternalCache() {
@Override public Response get(Request request) throws IOException {
  return Cache.this.get(request);
}

@Override public CacheRequest put(Response response) throws IOException {
  return Cache.this.put(response);
}

@Override public void remove(Request request) throws IOException {
  Cache.this.remove(request);
}

@Override public void update(Response cached, Response network) {
  Cache.this.update(cached, network);
}

@Override public void trackConditionalCacheHit() {
  Cache.this.trackConditionalCacheHit();
}

@Override public void trackResponse(CacheStrategy cacheStrategy) {
  Cache.this.trackResponse(cacheStrategy);
}
};

InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
複製代碼

` 在Cache類裏還定義一些內部類,這些類封裝了請求與響應信息。

  • Cache.Entry:封裝了請求與響應等信息,包括url、varyHeaders、protocol、code、message、responseHeaders、handshake、sentRequestMillis與receivedResponseMillis。
  • Cache.CacheResponseBody:繼承於ResponseBody,封裝了緩存快照snapshot,響應體bodySource,內容類型contentType,內容長度contentLength。

除了兩個類之外,Okhttp還封裝了一個文件系統類FileSystem類,這個類利用Okio這個庫對Java的FIle操做進行了一層封裝,簡化了IO操做。理解了這些剩下的就是DiskLruCahe裏的插入緩存 、獲取緩存和刪除緩存的操做。

關於這一部分的內容,能夠參考咱們以前寫的內容07Android開源框架源碼分析:LruCache與DiskLruCache

好了,到這裏關於Okhttp的所有內容就都講完了,能夠說Okhttp是設計很是優良的一個庫,有不少值得咱們學習的地方,下一篇咱們來分析它的好搭檔Retrofit。

相關文章
相關標籤/搜索