OkHttp源碼分析

本篇文章將從OkHttp的使用步驟入手逐步去分析Okhttp的源碼。

常見使用:

//OkHttpClient的創建
OkHttpClient client = new OkHttpClient();
//Request的創建
Request request = new Request.Builder()
                .url(url)
                .build();
//Response的獲取
Response response = client.newCall(request).execute()/enqueue();

OkHttpClient的創建

我們首先構建一個最簡單的OkhttpClient, OkHttpClient client = new OkHttpClient();

接下來進入OkHttpClient這個類,看下client的創建過程

  public OkHttpClient() {
    //調用自身的Builder
    this(new Builder());
  }

  //爲Builder設置默認值
  public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      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;
      pingInterval = 0;
    }

我們可以看到,OkHttp類中運用了建造者模式,如果我們沒通過Builder去創建OkhttpClient的話,它會爲builder設置上默認值,如果有相應的要求可以在創建的時候調用其Builder去自定義我們的OkhttpClient,至此OkHttpClient創建完畢。

Request的創建

 同樣構建一個最簡單的Request

//Request的創建
Request request = new Request.Builder().url(url).build();

進入Request類。

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder();
    }

    Builder(Request request) {
      this.url = request.url;
      this.method = request.method;
      this.body = request.body;
      this.tag = request.tag;
      this.headers = request.headers.newBuilder();
    }

同樣是利用建造者模式創建出來的一個對象,其默認請求方法是GET,至此Request創建完畢,本文2/3結束。

Response的獲取

接下來開始本文最重要的部分。

Response response = client.newCall(request).execute()/enqueue();

首先分析一下上面這段代碼,可以看出我們的response是client將request傳入了newCall這個方法,然後調用execute或者enqueue,最後得到的返回值。這裏我們發現最終是newCall調用了我們的執行方法,所以接下來進入newCall這個方法。

  public Call newCall(Request request) {
    return new RealCall(this, request, false /* for web socket */);
  }

我們可以看到newCall這個方法將我們的request作爲參數傳入了RealCall的構造函數並返回了一個RealCall的對象。所以我們接下來進入RealCall看看到底是返回了一個怎樣的對象。但是在進入RealCall之前我們先看看這個Call是個什麼東西。

  • Call:它是一個已經準備好執行的request,它能夠被取消,每個Call只能執行一次。

接下來我們看看Call 的源碼 

/**
 * A call is a request that has been prepared for execution. A call can be canceled. As this object
 * represents a single request/response pair (stream), it cannot be executed twice.
 */
public interface Call extends Cloneable {
  /** Returns the original request that initiated this call. */
  Request request();

  /**
   * Invokes the request immediately, and blocks until the response can be processed or is in error
   * 立刻執行request,並且在response在執行過程中或者產生error時發生阻塞
   */
  Response execute() throws IOException;

  /**
   * Schedules the request to be executed at some point in the future.
   * 安排我們的request,它將會在之後某個時間點執行
   */
  void enqueue(Callback responseCallback);

  /** Cancels the request, if possible. Requests that are already complete cannot be canceled. 
   * request能夠被cancel,但是已經完成的不能被cancel
   */
  void cancel();

  /**
   * Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain
   * #enqueue(Callback) enqueued}. 
   */
  boolean isExecuted();

  boolean isCanceled();

  /**
   * Create a new, identical call to this one which can be enqueued or executed even if this call has already been.
   * 通過clone創建一個新的Call,使該Call能夠執行“兩次”
   */
  Call clone();

  interface Factory {
    Call newCall(Request request);
  }
}

我們看到了Call是個接口,裏面的每個方法我都進行了標註,現在我們大概瞭解了Call是個什麼東西,接下來繼續看看RealCall

但是我們先回到開始,把以下兩段代碼結合起來。

Response response = client.newCall(request).execute()/enqueue();

public Call newCall(Request request) {
    return new RealCall(this, request, false /* for web socket */);
}

不難看出實際調用我們的execute()和enqueue()方法的對象其實是RealCall,而這兩個執行方法也正是我們網絡請求的重點,所以我們接下來重點去看RealCall裏這兩個方法的實現。

