OkHttp原始碼分析

BlackFlagBin發表於2018-05-10

在現在的Android開發中,請求網路獲取資料基本上成了我們的標配。在早期的Android開發中會有人使用HttpClient、HttpUrlConnection或者Volley等網路請求方式,但對於如今(2018年)而言,絕大多數的開發者都會使用OkHttp+Retrofit+RxJava進行網路請求,而對於這三者而言,實際請求網路的框架是OkHttp,所以OkHttp的重要性不言而喻。

OkHttp的基本用法

//建立OkHttpClient物件
OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
   //建立Request請求物件
  Request request = new Request.Builder()
      .url(url)
      .build();

   //建立Call物件,並執行同步獲取網路資料
  Response response = client.newCall(request).execute();
  return response.body().string();
}
複製程式碼

使用OkHttp基本是以下四步:

  • 建立OkHttpClient物件
  • 建立Request請求物件
  • 建立Call物件
  • 同步請求呼叫call.execute();非同步請求呼叫call.enqueue(callback) 接下來我會對這四步進行詳細的說明。

建立OkHttpClient物件

通常來說,我們使用OkHttp並不會直接通過new OkHttpClient()來建立出一個OkHttpClient。一般來說,我們會對這個OkHttpClient做一些配置,比如:

OkHttpClient.Builder().connectTimeout(
                DEFAULT_MILLISECONDS, TimeUnit.SECONDS).readTimeout(
                DEFAULT_MILLISECONDS, TimeUnit.SECONDS).addInterceptor { chain ->
            val builder = chain.request().newBuilder()
            headerMap?.forEach {
                builder.addHeader(it.key, it.value)
            }
            val request = builder.build()
            chain.proceed(request)
        }.addInterceptor(httpLoggingInterceptor).build()
複製程式碼

上面是一段使用Kotlin程式碼建立OkHttpClient的過程,很明顯,OkHttpClient內部是使用了 Builder 模式,好處很明顯: 我們在建立物件的同時可以自由的配置我們需要的引數 。我們簡單看一下OkHttpClient內部類Builder中的構造方法,看一下OkHttpClient內部都可以做哪些配置:

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;
      //預設連線超時10s
      connectTimeout = 10_000;
      //預設讀取超時10s
      readTimeout = 10_000;
      //預設寫入超時10s
      writeTimeout = 10_000;
      pingInterval = 0;
    }
複製程式碼

上面的程式碼中我們非常熟悉的就是連線超時、讀取超時、寫入超時,它們的預設事件都是10s,其實這也提醒我們,如果我們想要設定的超時時間也是10s的話,完全沒有必要重複進行配置,其實我的建議也是不需要配置,直接使用預設的就好。值得注意的是 Dispatcher 這個類,這是一個網路請求的分發器,主要作用是在同步,非同步網路請求時會做一些不同的分發處理,我們先有個印象即可, Dispatcher 會在之後詳細的分析。

可能細心的小夥伴這時候會說了:我平時會對OkHttpClient加上一些interceptor來攔截網路請求,比方說在請求之前加上token等請求頭之類的,上面這段程式碼為什麼沒有攔截器相關的變數呢?

沒錯,OkHttpClient中的Builder類內部確實是有攔截器相關成員變數,只不過沒寫在Builder的構造方法內:

public static final class Builder {
    //省略無關程式碼......
    final List<Interceptor> interceptors = new ArrayList<>();
    final List<Interceptor> networkInterceptors = new ArrayList<>();
    //省略無關程式碼......
}
複製程式碼

我們平常新增的interceptor就存放在interceptors這個ArrayList中。OkHttpClient物件的配置建立不是什麼難以理解的點,接下來我們看Request物件的建立。

建立Request請求物件

為什麼要建立Request物件,很簡單,我們請求網路需要一些必要的引數,比如url,請求方式是get或者post等等資訊。而Request這個類就是對這些網路請求引數的統一封裝。看一下程式碼就一目瞭然了:

public final class Request {
  final HttpUrl url;
  final String method;
  final Headers headers;
  final @Nullable RequestBody body;
  final Object tag;

