OkHttp源碼解析

應用攔截器和網絡攔截器

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

攔截器有兩種,應用攔截器和網絡攔截器。用法可以看下面的代碼:

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
    ...(省略部分打印)

攔截器是一種強大的機制,可以在攔截器中進行監視、重寫和重試調用。像我們上面的代碼就對請求進行了監視。

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

簡單來講,網絡攔截器在每一次網絡訪問的時候都會攔截到請求,而應用攔截器只會在OkHttpClient.newCall返回的Call執行的時候被調用一次。

okhttp的運行流程

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

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

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

異步訪問

RealCall實現了Call。接口,我們通過調用enqueue方法可以實現異步網絡訪問。讓我們直接看看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,它用于網絡訪問任務的調度,我們的異步并發網絡訪問就是通過Dispatcher實現的。這里創建了一個AsyncCall,然后將它傳入Dispatcher.enqueue。AsyncCall是RealCall的內部類,而且它實際上是一個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方法里面會調用抽象的execute方法,在這個方法內部就會進行實際的網絡訪問。那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放到了線程池中執行去訪問網絡,而AsyncCall是RealCall的一個內部類,它持有RealCall的引用,所以在被線程池調用的時候可以獲得Request的信息。

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

1.png

然后我們看看AsyncCall.execute的具體實現:

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來訪問網絡獲取Response的。

同步訪問

如果想用OkHttp去阻塞是的訪問網絡我們可以這樣調用:

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

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

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

現在我們來看看具體實現:

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來訪問網絡獲取Response的。

攔截器的實現

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

okhttp3.4.0-RC1之前的實現

okhttp3.4.0-RC1之前的RealCall.getResponseWithInterceptorChain 中實際上是調用了ApplicationInterceptorChain.proceed方法去訪問網絡獲取Response:

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

然后繼續看源碼,可以發現proceed內部會從OkHttpClient獲取序號為index的攔截器,并且創建新的序號加一的ApplicationInterceptorChain傳遞給攔截器去執行。于是有多少個攔截器就創建了多少個ApplicationInterceptorChain,他們會按照自己的序號調用對應的攔截器。這其實就是一種責任鏈模式的實現方式:

@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中注冊的攔截器的數量,則調用getResponse方法。這里ApplicationInterceptorChain是RealCall的內部類,getResponse調用的是RealCall.getResponse方法。

再看RealCall.getResponse方法,它內部有個while true的死循環,調用HttpEngine.sendRequest和HttpEngine.readResponse去發送請求和接收響應,如果出現了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;
    }
    ...
}

我們繼續看engine.readResponse的實現,可以看到它調用了NetworkInterceptorChain.proceed方法去獲取響應:

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

NetworkInterceptorChain.proceed和ApplicationInterceptorChain.proceed類似,也會不斷的創建新的NetworkInterceptorChain并且調用網絡攔截器,如果沒有網絡攔截器可以調用了,則會調用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的內部類,它調用的readNetworkResponse方法實際上是HttpEngine.readNetworkResponse。現在我們就對OkHttp攔截器的請求流程和攔截器的實現原理有了比較全面的了解,下面這張圖對整個流程做一個總結:

2.png

okhttp3.4.0-RC1之后的實現

然后讓我們再來看一下3.4.0-RC1之后的實現:

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

這里已經不再區分ApplicationInterceptorChain和NetworkInterceptorChain了,統一用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處理、緩存處理、網絡連接都作為責任鏈的一部分,比起3.4.0.RC-1之前更加完全的實現了責任鏈模式。這里有必要講一下的就是retryAndFollowUpInterceptor, 它是一個RetryAndFollowUpInterceptor實例,它負責重連和重定向我們之前在3.4.0.RC-1之前看到的getResponse的while true就放到了這里來實現。

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

3.png

這樣的實現是不是以前要清晰很多?所有的步驟一目了然,看過原來的版本再看看3.4.0.RC-1重構后的版本,的確有一種眼前一亮的驚艷之感。果然好代碼都是需要一點點優化出來的。

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

推薦閱讀更多精彩內容