首先是execute()的實現:

  public Response execute() throws IOException {
    //首先給當前對象加鎖,如果已經被執行過則拋出"Already Executed"異常
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
     //捕獲該請求的stackTrace
    captureCallStackTrace();

    //執行核心請求代碼
    try {
      client.dispatcher().executed(this);
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

在上面execute()的代碼中我們看到他首先給當前對象加上了鎖,並設置executed標誌位保證其只能執行一次,接着調用captureCallStackTrace方法捕獲該請求的stackTrace,在最後我們看到了核心的執行代碼,client調用了dispatcher來執行當前的請求,而返回的請求是由getResponseWithInterceptorChain這個方法返回的。所以我們接下來的重點就是看dispatcher是怎麼執行請求的,和get那個方法是怎麼返回結果的

首先看看dispatcher是怎樣去執行我們的請求的,根據dispatcher的executed的方法我們先進入到Dispatcher的executed。

  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

這裏我們發現dispatcher的executed方法僅僅是將我們的call加入到了runningSyncCalls當中,所以看下這是個什麼東西。

  /** Ready async calls in the order they'll be run. */
  //存放準備執行的Call
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //存放異步執行中的Call
  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //存放同步執行中的Call
  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

在Dispatcher中我們看到了三個這樣的雙向隊列,分別用於存放各種請求。這裏executed方法執行完畢???執行請求僅僅是將一個Call加入到一個雙向隊列?

接下來看看getResponseWithInterceptorChain()這個方法。

Response result = getResponseWithInterceptorChain();

  Response getResponseWithInterceptorChain() throws IOException {
    // 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 (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    //網絡攔截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
    
    //創建RealInterceptorChain並執行proceed方法
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

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

      interface Chain {
        Request request();

        Response proceed(Request request) throws IOException;

        @Nullable Connection connection();
      }
  }

在這個重要的方法當中我們發現了Response是各個攔截器進行攔截之後最後返回的一個結果,而這裏用到的就是一種責任鏈模式,request經過一層一層攔截器後交到服務器,response反過來經過一層一層攔截器把返回結果交回來。所以到這裏,整體的工作流程就已經梳理完畢了。大致如下圖所示

感謝前輩json_it的圖

接下來我們要關心的是各個攔截器是怎樣將我們的response生成並返回的,所以接下來分析一下每個攔截器各自的作用。

  • RetryAndFollowUpInterceptor 

 這個攔截器它的作用主要是負責請求的重定向操作,用於處理網絡請求中,請求失敗後的重試機制,所以我們okHttp的重試機制和它密切相關。首先我們看看它的內部實現。

public RetryAndFollowUpInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    //將chain向下轉型爲對應的RealIntercepterChain
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //它主要用於管理客戶端與服務器之間的連接,同時管理連接池,以及請求成功後的連接釋放等操作
    Transmitter transmitter = realChain.transmitter();

    int followUpCount = 0;
    Response priorResponse = null;
    while (true) {
      transmitter.prepareToConnect(request);
      //首先檢查當前請求是否被取消
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      Response response;
      boolean success = false;
      try {
        //交給上層攔截器處理過程中如果拋出異常則執行相應的catch語句,成功則設置標誌位success = true
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.getLastConnectException(), transmitter, false, request)) {
          throw e.getFirstConnectException();
        }
        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, transmitter, requestSendStarted, request)) throw e;
        continue;
      } finally {
        //最後如果還是失敗,拋出異常並釋放所有資源
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException();
        }
      }

      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                    .body(null)
                    .build())
            .build();
      }

      ......
  }

我們從源碼能夠看出它首先創建了一個Transmitter,接着開啓了一個while死循環,進入循環後首先判斷當前請求是否已經被取消,如果沒有接着交給上層攔截器處理返回response,如果執行過程中有異常則執行catch語句,並且continue繼續循環。如果沒有產生異常的話接着執行之後的代碼,但是看後續的邏輯之前我們先看下最開始生成的Transmitter是什麼?有什麼用?怎麼來的?所以帶着這些疑問我們先分析下這個Transmitter的來龍去脈。

  • Transmitter是什麼?它的作用是什麼?