  private volatile CacheControl cacheControl; // Lazily initialized.
  //省略無關程式碼......
}
複製程式碼

相信大家都能看明白,這個Request類中封裝了url、請求方式、請求頭、請求體等等網路請求相關的資訊。Request裡面也是一個Builder模式,這裡就不贅述了。

建立Call物件

Call物件我們可以這樣理解:Call物件是對 一次 網路請求的封裝。注意這個關鍵字: 一次 ,熟悉OkHttp的同學應該都知道,一個Call物件只能被執行一次,不論是同步execute還是非同步的enqueue,那麼這個只能執行一次的特性是如何保證的呢?我們來看程式碼:

@Override public Call newCall(Request request) {
    //實際上是通過 RealCall.newRealCall 來獲取Call物件
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
複製程式碼

上面的程式碼能看到OkHttpClient的newCall實際上是通過RealCall.newRealCall(this, request, false /* for web socket */)來獲得的,我們來看一下這個RealCall:

final class RealCall implements Call {
  final OkHttpClient client;
  //錯誤重試與重定向攔截器
  final RetryAndFollowUpInterceptor retryAndFollowUpInterceptor;
  //監聽OkHttp網路請求各個階段的事件監聽器
  private EventListener eventListener;
  final Request originalRequest;
  final boolean forWebSocket;
  //判斷Call物件是否被執行過的標誌變數
  private boolean executed;

  private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }

  static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Call只是一個介面,我們實際建立的是Call的實現類RealCall的物件
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
  }
  
  Override public Response execute() throws IOException {
    //確保執行緒安全的情況下通過executed來保證每個Call只被執行一次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
   //省略無關程式碼......
  }
  
  @Override public void enqueue(Callback responseCallback) {
     /確保執行緒安全的情況下通過executed來保證每個Call只被執行一次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    //省略無關程式碼
  }
  //省略無關程式碼......
}
複製程式碼

我們可以看到,Call只是一個介面,我們建立的實際上是RealCall物件。在RealCall中存在一個 execute 的成員變數,在execute()和enqueue(Callback responseCallback) 方法中都是通過 execute 來確保每個RealCall物件只會被執行一次。

建立Call物件的過程其實也是很簡單的,麻煩的地方在最後一步: **execute()和enqueue(Callback responseCallback) **

同步請求與非同步請求

前三步非常簡單,我們可以知道並沒有涉及網路的請求,所以核心肯定是在這關鍵的第四步。

同步請求execute()和非同步請求enqueue(Callback responseCallback)

先說同步請求,看程式碼:

@Override public Response execute() throws IOException {
    //通過executed確保每個Call物件只會被執行一次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    //網路請求開始的回撥
    eventListener.callStart(this);
    try {
      //呼叫分發器的executed(this)方法
      client.dispatcher().executed(this);
      //真實的網路請求是在這裡處理的
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      //網路請求失敗的回撥
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      //網路請求結束
      client.dispatcher().finished(this);
    }
  }
複製程式碼

execute() 方法中首先通過executed確保每個Call物件只會被執行一次,之後呼叫了eventListener.callStart(this);來執行網路請求開始的回撥。接下來呼叫了client.dispatcher().executed(this),那麼這句程式碼具體是做了什麼呢:

public Dispatcher dispatcher() {
    //返回了一個OkHttpClient內部的dispather分發器
    return dispatcher;
  }
複製程式碼

這句程式碼首先返回一個 dispatcher ,這個分發器我們在上面也提到過,這是一個比較重要的概念,來看一下這個分發器:

public final class Dispatcher {
  //最大請求數
  private int maxRequests = 64;
  //每個host的最大請求數
  private int maxRequestsPerHost = 5;
  //網路請求處於空閒時的回撥
  private @Nullable Runnable idleCallback;
  //執行緒池的實現
  private @Nullable ExecutorService executorService;
  //就緒等待網路請求的非同步佇列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
  //正在執行網路請求的非同步佇列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
  //正在執行網路請求的同步佇列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
  
  //忽略無關程式碼......
  
  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }
  
  synchronized void executed(RealCall call) {
    //將call物件加入網路請求的同步佇列中
    runningSyncCalls.add(call);
  }
  
  //忽略無關程式碼......
}

