1. 程式人生 > >深入理解OkHttp原始碼(一)——提交請求

深入理解OkHttp原始碼(一)——提交請求

本篇文章主要介紹OkHttp執行同步和非同步請求的大體流程。主要流程如下圖:
OkHttp提交請求流程
主要分析到getResponseWidthInterceptorChain方法,該方法為具體的根據請求獲取響應部分,留著後面的部落格再介紹。

Dispatcher類

Dispatcher類負責非同步任務的請求策略。首先看它的部分定義:

public final class Dispatcher {
  private int maxRequests = 64;
  private int maxRequestsPerHost = 5;
  private Runnable idleCallback;

  /** Executes calls. Created lazily. */
private ExecutorService executorService; /** Ready async calls in the order they'll be run. */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<AsyncCall> runningAsyncCalls = new
ArrayDeque<>(); /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); public Dispatcher(ExecutorService executorService) { this.executorService = executorService; } public
Dispatcher() { } 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; } ... }

內部有一個執行緒池,三個佇列,分別是readyAsyncCalls、runningAsyncCalls和runningSyncCalls佇列,其中runningSyncCalls用於儲存同步請求RealCall物件,runningAsyncCalls佇列用於儲存進入佇列的非同步請求AsyncCall物件,而readyAsyncCalls佇列用於當runningAsyncCalls的尺寸達到maxRequests引數時(預設64)儲存新加的非同步請求。至於為什麼要什麼做呢?
我的理解是為了避免一時間創造大量的執行緒浪費資源,那麼為什麼有執行緒池,還要用到這樣一個控制策略呢?這是因為建立預設執行緒池的引數導致的。預設的executorService的建立類似於Executors.newCachedThreadPool,該執行緒池的問題在於不會限制執行緒數量,如果一下子需要開啟1000乃至更多的執行緒,依然會開啟,而OkHttp這兒在Dispacther中做了控制。待會兒在下面的分析中可以看到這種控制策略。
其餘的引數maxRequestsPerHost表示每個主機的最大請求數,,預設為5,比如說如果這時好多個非同步請求都是請求百度上面的圖片,如果達到了5,那麼新的請求就會被放入到readyAsyncCalls佇列中,等該主機的請求數降下去後才會再次執行。
而引數idleCallback是Dispatcher中請求數量為0時的回撥,這兒的請求包含同步請求和非同步請求,該引數預設為null。
在Dispatcher中,需要明白一點,儘管同步請求自己負責執行請求,但是依然會先加入到Dispatcher的同步佇列,完成後從佇列中移除,而非同步請求則完全屬於Dispatcher控制,但是有些方法是對所有請求操作的,有些則是對非同步請求操作的,需要特別注意。
一般地,我們會將OkHttpClient作為單例,而Dispatcher是其一個成員,自然也是單例,所以一般整個應用的所有請求都會經過Dispatcher,不論是同步請求還是非同步請求。

同步請求的執行流程

使用OkHttp進行網路同步非同步操作中知道了如何進行同步請求,建立一個Request物件,然後再建立一個Call物件,呼叫Call物件的execute方法即可。那麼就從execute方法看起。Call是一個介面,具體實現是RealCall,下面是RealCall的execute方法實現:

@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();
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }

首先是設定executed標誌為true,同一個Call只允許執行一次,執行多次就會丟擲異常。接下來是呼叫OkHttpClient的dispatcher()方法獲得Dispatcher物件,然後呼叫其executed(RealCall)方法,然後就是呼叫getResponseWithInterceptorChain方法同步獲取響應,最後呼叫Dispatcher的finished方法,下面先看executed方法:

/** Used by {@code Call#execute} to signal it is in-flight. */
  synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

從程式碼中可以看出,Dispatcher的executed方法只是將同步請求加入到了runningSyncCalls佇列中。下面再看finished方法:

/** Used by {@code Call#execute} to signal completion. */
  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);
  }

從上面程式碼中,可以看到finished方法再呼叫另一個finished方法,並將runningSyncCalls佇列傳入,具體實現如下:

 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

首先是從佇列中移除請求,如果不能移除,則丟擲異常;在上面finished方法呼叫中看出傳入第三個引數為false,所以不會呼叫promoteCalls方法,該引數用於非同步請求時為true,這個下面分析非同步請求時再講。然後呼叫runningCallsCount統計目前還在執行的請求,最後,如果正在執行的請求數為0表示Dispatcher中沒有可執行的請求了,進入Idle狀態,此時如果idleCallback不為null,則呼叫其run方法。下面是runningCallsCount()方法的實現:

public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

可以看到這個方法返回的請求包括同步請求和非同步請求。
至此,同步請求的執行流程分析完成,可以看到Dispatcher只是儲存了一下同步請求和移除同步請求,而對於非同步請求,Dispatcher的工作就不只是這麼簡單了。

非同步請求的執行流程