首先我們需要分析清楚我們爲什麼要去了解這個Transmitter,所以首先我們瞭解下他是幹什麼的?爲什麼後續很多操作都和它息息相關?它有什麼用?所以我們進入它的源碼。

Bridge between OkHttp's application and network layers. This class exposes high-level application layer primitives: connections, requests, responses, and streams. 

源碼中的第一句註釋就回答了我們的問題:這個Transmitter它是OkHttp應用程序和網絡層之間的橋樑。它封裝了應用的連接、請求、響應和流。 

所以我們知道了它主要用於管理客戶端與服務器之間的連接,同時管理連接池,以及請求成功後的連接釋放等操作,我們在它內部也看到了socket的身影,所以這裏我們也能看出okhttp內部其實也是基於socket,只不過對它進行封裝,讓我們更易用而已。所以這裏大致瞭解下Transmitter這個東西就行了,繼續分析下去就是socket的東西了,本文就不再進行進一步分析了。

  • Transmitter怎麼來的?

Transmitter transmitter = realChain.transmitter(); 

所以我們接着去看看realChain是怎麼返回這個transmitter的

  private final Transmitter transmitter;

  public RealInterceptorChain(List<Interceptor> interceptors, Transmitter transmitter,
      @Nullable Exchange exchange, int index, Request request, Call call,
      int connectTimeout, int readTimeout, int writeTimeout) {
    ...
    this.transmitter = transmitter;
    ...
  }

 可以看到在創建這個RealInterceptorChain的時候就對transmitter進行了初始化

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.transmitter = new Transmitter(client, call);
    return call;
  }

繼續尋找,終於發現原來我們在創建Call的時候就已經初始化了Transmitter。好了,現在已經大致弄清楚了Transmitter的問題,接着回到我們的主線,如果在攔截器處理過程中沒有拋出異常的話做了些什麼。

      Exchange exchange = Internal.instance.exchange(response);
      //配置路由
      Route route = exchange != null ? exchange.connection().route() : null;
      //重定向方法
      Request followUp = followUpRequest(response, route);
      ...
    

可以看到,首先配置了路由,接着傳入路由進入followUpRequest這個方法來處理重定向操作,並返回了一個Request followUp

private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
    if (userResponse == null) throw new IllegalStateException();
    int responseCode = userResponse.code();

    final String method = userResponse.request().method();
    switch (responseCode) {
      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);

      case HTTP_UNAUTHORIZED:
        return client.authenticator().authenticate(route, userResponse);

      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
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_MOVED_TEMP:
      case HTTP_SEE_OTHER:
        // Does the client allow redirects?
        if (!client.followRedirects()) return null;

        String location = userResponse.header("Location");
        if (location == null) return null;
        HttpUrl url = userResponse.request().url().resolve(location);

        // Don't follow redirects to unsupported protocols.
        if (url == null) return null;

        // If configured, don't follow redirects between SSL and non-SSL.
        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.request().url(), url)) {
          requestBuilder.removeHeader("Authorization");
        }

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

      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 (!client.retryOnConnectionFailure()) {
          // The application layer has directed us not to retry the request.
          return null;
        }

        RequestBody requestBody = userResponse.request().body();
        if (requestBody != null && requestBody.isOneShot()) {
          return null;
        }

        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, 0) > 0) {
          return null;
        }

        return userResponse.request();

      case HTTP_UNAVAILABLE:
        if (userResponse.priorResponse() != null
            && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
          // We attempted to retry and got another timeout. Give up.
          return null;
        }

        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          // specifically received an instruction to retry without delay
          return userResponse.request();
        }

        return null;

      default:
        return null;
    }
  }

我們可以看到,它內部其實是通過response的http的code請求碼來判斷是否需要重定向的,如果需要就將url重新構建並且重新創建request將其返回。


      if (followUp == null) {
        if (exchange != null && exchange.isDuplex()) {
          transmitter.timeoutEarlyExit();
        }
        return response;
      }

      RequestBody followUpBody = followUp.body();
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response;
      }

      closeQuietly(response.body());
      if (transmitter.hasExchange()) {
        exchange.detachWithViolence();
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }

      request = followUp;
      priorResponse = response;

