淺談OkHttp

本來想看看源碼提高下自己的水平,以前也有看過某些框架的源碼,但是基本都是只看某段,這次打算好好的研究一遍。本來是想拿RecyclerView的源碼來開刀的,但是好像RecyclerView的代碼沒那么容易看懂,而且變量還賊多,還大量用了設計模式,講真,看起來還真覺得挺費勁的。既然前段時間寫了Http,那我就拿okhttp的代碼來看看好了。

一.Okhttp

Android 4.4之后,HttpURLConnection底層實現已被OkHttp替換,okhttp內部依賴okio,現在的最新版本應該還是OkHttp3。其實我并直接的用過OkHttp來做網絡請求的操作,我用的是Retrofit,基于OkHttp,其實差不多。

1.簡單調用

一個簡單的異步GET請求的話可以這樣寫

OkHttpClient mOkHttpClient = new OkHttpClient();  
final Request request = new Request.Builder()  
                .url("https://www.baidu.com/")  
                .build();  
Call call = mOkHttpClient.newCall(request);  
call.enqueue(new Callback());
2.okhttp的原理

我的總結就是獲取到請求Call,進行一系列的Interceptors鏈的操作,異步的時候會涉及到Dispather的操作。

在網上很好的找到了一張圖來說明這個過程(出自https://blog.piasy.com/2016/07/11/Understand-OkHttp/

我覺得圖中最核心的三個部分是


二.淺談Okhttp執行流程

同步和異步在調用時的區別就是同步調用的是execute方法,異步調用的是enqueue方法。

1.OkHttpClient

這個類用了Builder模式

  private OkHttpClient(Builder builder) {
    this.dispatcher = builder.dispatcher;
    this.proxy = builder.proxy;
    this.protocols = builder.protocols;
    this.connectionSpecs = builder.connectionSpecs;
    this.interceptors = Util.immutableList(builder.interceptors);
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    this.proxySelector = builder.proxySelector;
    this.cookieJar = builder.cookieJar;
    this.cache = builder.cache;
    this.internalCache = builder.internalCache;
    this.socketFactory = builder.socketFactory;

    boolean isTLS = false;
    for (ConnectionSpec spec : connectionSpecs) {
      isTLS = isTLS || spec.isTls();
    }

    if (builder.sslSocketFactory != null || !isTLS) {
      this.sslSocketFactory = builder.sslSocketFactory;
      this.certificateChainCleaner = builder.certificateChainCleaner;
    } else {
      X509TrustManager trustManager = systemDefaultTrustManager();
      this.sslSocketFactory = systemDefaultSslSocketFactory(trustManager);
      this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
    }

    this.hostnameVerifier = builder.hostnameVerifier;
    this.certificatePinner = builder.certificatePinner.withCertificateChainCleaner(
        certificateChainCleaner);
    this.proxyAuthenticator = builder.proxyAuthenticator;
    this.authenticator = builder.authenticator;
    this.connectionPool = builder.connectionPool;
    this.dns = builder.dns;
    this.followSslRedirects = builder.followSslRedirects;
    this.followRedirects = builder.followRedirects;
    this.retryOnConnectionFailure = builder.retryOnConnectionFailure;
    this.connectTimeout = builder.connectTimeout;
    this.readTimeout = builder.readTimeout;
    this.writeTimeout = builder.writeTimeout;
  }

里面也是有大量的屬性,基本都是和整個請求過程中相關的參數,比如dns 啊,connectTimeout 啊這些,所以我把它看成是一個抽象整個請求過程的對象。而且它的注釋也說了



就是一個Call的工廠,Call是什么之后會講。

2.Request與Response

請求時會創建一個Request對象。這兩個東西其實不用說就知道一個代表請求,一個代表響應。
看看這個Request其實也是在內部用了Builder模式



記住這個請求有五個屬性,地址,方法(就是GET這些),tag表示的是標志,不知道是干啥的,好像是取消請求的時候會用到的。然后還有請求頭和請求體。
(1)headers請求頭
它有個參數,20個長度的列表,保存的是請求頭相關的參數



比如addHeader方法,會調用Headers的add方法。
image.png

這個checkNameAndValue是判斷key和value是否符合,先不用管。

然后看到addLenient中是先加name再加value,所以到這里你就能知道namesAndValues的數組是怎么存值的。
其實這些都不是很重要,簡單了解一下就行。

(2)RequestBody請求體



它是一個抽象類,然后實體類,其實我這里主要是為了看這個,驗證我上一篇講http的請求體



可以看到請求的body是分普通情況和文件的情況。
這些都不重要。
3.Call

這個就是其中一個重要的東西了。
可以看到是用OkHttpClient的newCall方法創建Call,也驗證了OkHttpClient就是Call的工廠。



Call是一次請求的抽象。既然是抽象,那就需要一個實現,那就是這里的RealCall,記住它代表一次請求,它代表一次請求,它代表一次請求,這一聽是沒什么,但是它這樣的一個思想我覺得是很好的,你想想一次請求是什么,是一個行為,一個過程,它這里把行為進行抽象,從而方便于進行管理與操作。
RealCall有4個屬性

(1)它這里引用了OkHttpClient,我覺得是因為OkHttpClient里面保存了和整個過程相關的參數。但是這樣相互引用的話耦合度會不會有點高。
(2)RetryAndFollowUpInterceptor是一個攔截器,后面會說。
(3)executed用來記錄請求是否執行,一個請求只能執行一次
(4)originalRequest就是傳進來的Request。

這個請求類,它的行為主要是一些請求的操作,比如開始同步請求,開始異步請求這些。所以如果你想對“請求”做什么操作,可以先來這個類來找找有沒有對應的方法,比如取消請求,就在這里。

看看執行同步請求的方法。



第一個判斷請求是否執行夠,因為一個請求只允許執行一次。主要的操作是這3行

client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
client.dispatcher().finished(this);

這里有涉及到client.dispatcher(),這是OkHttpClient的一個參數,看看是個啥子。Dispatcher,它又是一個okhttp里面比較重要的對象,表示檢測者,負責監視整個請求的過程,它內部有很多的參數。


最大請求數maxRequests,最大主機請求數maxRequestsPerHost(好吧,其實我也不懂這是啥),有個Runnable,然后ExecutorService線程池,還有三個隊列readyAsyncCalls、runningAsyncCalls和runningSyncCalls。
我講講這三個隊列的意思,runningSyncCalls是存儲同步請求,也就是Call(其實這里我有點蒙,你想想,同步請求如果有多個的話肯定是個排隊請求的過程,而每個請求結束后都會移除隊列,那這樣的話這個隊列不是一直都只有當前進行的請求嗎,這樣做有什么意義)。runningAsyncCalls是存儲異步請求的隊列,這個很有必要,因為異步是同時進行的,所以可以用個隊列來存儲,readyAsyncCalls表示準備進行異步請求的隊列,因為有最大請的限制。

扯多了,看看同步請求時的executed方法和finished方法。



executed就是把請求添加到同步請求隊列。



finish就是把請求從隊列中移除。這個run()方法我不知道,我沒找到在哪。

看完同步再來看看異步



異步的具體操作是寫在Dispatcher里面



判斷當前異步的隊列數量做比較,超過了最大值就把Call添加到準備隊列,否則添加到執行隊列并執行線程,而AsyncCall繼承了Runnable,所以executorService().execute(call)會執行call的run方法。這個run在內部又調用了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 {
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

這個也不難看懂吧,主要兩行代碼

Response response = getResponseWithInterceptorChain();
client.dispatcher().finished(this);

finished和上面的一樣講過了,不同的是在里面多調用了個promoteCalls方法,其實就是對runningAsyncCalls、readyAsyncCalls兩個隊列做操作,也不難看懂,影響不大。


總結一下,不管是同步還是異步,做的操作都是 添加進隊列——>調用getResponseWithInterceptorChain()方法——>從隊列中移除。

到這里是不是就很容易看出getResponseWithInterceptorChain()就是請求網絡整個過程的核心操作。

4.getResponseWithInterceptorChain()方法

我覺得這個就是整個okhttp里面最核心的操作,這個方法就是按順序調用個個攔截器對請求進行處理。說得好聽一點,這個叫責任鏈模式。

什么是責任鏈模式,不懂的我覺得有必要百度一波。簡單點說就是一個請求就來,你對這個請求做處理或者不做處理,然后傳給下一個人,一個一個的傳。不了解的話不太容易看懂我下面的一些話。

getResponseWithInterceptorChain方法是RealCall類里面的,也可以說,這個請求執行了責任鏈的一系列操作。

interceptors表示存儲攔截器的列表,進行添加一些列的攔截器操作之后,創建攔截器鏈對象,然后執行proceed方法就能返回請求的響應Response。

那么重心又到了RealInterceptorChain這個鏈對象的身上。


先要注意一下它的這些參數。
(1)interceptors是攔截器列表
(2)streamAllocation 一開始傳空進來
(3)httpStream 一開始傳空進來
(4)connection 一開始傳空
(5)index表示當前鏈的一個下標。我上邊也說了,責任鏈執行操作后丟給下一個執行,這里就是每次丟給下一個攔截器的時候index加一。所以初始肯定是0
(6)request就是請求,包含什么參數之前也有寫。

簡單了解一下參數后看看proceed,看看具體的請求數據的流程是咋樣的。

public Response proceed(Request request, StreamAllocation streamAllocation, HttpStream httpStream,
      Connection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpStream != null && !sameConnection(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpStream != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpStream, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpStream != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    return response;
  }

先這樣,所有關于報錯的判斷我們先不管,抽出核心的代碼。

 RealInterceptorChain next = new RealInterceptorChain(
        interceptors, streamAllocation, httpStream, connection, index + 1, request);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

它先創建一個新的責任鏈對象,然后下標加1,然后讓第一個攔截器Interceptor 執行intercept操作,其實這個操作里面會調用新的責任鏈的proceed方法,這樣就使得整個責任鏈模式給運作起來。其實我是感覺這個有點繞的,反正就是創建一個新的鏈對象,把鏈對象傳給攔截器,然后攔截器里面又調用鏈對象的proceed方法,這個方法又重復這些操作直到最后一個攔截器。

稍微理清楚后來看看這個鏈上攔截器的一個順序
其實就是列表中的順序


先添加用戶自定義的攔截器,這個我們后面再說吧。然后按這樣的順序添加
(1)RetryAndFollowUpInterceptor重試和重定向攔截器
(2)BridgeInterceptor 橋梁攔截器(我喜歡叫它配置請求攔截器)
(3)CacheInterceptor 緩存攔截器
(4)ConnectInterceptor 連接攔截器
(5)interceptors.addAll(client.networkInterceptors()); 是添加用戶定義的網絡請求攔截器(所以可以看出用戶可以定義兩種攔截器)
(7)CallServerInterceptor 內部的網絡攔截器

因為我也并不是能完全的看懂這些攔截器內部的所有代碼,所以我就不講攔截器內詳細做的操作了,免得誤人子弟,我當時是看了這篇文章
https://blog.csdn.net/qq_19431333/article/details/53207220
其實還是看不懂詳細的代碼。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容