我們知道如果要發起非同步請求,那麼就呼叫Call的enqueue方法並傳入回撥,依然從RealCall的enqueue方法看起:

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

可以看到,依然是首先將executed引數設為true,同樣地,非同步請求也不可以被執行兩次,然後呼叫Dispatcher的enqueue方法,但是這兒涉及到了一個新的類,AsyncCall。AsyncCall是RealCall的一個內部類並且繼承NamedRunnable,那麼首先看NamedRunnable類是什麼樣的,如下:

/**
 * Runnable implementation which always sets its thread name.
 */
public abstract class NamedRunnable implements Runnable {
  protected final String name;

  public NamedRunnable(String format, Object... args) {
    this.name = Util.format(format, args);
  }

  @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實現了Runnbale介面並且是個抽象類,其抽象方法是execute(),該方法是在run方法中被呼叫的,這也就意味著NamedRunnable是一個任務,並且其子類應該實現execute方法。下面再看AsyncCall的實現:

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

    private AsyncCall(Callback responseCallback) {
      super("OkHttp %s", redactedUrl().toString());
      this.responseCallback = responseCallback;
    }

    String host() {
      return originalRequest.url().host();
    }

    Request request() {
      return originalRequest;
    }

    RealCall get() {
      return RealCall.this;
    }

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

AsyncCall實現了execute方法,首先是呼叫getResponseWithInterceptorChain()方法獲取響應,然後獲取成功後,就呼叫回撥的onReponse方法,如果失敗,就呼叫回撥的onFailure方法。最後,呼叫Dispatcher的finished方法。
由於AsyncCall的execute()方法是在run中被呼叫的,所以getResponseWithInterceptorChain是在非呼叫執行緒中被呼叫的,然後得到響應後再交給Callback。
從上面的流程看出,與Dispatcher的互動主要涉及enqueue方法和finished方法,與同步請求類似。下面先看enqueue方法:

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

首先如果正在執行的非同步請求的數量小於maxRequests並且與該請求相同的主機數量小於maxRequestsPerHost,也就是說符合放入runningAsyncCalls佇列的要求,那麼放入佇列,然後將AsyncCall交給執行緒池;如果不符合,那麼就放入到readyAsyncCalls佇列中。
當執行緒池執行AsyncCall任務時,它的execute方法會被呼叫,getResponseWithInterceptorChain()會去獲取響應,最後呼叫Dispatcher的finished方法,下面看finished方法:

/** Used by {@code AsyncCall#run} to signal completion. */
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call, true);
  }

從上面的程式碼可以看出,與同步請求的finished方法不同的是第一個引數傳入的是正在執行的非同步佇列,第三個引數為true,下面再看有是三個引數的finished方法:

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();
      runningCallsCount = runningCallsCount();
      idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }

與同步請求相同的是,移除請求,獲取執行數量判斷是否進入了Idle狀態,不同的是會呼叫promoteCalls()方法,下面是promoteCalls()方法:

private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();

      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

promoteCalls方法主要負責從非同步等待佇列中將請求移步到非同步執行佇列中。主要就是遍歷等待佇列,並且需要滿足同一主機的請求小於maxRequestsPerHost時,就移到執行佇列中並交給執行緒池執行。

總結

至此,分析完了同步請求和非同步請求的提交流程,Dispatcher負責非同步請求是放入執行佇列還是等待佇列中,並且在每個非同步請求執行完後,需要判斷是否需要把等待佇列中的請求移到執行佇列中並執行。不管是同步請求還是非同步請求,最終都會呼叫getResponseWithInterceptorChain()方法進行具體的網路請求,該方法下篇部落格深入理解OkHttp原始碼(二)——獲取響應會具體介紹。

相關推薦

深入理解OkHttp原始碼——提交請求

本篇文章主要介紹OkHttp執行同步和非同步請求的大體流程。主要流程如下圖: 主要分析到getResponseWidthInterceptorChain方法,該方法為具體的根據請求獲取響應部分,留著後面的部落格再介紹。 Dispatcher類

深入理解OkHttp源碼——提交請求

mat esp 屬於 idt set ref setname 失敗 class 本篇文章主要介紹OkHttp執行同步和異步請求的大體流程。主要流程如下圖: 主要分析到getResponseWidthInterceptorChain方法,該方法為具體的根據請求獲取響應

Java——深入理解Class物件:什麼是Class物件

Class類是我們再熟悉不過的東西,但是對於Class物件,很多人卻是一臉懵逼。 Class物件到底是什麼呢?今天我們就來深入瞭解一下它。 1.RTTI的概念 RTTI(Run-Time Type Identification),即執行時型別識別,這個詞一直是 C++ 中的概念,至

深入理解計算機系統