可以看到在處理完了重定向操作後,執行了相關的if判斷,如果其中沒有導致return的操作,剛剛通過重定向處理得到的followUp這個request又會重新被賦值給request。然後這個新的request就繼續開始新一輪的循環,如此往返完成了我們網絡請求的重定向,這裏就是okHttp重試機制的大體實現。

現在我們簡單梳理一下okHttp是怎麼通過RetryAndFollowUpInterceptor這個攔截器來實現重試、重定向功能的。這裏簡化了源碼,可以更好的幫助我們理解整個攔截流程。

public Response intercept(Chain chain) throws IOException {
    //取得當前攔截鏈上的request
    Request request = chain.request();
    //將chain向下轉型爲對應的RealIntercepterChain
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    //它主要用於管理客戶端與服務器之間的連接,同時管理連接池,以及請求成功後的連接釋放等操作
    Transmitter transmitter = realChain.transmitter();

    while (true) {
      //準備創建攜帶request的請求流
      transmitter.prepareToConnect(request);
      //首先檢查當前請求是否被取消
      if (transmitter.isCanceled()) {
        throw new IOException("Canceled");
      }

      try {
        //交給上層攔截器處理過程中如果拋出異常則執行相應的catch語句,成功則設置標誌位success = true
        response = realChain.proceed(request, transmitter, null);
        success = true;
      } catch (XXException e) {
        ...
        //如果有異常就返回又開始循環
        continue;
      }
    //處理重定向並返回request
    Request followUp = followUpRequest(response, route);
    //將重定向處理後返回的followUp這個request賦值給request
    request = followUp;
    //接着繼續用新的request繼續循環開始重試操作
    }
  }

總結:首先從Chain這個接口中取得我們的請求request,接着創建了Transmitter(這個用來連接okhttp和網絡層的橋樑來處理兩者的通信),然後準備好發送請求,接着檢查當前請求是否被取消,開始執行攔截器的處理,如果有異常則進行catch並重新開始循環,如果成功則調用處理請求重定向的方法followUpRequest去判斷是否需要重定向,這個重定向的判斷是根據HTTP返回碼code來判斷的,如果需要重定向則將url重新構建並封裝到新的request進行返回,拿到follow這個請求後,判斷它是否爲null,爲null的話就說明不需要進行重定向,則跳出循環,如果不爲null則將其賦值給request,又一次進入while循環開始網絡的重新請求。 

  • BridgeInterceptor

Bridges from application code to network code. First it builds a network request from a user request. Then it proceeds to call the network. Finally it builds a user response from the network response.

 從源碼中的介紹,我們瞭解到這個BridgeInterceptor就是應用程序代碼到網路代碼的一個橋樑,首先以用戶的請求爲基礎封裝一個網絡請求,然後開始請求網絡,在有響應後把網絡的響應封裝成面向用戶的response。

所以從這裏我們基本就能夠知道請求的相關信息和響應的相關信息應該都是由BridgeInterceptor分別封裝好後,分發給不同角色(用戶——網絡),從而架起了app和網絡的橋樑。然後我們接下來繼續分析它內部是怎樣的。

  public Response intercept(Chain chain) throws IOException {
    //取出user的request,以下簡稱userRequest
    Request userRequest = chain.request();
    //構建一個面向網絡的request(這個request是真正提交給網絡層的),以下簡稱netRequest
    Request.Builder requestBuilder = userRequest.newBuilder();

    //取出request body
    RequestBody body = userRequest.body();
    if (body != null) {
      //取得body的類型(常見有json等)
      MediaType contentType = body.contentType();
      if (contentType != null) {
        //不爲null則將其添加到netRequest的header中
        requestBuilder.header("Content-Type", contentType.toString());
      }

      //取得body的length,如果我們知道寫進Call裏的字節數目則將其返回,如果沒有則返回-1
      long contentLength = body.contentLength();
      if (contentLength != -1) {
        //如果Call的bytes裏有數目則爲netRequest添加又一個header body的長度
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        //同理如果header中包含的參數是"Transfer-Encoding",則說明body中不含有任何字節
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }

    //如果userRequest中Host參數爲null,則從url中取出其host併爲其賦值
    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }

    //如果header中以Connection爲name映射的value爲null的話則,往header加入keep-Alive參數
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    //如果沒有設置"Accept-Encoding"和"Range"這兩個Http響應頭,則默認使用gzip
    //反之如果設置了"Accept-Encoding",就算設置爲gzip,也不會使用gzip進行解壓縮,因爲transparentGzip這個標誌位只有進入了這個判斷才能被設置爲true
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

    //從cookieJar中取得cookies,如果cookie不爲空則爲header加上
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }

    //判斷是否需要添加user-Agent
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }

    /**
    * 接下來是對response的處理
    */
    Response networkResponse = chain.proceed(requestBuilder.build());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    //判斷是否需要用Gzip對響應結果進行解碼(就是根據請求中request的"Accept-Encoding"方式來確定)
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      //將body的source交給Gzip處理,返回經過Gzip處理的body
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      //如果使用Gzip來進行解壓的話則移除以下兩個頭
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      //最後的body是經過Okio.buffer處理後的
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    //返回處理經過BridgeInterceptor處理的response
    return responseBuilder.build();
  }