複製程式碼

可以看到 Dispatcher 這個分發器類內部定義了很多的成員變數:maxRequests 最大請求個數,預設值是64; maxRequestsPerHost 每個host的最大請求個數,這個host是什麼?舉個栗子,一個URL為 http://gank.io/api ,那麼host就是 http://gank.io/ 相當於baseUrl。 ** idleCallback** 這是一個空閒狀態時的回撥,當我們的所有的網路請求佇列為空時會執行。 executorService 這是一個執行緒池,主要是為了高效執行非同步的網路請求而建立的執行緒池,之後會再次提到它。接下來就是比較重要的三個佇列:

  • readyAsyncCalls -> 在就緒等待的非同步Call佇列
  • runningAsyncCalls -> 正在執行的非同步Call佇列
  • runningSyncCalls -> 正在執行的同步Call佇列

對於這三個佇列來說,執行同步請求的Call物件會加入到runningSyncCalls中;執行非同步請求的Call物件會加入到readyAsyncCalls或者runningAsyncCalls中,那麼什麼時候加入到等待佇列,什麼時候加入到執行佇列呢?簡單的說,如果執行非同步網路請求的執行緒池很忙,非同步請求的Call物件會加入到等待佇列;反之則加入到執行佇列。那麼這個忙於不忙的標準是什麼呢?很簡單,在enqueue方法中有runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost一個判斷的標準,即正在執行的非同步佇列中Call物件個數小於maxRequests(64)並且執行佇列中的同一個host對應的Call物件個數小於maxRequestsPerHost(5)的時候。

說完了 Dispatcher 關鍵的成員變數,我們來看一下它的 **executed(RealCall call) ** 方法:

 synchronized void executed(RealCall call) {
    //將call物件加入網路請求的同步佇列中
    runningSyncCalls.add(call);
  }
複製程式碼

這是一個synchronized修飾的方法,為了確保執行緒安全。Dispatcher中的executed(RealCall call)方法及其簡單,就是把Call物件加入到同步Call佇列中。對,你沒有看錯,它確實就只有這一行程式碼,沒什麼複雜的操作。

說完了 Dispatcher 中的同步方法,我們再來看一下非同步:

  synchronized void enqueue(AsyncCall call) {
      //判斷Call物件應該新增到等待佇列還是執行佇列
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      //加入執行佇列
      runningAsyncCalls.add(call);
      //執行緒池開啟執行緒執行非同步網路請求
      executorService().execute(call);
    } else {
      //加入等待佇列
      readyAsyncCalls.add(call);
    }
  }
複製程式碼

和同步方法相比,非同步方法中的內容要稍微多一點。首先是判斷Call物件應該新增到等待佇列還是執行佇列,這個判斷上面已經說過。加入執行佇列後,開啟執行緒池並執行Call物件。這裡需要注意的是非同步請求時的Call物件和同步請求時不一樣,會轉換成一個 AsyncCall 物件,這個 AsyncCall 實際上是一個 NamedRunnable ,那既然是一個 Runnable ,我們肯定要看一下它的execute()方法:

@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 {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }
複製程式碼

其實整段程式碼看似非常多,核心就只有Response response = getResponseWithInterceptorChain()這一句:通過攔截器鏈獲取網路返回結果。其實不止是非同步請求,同步請求的核心也是這一行程式碼。我們繼續看一下RealCall中的execute方法:

@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      client.dispatcher().executed(this);
      //跟非同步請求一樣,核心也是通過攔截器鏈來獲取網路資料
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      client.dispatcher().finished(this);
    }
  }
複製程式碼

很明顯,在client.dispatcher().executed(this)將Call物件加入同步請求佇列中之後,同樣呼叫的是Response result = getResponseWithInterceptorChain()。明白了嗎,不論是在同步請求或者是非同步請求,最終獲取網路資料的核心處理都是一致的:getResponseWithInterceptorChain()

接下來我們來分析這個在OkHttp中非常核心的方法:

Response getResponseWithInterceptorChain() throws IOException {
    //建立存放攔截器的list
    List<Interceptor> interceptors = new ArrayList<>();
    //攔截器列表加入我們配置OkHttpClient時新增的攔截器
    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) {
      //如果不是針對WebSocket的網路訪問,加入網路攔截器
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //建立攔截器鏈
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    //執行攔截器鏈
    return chain.proceed(originalRequest);
  }
複製程式碼

在這個方法中首先建立了一個ArrayList,用來存放所有的攔截器。從上到下可以看到,一共是新增了7中不同的攔截器:

  • client.interceptors() -> 我們自己新增的請求攔截器,通常是做一些新增統一的token之類操作
  • retryAndFollowUpInterceptor -> 主要負責錯誤重試和請求重定向
  • BridgeInterceptor -> 負責新增網路請求相關的必要的一些請求頭,比如Content-Type、Content-Length、Transfer-Encoding、User-Agent等等
  • CacheInterceptor -> 負責處理快取相關操作
  • ConnectInterceptor -> 負責與伺服器進行連線的操作
  • networkInterceptors -> 同樣是我們可以新增的攔截器的一種,它與client.interceptors() 不同的是二者攔截的位置不一樣。
  • CallServerInterceptor -> 在這個攔截器中才會進行真實的網路請求

在新增完各種攔截器後,建立了一個攔截器鏈,然後執行了攔截器鏈的proceed方法,我們來看一下這個proceed方法:

@Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }
複製程式碼

這個方法呼叫的是 RealInterceptorChain 內部的另一個proceed方法,再跟進去看一下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
     RealConnection connection) throws IOException {
   //忽略無關程式碼......
   
   // 獲取攔截鏈中的下一個攔截器
   RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
       connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
       writeTimeout);
   Interceptor interceptor = interceptors.get(index);
   //通過呼叫攔截器的intercept方法獲取網路資料
   Response response = interceptor.intercept(next);
   
   //忽略無關程式碼......
   
   return response;
 }
複製程式碼

這個方法中主要分為兩步:獲取攔截鏈中的下一個攔截器,然後呼叫這個攔截器的 intercept(next) 方法,在構建OkHttpClietn時新增過interceptor的同學應該都比較清楚,在intercept()方法中我們必須要存在chain.proceed(request)這樣一句程式碼。類似於這樣:

OkHttpClient.Builder().connectTimeout(
                DEFAULT_MILLISECONDS, TimeUnit.SECONDS).readTimeout(
                DEFAULT_MILLISECONDS, TimeUnit.SECONDS).addInterceptor { chain ->
            val builder = chain.request().newBuilder()
            headerMap?.forEach {
                builder.addHeader(it.key, it.value)
            }
            val request = builder.build()
            //將攔截器鏈執行下去
            chain.proceed(request)
        }.addInterceptor(httpLoggingInterceptor).build()
複製程式碼

每個攔截器內部的intercept方法內部必須存在chain.proceed(request),同樣,OkHttp提供的攔截器的intercept方法內部都必須存在chain.proceed(request)這句程式碼,除了最後一個攔截器CallServerInterceptor。

整個邏輯是不是有些混亂,沒關係,我們來整理一下。

  • getResponseWithInterceptorChain()方法通過呼叫chain.proceed(originalRequest)開啟攔截鏈。
  • 在RealInterceptorChain的proceed(request)方法中會呼叫下一個攔截器中的intercept(chain)方法
  • 在攔截器中的intercept(chain)中會呼叫chain.proceed(request) 上面是一個迴圈呼叫,由於每次獲取的攔截器是攔截器列表中的下一個攔截器,所以實現了順序呼叫攔截器列表中的每個不同的攔截器的攔截方法。因為最後一個攔截器並沒有呼叫chain.proceed(request),所以能夠結束迴圈呼叫。

再來張圖,加深大家對攔截器鏈的理解:

OkHttp的攔截過程

看完這張圖,小夥伴們應該會對整個攔截器鏈的運作流程有一定的瞭解。到此為止,OkHttp的原始碼分析就告一段落了,具體每個攔截器中的實現細節,大家如果有興趣的話可以自己去深入瞭解一下,我這裡就不再贅述了。

相關文章