從Hello World開始認識計算機系統(c語言) 一枚程式設計小白從2018.9.1的學習歷程… 世界上沒有什麼是努力辦不到的,如果有,那麼就更努力一些吧 1.在Unix系統上,原始檔到目標檔案是如何轉化的呢? 從源程式也就是hello.c經過預處理(cpp

深入理解Spring4框架 簡介

                        &nbs

深入理解HTTP協議——基礎概念篇

1.介紹 HTTP是Hyper Text Transfer Protocol(超文字傳輸協議)的縮寫。它的發展是全球資訊網協會(World Wide Web Consortium)和Internet工作小組IETF(Internet Engineering Task

深入理解Spring框架

Spring設計理念與整體架構 1、Spring是一個非入侵性框架,其目標是使應用程式程式碼對框架的以來最小化,應用程式碼可以在沒有 Spring或者其他容器的情況下使用。 2、

ItemDecoration深入解析與實戰——原始碼分析

一 概述 ItemDecoration 是 RecyclerView 中的一個抽象靜態內部類。 An ItemDecoration allows the application to add a special drawing and layout offset to specific item v

深入理解阻塞佇列——LinkedBlockingQueue原始碼分析

LinkedBlockingQueue是一個基於連結串列實現的可選容量的阻塞佇列。隊頭的元素是插入時間最長的,隊尾的元素是最新插入的。新的元素將會被插入到佇列的尾部。 LinkedBlockingQueue的容量限制是可選的,如果在初始化時沒有指定容量,那麼預

深入Preact原始碼jsx要轉化成virtualDOM發生了什麼

本文和自己在掘金的同步 jsx要轉化成virtualDOM,首先經過babel,再經過h函式的呼叫形成virtualDOM。具體如下 原始碼連結 ./src/h.js 相當於react得createElement(),jsx經過babel轉碼後是h的迴圈

深入理解阻塞佇列——ArrayBlockingQueue原始碼分析

在深入理解阻塞佇列(一)——基本結構中,介紹了BlockingQueue這一介面的子類以及子介面。本文主要就其中的一個實現類:ArrayBlockingQueue進行原始碼分析,分析阻塞佇列的阻塞是如何實現的。 概述 ArrayBlockingQueue

理解Spring+SpringMVC+Hibernate開發流程,附一定原始碼

本例採用Eclipse實現spring+springMVC+hibernate專案的建立,在專案建立的過程中進行一定講解,方便理解是如何開發使用spring+springMVC+hibernate的思想。 接下來是具體的開發步驟: 一、初步開發環境搭建

深入理解阻塞佇列——LinkedBlockingDeque原始碼分析

LinkedBlockingDeque是一個基於連結串列的雙端阻塞佇列。和LinkedBlockingQueue類似,區別在於該類實現了Deque介面,而LinkedBlockingQueue實現了Queue介面。該類的繼承關係如下圖: 本文將與Lin

深入剖析PHP7核心原始碼- PHP架構與生命週期

PHP7 為什麼這麼快? 全新的zval 更節約的空間,棧上分配記憶體 zend_string 儲存字串的Hash值,陣列查詢的時候不需要進行Hash計算 在HashTable桶內直接存資料,減少了記憶體的申請次數,提升了cache命中率和記憶體訪問速度 zend_parse_parameters改為了巨集

[轉]畢設- 深入HBase架構解析

mil https 文件存儲 back 高效 索引 asa dia 隨機 深入HBase架構解析(一) 前記 公司內部使用的是MapR版本的Hadoop生態系統,因而從MapR的官網看到了這篇文文章:An In-Depth Look at the HBase Ar

android深入之設計模式托付模式

-h listen back != new 聚合 string static data- (一)托付模式簡單介紹 托付模式是主要的設計模式之中的一個。托付。即是讓還有一個對象幫你做事情。 更多的模式,如狀態模式、策略模式、訪問者模式本質上是在更特殊的場合採用了托

深入理解JavaScript系列16:閉包Closures

ava hive auto flow style this quest 情況 知識 介紹 本章我們將介紹在JavaScript裏大家常常來討論的話題 —— 閉包(closure)。閉包事實上大家都已經談爛了。雖然如此,這裏還是要試著從理論角度來討論下閉包,

laravel服務容器-----深入理解控制反轉IoC和依賴註入DI

outer 十分 綁定 之間 module 還需 true 更多 我們 首先大家想一想什麽是容器,字面意思就是盛放東西的東西,常見的變量,對象屬性都是容器,一個容器能夠裝什麽東西,完全在於你對這個容器的定義。有的容器不僅僅只是存文本,變量,而是對象,屬性,那麽我們通過這種容

深入理解多態 1

之間 數據 override 擴展 結束 不同的 img over str 1.1 public abstract class Birds{ 2 3 //什麽樣的方法是抽象方法 4 5 public abstract void Fly(); 6

[轉]深入理解閉包

copy AI strong 查找 cte 分組操作 spa 方法 詳細介紹 嚴格來講,IIFE並不是閉包,因為它並不滿足函數成為閉包的三個條件。但一般地,人們認為IIFE就是閉包,畢竟閉包有多個定義。本文將詳細介紹IIFE的實現和用途 實現   函數跟隨一對圓括號()