這個BridgeInterceptor的intercept()方法,我已經在源碼裏面寫滿了註釋,也是很好理解的,接下來分享一下在閱讀過程中發現的一點有意思的地方。

    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
  • Range請求頭

 用於請求頭中,指定第一個字節的位置和最後一個字節的位置

常見方式 要下載一個201字節大小的文件,我們可以通過Range把它分爲兩段,進而實現多線程異步同時下載

Range: bytes=0-100,101-200

如果服務器響應正常的話會返回這個:HTTP/1.1 206 OK

經過學習發現Range就是實現斷點續傳以及單文件分字節並行下載的關鍵。與之對應的它還有個兄弟content-Range返回頭,以上述寫的爲例,如果第一段0-100個字節已經下載完畢,響應頭response header裏會攜帶Content-Range: bytes 0-100/201進行返回,表示總共大小201字節的內容,其中0-100字節已經下載完畢了。好了,本文對Range這個請求頭的解釋就到這裏,不再繼續延伸了。

  • 發現的一個關於gzip的小細節

爲什麼手動給請求頭設置了addHeader("Accept-Encoding", "gzip") ,okhttp就不會自動解壓gzip流了???

先看看這個看似特別奇怪的問題,誒,是不是突然發現我們剛剛閱讀源碼的過程中已經發現答案了,下面我貼出代碼。 

 if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }

if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      //gzip對body的壓縮處理
      ...
    }

我們先看一下上面這一小段代碼, 再結合問題,發現如果要求okhttp自動解壓gzip流的話就要進入下面一個if判斷,所以我們看看滿足什麼條件才能進入。第一條:transparentGzip這個標誌位首先得爲true,然後我們發現這個標誌位默認是false的,只有進入了上面那個判斷後纔會被設置爲true。所以輕鬆找到這個問題的答案:不是因爲他沒添加addHeader("Accept-Encoding", "gzip")這行代碼,正是因爲添加了之後"Accept-Encoding"不爲null了,所以導致標誌位沒有被設置爲true,所以okhttp不會自動去解壓gzip流。原來這個錯誤是因爲我們的畫蛇添足,至於okhttp爲何如此設計,我暫時還沒找到原因,這個問題留着後續再說。

好了,現在總結一下我們這個BridgeInterceptor就是給我們的請求(request)和響應(response)添加各種頭,並且在這個攔截器中也用gzip對response的body進行了解壓處理。至於gzip解壓的具體步驟本文就不再深入了。

  • CacheInterceptor

在介紹這個攔截器之前,我們先看看http的緩存協議

 

http緩存機制
http緩存機制

 

 上面我們看到了請求過程中緩存策略對請求方式的影響,就http的緩存機制來說,他的緩存策略完全是在頭部體現的,而這個策略根據是否需要向服務器重新發起HTTP請求將緩存過程分爲兩個部分,分別是強制緩存對比緩存。

  • 強制緩存 

