OkHttp源碼解析

應用攔截器和網(wǎng)絡(luò)攔截器

以前其實就有一直在使用okhttp,也有聽說過攔截器這東西,但是一直沒有去深入了解。最近看《安卓進階之光》剛好看到okhttp攔截器的內(nèi)容,然后自己也去挖了下源碼,才發(fā)現(xiàn)其巧妙之處长搀。

攔截器有兩種,應用攔截器和網(wǎng)絡(luò)攔截器宇弛。用法可以看下面的代碼:

class LogInterceptor implements Interceptor {
    private String mName;

    LogInterceptor(String name) {
        mName = name;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response response = chain.proceed(chain.request());
        Log.d("LogInterceptor", "[" + mName + "] : request url = " + response.request().url() + ", " + response.headers().toString());
        return response;
    }
}

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new LogInterceptor("ApplicationInterceptor"))
        .addNetworkInterceptor(new LogInterceptor("NetworkInterceptor"))
        .build();

Request request = new Request.Builder()
        .url("http://www.github.com")
        .build();

client.newCall(request).enqueue(null);

運行之后的打印如下:

12-29 00:07:02.378 12641-12859/com.example.okhttp D/LogInterceptor: [NetworkInterceptor] : request url = http://www.github.com/, Content-length: 0
    Location: https://www.github.com/
12-29 00:07:03.653 12641-12859/com.example.okhttp D/LogInterceptor: [NetworkInterceptor] : request url = https://www.github.com/, Content-length: 0
    Location: https://github.com/
12-29 00:07:04.889 12641-12859/com.example.okhttp D/LogInterceptor: [NetworkInterceptor] : request url = https://github.com/, Date: Thu, 28 Dec 2017 16:07:05 GMT
    Content-Type: text/html; charset=utf-8
    Transfer-Encoding: chunked
    Server: GitHub.com
    Status: 200 OK
    ...(省略部分打印)
12-29 00:07:04.896 12641-12859/com.example.okhttp D/LogInterceptor: [ApplicationInterceptor] : request url = https://github.com/, Date: Thu, 28 Dec 2017 16:07:05 GMT
    Content-Type: text/html; charset=utf-8
    Transfer-Encoding: chunked
    Server: GitHub.com
    Status: 200 OK
    ...(省略部分打印)

攔截器是一種強大的機制,可以在攔截器中進行監(jiān)視、重寫和重試調(diào)用源请。像我們上面的代碼就對請求進行了監(jiān)視枪芒。

從打印可以看到,網(wǎng)絡(luò)攔截器攔截到了三個請求,同時攔截到了重定向的訪問。而應用攔截器只攔截到了一個請求,同時我們可以看到它攔截到的請求的url是 https://github.com/ 和我們在代碼中的請求 http://www.github.com 并不一致谁尸。

簡單來講,網(wǎng)絡(luò)攔截器在每一次網(wǎng)絡(luò)訪問的時候都會攔截到請求,而應用攔截器只會在OkHttpClient.newCall返回的Call執(zhí)行的時候被調(diào)用一次舅踪。

okhttp的運行流程

在講攔截器的實現(xiàn)之前我們先來簡單介紹一下okhttp的運行流程。

首先通過OkHttpClient.newCall我們可以獲得一個RealCall:

public class OkHttpClient implements Cloneable, Call.Factory {
  ...
  public Call newCall(Request request) {
    return new RealCall(this, request);
  }
  ...
}

異步訪問

RealCall實現(xiàn)了Call症汹。接口,我們通過調(diào)用enqueue方法可以實現(xiàn)異步網(wǎng)絡(luò)訪問硫朦。讓我們直接看看RealCall.enqueue吧:

final class RealCall implements Call {
  ...
  public void enqueue(Callback responseCallback) {
    enqueue(responseCallback, false);
  }

  void enqueue(Callback responseCallback, boolean forWebSocket) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
  }
  ...
}

client.dispatcher()可以獲得一個Dispatcher,它用于網(wǎng)絡(luò)訪問任務的調(diào)度,我們的異步并發(fā)網(wǎng)絡(luò)訪問就是通過Dispatcher實現(xiàn)的贷腕。這里創(chuàng)建了一個AsyncCall,然后將它傳入Dispatcher.enqueue背镇。AsyncCall是RealCall的內(nèi)部類,而且它實際上是一個Runnable:

final class RealCall implements Call {
  ...
  final class AsyncCall extends NamedRunnable {
    ...
  }
  ...
}
public abstract class NamedRunnable implements Runnable {
  ...
  @Override public final void run() {
    String oldName = Thread.currentThread().getName();
    Thread.currentThread().setName(name);
    try {
      execute();
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

NamedRunnable在run方法里面會調(diào)用抽象的execute方法,在這個方法內(nèi)部就會進行實際的網(wǎng)絡(luò)訪問咬展。那Dispatcher.enqueue又做了寫什么呢?其實Dispatcher.enqueue實際上將AsyncCall這個Runnable放到了一個線程池中:

public final class Dispatcher {
  ...
  synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }
  ...
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
  ...
}

一切明了,Call.enqueue實際上是將AsyncCall這個Runnable放到了線程池中執(zhí)行去訪問網(wǎng)絡(luò),而AsyncCall是RealCall的一個內(nèi)部類,它持有RealCall的引用,所以在被線程池調(diào)用的時候可以獲得Request的信息瞒斩。

所以將okhttp的異步流程簡化之后實際上就是Dispatcher中的線程池對Runnable的執(zhí)行:

1.png

然后我們看看AsyncCall.execute的具體實現(xiàn):

final class AsyncCall extends NamedRunnable {
  ...
  @Override protected void execute() {
   boolean signalledCallback = false;
   try {
     Response response = getResponseWithInterceptorChain(forWebSocket);
     if (canceled) {
       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來訪問網(wǎng)絡(luò)獲取Response的破婆。

同步訪問

如果想用OkHttp去阻塞是的訪問網(wǎng)絡(luò)我們可以這樣調(diào)用:

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

這個execute是不是有點眼熟,但它是Call的一個方法,并不是我們上面異步訪問中提到的NamedRunnable.execute:

public interface Call {
  ...
  Response execute() throws IOException;
  ..
}

現(xiàn)在我們來看看具體實現(xiàn):

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

它也是通過getResponseWithInterceptorChain來訪問網(wǎng)絡(luò)獲取Response的。

攔截器的實現(xiàn)

我們在前面的小節(jié)中已經(jīng)知道了,無論是同步還是異步,最終都是通過RealCall.getResponseWithInterceptorChain方法去訪問網(wǎng)絡(luò)的胸囱。但是在查看具體源代碼的時候發(fā)現(xiàn)在okhttp3.4.0-RC1開始其具體的實現(xiàn)細節(jié)有了一些不一樣的地方。所以我這邊分開兩部分來講一講okhttp3.4.0-RC1之前和之后攔截器的具體實現(xiàn)細節(jié)。

okhttp3.4.0-RC1之前的實現(xiàn)

okhttp3.4.0-RC1之前的RealCall.getResponseWithInterceptorChain 中實際上是調(diào)用了ApplicationInterceptorChain.proceed方法去訪問網(wǎng)絡(luò)獲取Response:

private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
  Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
  return chain.proceed(originalRequest);
}

然后繼續(xù)看源碼,可以發(fā)現(xiàn)proceed內(nèi)部會從OkHttpClient獲取序號為index的攔截器,并且創(chuàng)建新的序號加一的ApplicationInterceptorChain傳遞給攔截器去執(zhí)行收津。于是有多少個攔截器就創(chuàng)建了多少個ApplicationInterceptorChain,他們會按照自己的序號調(diào)用對應的攔截器澄耍。這其實就是一種責任鏈模式的實現(xiàn)方式:

@Override public Response proceed(Request request) throws IOException {
  // If there's another interceptor in the chain, call that.
  if (index < client.interceptors().size()) {
    Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
    Interceptor interceptor = client.interceptors().get(index);
    Response interceptedResponse = interceptor.intercept(chain);

    if (interceptedResponse == null) {
      throw new NullPointerException("application interceptor " + interceptor
          + " returned null");
    }

    return interceptedResponse;
  }

  // No more interceptors. Do HTTP.
  return getResponse(request, forWebSocket);
}

如果ApplicationInterceptorChain的序號大于OkHttpClient中注冊的攔截器的數(shù)量,則調(diào)用getResponse方法。這里ApplicationInterceptorChain是RealCall的內(nèi)部類,getResponse調(diào)用的是RealCall.getResponse方法谤职。

再看RealCall.getResponse方法,它內(nèi)部有個while true的死循環(huán),調(diào)用HttpEngine.sendRequest和HttpEngine.readResponse去發(fā)送請求和接收響應,如果出現(xiàn)了RouteException異呈尾颍或者IOException異常則重新嘗試訪問:

Response getResponse(Request request, boolean forWebSocket) throws IOException {
    ...
    while (true) {
    ...
    try {
        engine.sendRequest();
        engine.readResponse();
        releaseConnection = false;
    } catch (RouteException e) {
        HttpEngine retryEngine = engine.recover(e.getLastConnectException(), true, null);
        if (retryEngine != null) {
            releaseConnection = false;
            engine = retryEngine;
            continue;
        }
        throw e.getLastConnectException();
    }catch (IOException e) {
        HttpEngine retryEngine = engine.recover(e, false, null);
        if (retryEngine != null) {
            releaseConnection = false;
            engine = retryEngine;
            continue;
        }
        throw e;
    }
    ...
}

我們繼續(xù)看engine.readResponse的實現(xiàn),可以看到它調(diào)用了NetworkInterceptorChain.proceed方法去獲取響應:

public void readResponse() throws IOException {
...
Response networkResponse;
...
networkResponse = new NetworkInterceptorChain(0, networkRequest,
                streamAllocation.connection()).proceed(networkRequest);
...
}

NetworkInterceptorChain.proceed和ApplicationInterceptorChain.proceed類似,也會不斷的創(chuàng)建新的NetworkInterceptorChain并且調(diào)用網(wǎng)絡(luò)攔截器,如果沒有網(wǎng)絡(luò)攔截器可以調(diào)用了,則會調(diào)用readNetworkResponse方法讀取響應:

@Override public Response proceed(Request request) throws IOException {
...
if (index < client.networkInterceptors().size()) {
    NetworkInterceptorChain chain = new NetworkInterceptorChain(index + 1, request, connection);
    Interceptor interceptor = client.networkInterceptors().get(index);
    Response interceptedResponse = interceptor.intercept(chain);
    ...
    return interceptedResponse;
}
Response response = readNetworkResponse();
...
return response;
}

這里還有一點需要說明的是NetworkInterceptorChain是HttpEngine的內(nèi)部類,它調(diào)用的readNetworkResponse方法實際上是HttpEngine.readNetworkResponse。現(xiàn)在我們就對OkHttp攔截器的請求流程和攔截器的實現(xiàn)原理有了比較全面的了解,下面這張圖對整個流程做一個總結(jié):

2.png

okhttp3.4.0-RC1之后的實現(xiàn)

然后讓我們再來看一下3.4.0-RC1之后的實現(xiàn):

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));

  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

  return chain.proceed(originalRequest);
}

這里已經(jīng)不再區(qū)分ApplicationInterceptorChain和NetworkInterceptorChain了允蜈,統(tǒng)一用RealInterceptorChain去處理:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  ...

  // Call the next interceptor in the chain.
  RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
      connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
      writeTimeout);
  Interceptor interceptor = interceptors.get(index);
  Response response = interceptor.intercept(next);

  ...

  return response;
}

這里將cookie處理冤吨、緩存處理、網(wǎng)絡(luò)連接都作為責任鏈的一部分饶套,比起3.4.0.RC-1之前更加完全的實現(xiàn)了責任鏈模式漩蟆。這里有必要講一下的就是retryAndFollowUpInterceptor, 它是一個RetryAndFollowUpInterceptor實例,它負責重連和重定向我們之前在3.4.0.RC-1之前看到的getResponse的while true就放到了這里來實現(xiàn)妓蛮。

讓我們看看它的整個流程:

3.png

這樣的實現(xiàn)是不是以前要清晰很多怠李?所有的步驟一目了然,看過原來的版本再看看3.4.0.RC-1重構(gòu)后的版本蛤克,的確有一種眼前一亮的驚艷之感扔仓。果然好代碼都是需要一點點優(yōu)化出來的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末咖耘,一起剝皮案震驚了整個濱河市翘簇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌儿倒,老刑警劉巖版保,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異夫否,居然都是意外死亡彻犁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門凰慈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來汞幢,“玉大人,你說我怎么就攤上這事微谓∩瘢” “怎么了输钩?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長仲智。 經(jīng)常有香客問我买乃,道長,這世上最難降的妖魔是什么钓辆? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任剪验,我火速辦了婚禮,結(jié)果婚禮上前联,老公的妹妹穿的比我還像新娘功戚。我一直安慰自己,他們只是感情好似嗤,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布疫铜。 她就那樣靜靜地躺著,像睡著了一般双谆。 火紅的嫁衣襯著肌膚如雪壳咕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天顽馋,我揣著相機與錄音谓厘,去河邊找鬼。 笑死寸谜,一個胖子當著我的面吹牛竟稳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播熊痴,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼他爸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了果善?” 一聲冷哼從身側(cè)響起诊笤,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巾陕,沒想到半個月后讨跟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡鄙煤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年晾匠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梯刚。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡凉馆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情澜共,我是刑警寧澤向叉,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站咳胃,受9級特大地震影響植康,放射性物質(zhì)發(fā)生泄漏旷太。R本人自食惡果不足惜展懈,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望供璧。 院中可真熱鬧存崖,春花似錦、人聲如沸睡毒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽演顾。三九已至供搀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钠至,已是汗流浹背葛虐。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留棉钧,地道東北人屿脐。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像宪卿,于是被迫代替她去往敵國和親的诵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.OkHttp源碼解析(一):OKHttp初階2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HT...
    隔壁老李頭閱讀 15,665評論 36 72
  • 這段時間老李的新公司要更換網(wǎng)絡(luò)層佑钾,知道現(xiàn)在主流網(wǎng)絡(luò)層的模式是RxJava+Retrofit+OKHttp,所以老李...
    隔壁老李頭閱讀 32,729評論 51 406
  • 博文出處:OkHttp源碼解析西疤,歡迎大家關(guān)注我的博客,謝謝休溶! Header 注:本文 OkHttp 源碼解析基于 ...
    俞其榮閱讀 621評論 0 10
  • 隨著Google拋棄HttpClient和Volley的逐步?jīng)]落瘪阁,OkHttp越來越受到開發(fā)者的青睞。高樓大廈也是...
    24K男閱讀 481評論 0 1
  • 前言:對于OkHttp我接觸的時間其實不太長邮偎,一直都是使用Retrofit + OkHttp 來做網(wǎng)絡(luò)請求的管跺,但是...
    mecury閱讀 40,990評論 23 178