強制緩存的基本原理是:所請求的數據在緩存數據庫中尚未過期時,不與服務器進行交互,直接使用緩存數據庫中的數據。當緩存未命中時,則重新向服務器請求數據。控制強制緩存的有兩個字段:cache-control和expires

  • expires 字段規定了緩存的資源的過期時間,在此時間之前,緩存中的資源都是有效的,該字段的 value 是一個格林威治時間格式(GMT)的時間,即世界標準時間,如 Thu, 25 Apr 2019 18:21:32 GMT。缺點很明顯,時間期限是服務器生成,存在着客戶端和服務器的時間誤差,固定時間,HTTP 1.0時的規範。相比較cache-control優先級較低。 
  • Cache-Control

max-age 設置緩存存儲的最大週期,超過這個時間緩存被認爲過期(單位秒)。與Expires相反,時間是相對於請求的時間。如上面截圖中 max-age = 1800 則表示從現在開始的1800s/60 = 30min 內,緩存都有效。

public 表明響應可以被任何對象(包括:發送請求的客戶端,代理服務器,等等)緩存,即使是通常不可緩存的內容

private 表明響應只能被單個用戶緩存,不能作爲共享緩存(即代理服務器不能緩存它)。私有緩存可以緩存響應內容

no-cache 告訴瀏覽器、緩存服務器,不管本地副本是否過期,使用資源副本前,一定要到源服務器進行副本有效性校驗

no-store 強制緩存對比緩存都不會緩存有關客戶端請求或服務器響應的任何內容

所以看完Cache-Control的規則我們也明白了爲什麼緩存策略分爲了強制緩存和對比緩存兩種,而不是單純的使用強制緩存,其原因就是當緩存失效(max-age超時)或者無緩存 (no-cache)需要重新去服務器請求數據,所以需要使用對比緩存策略。

  • 對比緩存 

 當強緩存過期未命中必須每次請求驗證資源的狀態時,便使用對比緩存的方式去處理緩存文件。
對比緩存主要原理是從緩存數據庫中取出緩存的標識,然後向瀏覽器發送請求驗證請求的數據是否已經更新,如果已更新則返回新的數據,若未更新則使用緩存數據庫中的緩存數據所以我們這裏先看下相應的響應頭參數中有哪些緩存標識。在上圖http緩存機制中,我們看到當緩存過期時,有etag和last-modified兩個字段作爲緩存標識來做判斷。

  • Etag

 服務器爲當前資源根據自己的規則生成的一個唯一標識,在第一次請求時會將該值返回,當客戶端下次向服務端請求該資源時,會帶上If-None-Match請求頭,其值爲上一次請求時服務器返回的Etag響應頭的值。服務器收到請求後如果發現有請求頭If-None-Match則與被請求資源的唯一標識進行對比,如果相同則說明資源沒有改變,則返回304,response將會從緩存中拿取,如果不同則說明資源發生了改變,則返回200,並重新進行請求。

  • last-modified

這個字段表示最後一次該資源修改的時間。它和etag類似,第一次請求返回該值,服務器收到請求後如果發現有請求頭If-Modified-Since則與被請求資源的最後修改時間進行比對。若資源的最後修改時間晚於If-Modified-Since,說明資源有被改動過,則重新請求響應完整的資源內容,返狀回態碼爲200;若資源的最後修改時間早於或等於If-Modified-Since,說明資源未被修改,則響應HTTP狀態碼爲304,告知客戶端繼續使用所保存的緩存。

總結:發起請求,首先檢查是否有緩存,通過cache-control和expires這兩個字段查看強制緩存是否過期,如果未過期則直接使用緩存並返回200(from disk cache),如果過期了需要去驗證資源的的狀態時則應用對比緩存策略,通過etag和last-modified兩個字段作爲緩存標識來做判斷,同時客戶端也會在請求中帶上If-None-Match和If-Modified-Since去和服務端的標識進行對比,如果資源沒有被修改則返回304,並使用緩存內容,如果發現資源已經被修改了,則重新請求,並返回200。

接下來回到我們的緩存攔截器來看下CacheInterceptor 的 intercept方法

  public Response intercept(Chain chain) throws IOException {
    //這個cache是internalCache的一個實例
    //其中是使用DiskLruCache來實現緩存的
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //獲取緩存策略
    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()); // The cache candidate wasn't applicable. Close it.
    }

    //如果禁止使用網絡或者緩存不足則拋出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();
    }

    // 如果不需要網絡請求則直接使用緩存的response進行返回
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //如果沒有緩存則交給之後的攔截器執行
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    //如果response有cache,先將header合併再update response
    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();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //如果cache這個對象不爲null
    if (cache != null) {
      //response有body,緩存策略允許緩存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        //將response放進cache中
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //"POST""PATCH"PUT""DELET"MOVE"如果是這幾個方法則移除緩存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    return response;
  }

我們從上面的源碼可以看出,intercept()這個方法就是根據緩存策略做出不同的響應操作,所以我們看看okhttp的緩存策略和http的緩存策略有什麼區別。分析完了緩存策略後,我們再分析它的緩存實現。

首先cachestrategy是一個工廠方法創建的,看看創建它的工廠方法

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

如果cacheResponse緩存不爲null,取出header,在這裏我們看到了熟悉的參數“Expires” ,“Last-Modified”,“ETag”,這正是跟http的緩存對應的參數,接着進一步看看它的獲取。

public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();

      //對應外部返回504的處理
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

      return candidate;
    }

private CacheStrategy getCandidate() {
      
      //如果沒有緩存響應,返回一個沒有響應的策略
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      //如果是https,丟失了握手,返回一個沒有響應的策略
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // 響應不能被緩存
      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      //獲取請求頭裏面的CacheControl
      //這裏的cachecontrol對應着http策略裏的cachecontrol
      CacheControl requestCaching = request.cacheControl();
      //如果請求裏面設置了不緩存,則不緩存
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();

      long ageMillis = cacheResponseAge();
      long freshMillis = computeFreshnessLifetime();

      if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      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);
    }

從上面的代碼中可以看到,okhttp代碼中的緩存策略和http的緩存策略是一樣的,就不再進行贅述了,接下來分析Cache的緩存實現。首先回到我們的intercept方法


    //如果cache這個對象不爲null
    if (cache != null) {
      //response有body,緩存策略允許緩存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        //將response放進cache中
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //"POST""PATCH"PUT""DELET"MOVE"如果是這幾個方法則移除緩存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }

先看看這段代碼,其中有很重要的一行

CacheRequest cacheRequest = cache.put(response);

我們可以看到cache實際是通過這個put方法放進去的,所以我們接下來的重點就是分析put這個方法,以及更重要的調用put方法的這個cache對象所屬的類,充當的是什麼角色,用什麼來實現的,所以懷着這些疑問我們進入cache的源碼。

final DiskLruCache cache;

 一進入,我們發現cache其實就是一個DiskLruCache的實例,所以okhttp所以基於cache的操作基本都是以DiskLruCache來實現的,同時也正因爲LRU算法是緩存的靈魂,所以我打算單獨再開一篇文章來詳細介紹,在這裏就不展開了。

現在我們再回過頭來看CacheInterceptor這個攔截器,首先我們需要先理解Http的緩存機制,因爲okhttp的緩存也就是基於該機制來通過代碼表示各種策略,然後用DiskLruCache進行cache的緩存。

ConnectInterceptor

 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();
    Transmitter transmitter = realChain.transmitter();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);

    return realChain.proceed(request, transmitter, exchange);
  }
}

上面的代碼就是整個ConnectInterceptor的全部代碼了,代碼這麼簡單,它又是怎麼實現那些功能的呢,所以這裏我又去看了下okhttp對連接的處理及相關優化。

  1. 支持HTTP2/SPDY
  2. socket自動選擇最好路線,並支持自動重連
  3. 擁有自動維護的socket連接池,減少握手次數,降低響應延遲
  4. 擁有隊列線程池,輕鬆寫併發

 所以我對它內部的處理充滿了好奇,怎樣實現這些的,帶着這個問題我回頭看intercept方法,發現了我們之前在分析重定向攔截器時出現的Transmitter,所以我們這裏必須進入Transmittrer深入分析一下它對整個連接過程的影響,起到的作用。

待續。。。

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