Android OkHttp原始碼解析入門教程(一)

Focusing發表於2019-01-22

概述

從OkHttp問世以來,度娘,google上關於OkHttp的講解說明數不勝數,各種解讀思想不盡相同,一千個讀者就有一千個哈默雷特。本篇文章從原始碼出發向你介紹Okhttp的基本使用以及底層實現原理,讓你從會寫轉向會用,學習Android頂尖原始碼的設計理念和開源擴充套件性,如果解讀有誤,還望提出探討糾正。

工欲善其事,必先利其器

Android API23(6.0)版本以後,Google正式移除Apache-HttpClient 。OkHttp 作為一個現代,快速,高效的HttpClient,其功能之強大也是顯而易見的

Android OkHttp原始碼解析入門教程(一)

1.支援SPDY 可以合併多個請求到同一個主機的請求、連線池、GZIP和HTTP快取

2.支援HTTP/2協議,通過HTTP/2 可以讓客戶端中到伺服器的所有請求共用同一個Socket連線

3.非HTTP/2 請求時, OkHttp內部會維護一個執行緒池,通過執行緒池可以對HTTP/1.x的連線進行復用,減少延遲

4.支援post,get請求,基於http的檔案上傳和下載

5.預設情況下,OkHttp會自動處理常見的網路問題,像二次連線、SSL的握手問題

當然OkHttp的功能遠不止這些,這裡只是說明平時經常用到的。既然OkHttp已經作為官方庫使用,相比我們在做專案的時候也會用,但對於其底層的實現原理還是一知半解,那我們就從這篇文章開始解釋其底層實現原理。開車前先來一波介紹:

Ecplise引用:下載最新的Jar

Android studio引用:implementation 'com.squareup.okhttp3:okhttp:3.11.0' //最新版本號請關注okhttp官網

Maven引用:

<dependency>
  <groupId>com.squareup.okhttp3</groupId>
  <artifactId>okhttp</artifactId>
  <version>3.11.0</version> //最新版本號請關注okhttp官網
</dependency>
複製程式碼

Android OkHttp原始碼解析入門教程(一)

各位老司機們,馬上開車,嘀嘀嘀!

一、基本使用方法

流程如下


// 啟動客戶端類,主要有兩種方法進行建立,new物件和Builder內部類實現例項化
OkHttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();

// get請求
// 通過Builder模式建立一個Request物件(即請求報文)
// 這裡可以設定請求基本引數:url地址,get請求,POST請求,請求頭,cookie引數等
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .header("User-Agent", "xxx.java")
                .addHeader("token", "xxx")
                .get()
                .build();

// POST請求
// 表單形式上傳
RequestBody body = new FormBody.Builder().add("xxx","xxx").build();
// JSON引數形式,File物件上傳
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json); 
RequestBody body = RequestBody.create(MediaType.parse("File/*"), file);

Request request = new Request.Builder()
                .post(body)
                .url(url)
                .header("User-Agent", "xxx.java")
                .addHeader("token", "xxx")
                .build();


// 建立Call物件(Http請求) ,即連線Request和Response的橋樑
// newCall方法將request封裝成Call物件
Call call = client.newCall(request);
try{
// Response即響應報文資訊,包含返回狀態碼,響應頭,響應體等
    Response response = call.execute();
// 這裡深入一點,Call其實是一個介面,呼叫Call的execute()傳送同步請求其實是呼叫了Realcall實現類的方法,Realcall從原始碼可以看出示一個Runable
    System.out.println(response.body().string());
}catch(IOException e){
    e.printStackTrace();
}
複製程式碼

看完程式碼你可能覺得OkHttp基本流程很繁瑣,但是去掉一些擴充套件引數,你會發現OkHttp的使用其實很簡單,無非就是

1.建立一個OkHttpClient並例項化,可設定相關引數連線時長connectTimeout等

2.建立一個Request物件並例項化,可設定網路地址url,請求方式get,post,攜帶引數等;

3.建立一個Call物件,通過okhttpClient的newCall()方法將Request封裝成Call物件

4.建立一個Response響應,用於接收伺服器返回的相關資訊; 即OkHttpClient客戶端通過newCall()方法接受你的Request請求並生成Response響應

二、同步和非同步請求

看到這裡你可能會問,為什麼不繼續講些關於檔案上傳,檔案下載,Interceptors攔截器這些內容?其實同步和非同步請求的實現可以說是原始碼中非常重要的一環,涉及到執行緒池Dispatch排程反向代理等,掌握核心科技,剩下的都是開胃小菜。

1)基本使用方法

同步請求方法

OkhttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .get()
                .build();
Call call = client.newCall(request);
try{
    Response response = call.execute();//呼叫同步請求
    System.out.println(response.body().string());
}catch(IOException e){
    e.printStackTrace();  
}
複製程式碼

通過上面程式碼可以看出同步請求的基本流程: 1.建立OkHttpClient和Request物件 2.將Request封裝成Call物件 3.呼叫Call的excute()發起同步請求

*特別注意*:當前執行緒傳送同步請求後,就會進入阻塞狀態,直到資料有響應才會停止(和非同步最大的不同點)

非同步請求方法

OkhttpClient  client = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).build();
Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .get()
                .build();
Call call = client.newCall(request);
call.enqueue(new Callback() { //呼叫非同步請求,CallBack用於請求結束以後來進行介面回撥
        @Override
        public void onFailure(Call call, IOException e) { System.out.println("Failure");}
        @Override
        public void onResponse(Call call, Response response) throw s IOException {
           System.out.println(response.body().string());
        }
    });
複製程式碼

通過上面程式碼可以看出非同步請求的基本流程: 1.建立OkHttpClient和Request物件 2.將Request封裝成Call物件 3.呼叫Call的enqueue發起非同步請求

*特別注意*: onFailure和onResponse都是執行在子執行緒

不難看出,其實非同步和同步請求的不同點在於 1、發起請求方法呼叫 2、是否阻塞執行緒

到此,我們已經熟悉了OkHttp的同步,非同步請求方法的基本使用;不管同步還是非同步的呼叫都需要先初始化OkHttpClient,建立Request ,呼叫OkHttpClient.newcall()封裝Call物件,但其內部又是如何實現的呢?

Android OkHttp原始碼解析入門教程(一)

同步請求執行流程

第一步:初始化OkHttpClient(Builder builder)

    public Builder() {
      dispatcher = new Dispatcher(); // 排程分發器(核心之一)
      protocols = DEFAULT_PROTOCOLS; // 協議
      connectionSpecs = DEFAULT_CONNECTION_SPECS; // 傳輸層版本和連線協議
      eventListenerFactory = EventListener.factory(EventListener.NONE); // 監聽器
      proxySelector = ProxySelector.getDefault(); // 代理選擇器
      cookieJar = CookieJar.NO_COOKIES; // cookie
      socketFactory = SocketFactory.getDefault(); // socket 工廠
      hostnameVerifier = OkHostnameVerifier.INSTANCE; // 主機名字
      certificatePinner = CertificatePinner.DEFAULT; // 證照鏈
      proxyAuthenticator = Authenticator.NONE; // 代理身份驗證
      authenticator = Authenticator.NONE; // 本地省份驗證
      connectionPool = new ConnectionPool(); // 連線池(核心之一)
      dns = Dns.SYSTEM;// 基礎域名
      followSslRedirects = true;// 安全套接層重定向
      followRedirects = true;// 本地重定向
      retryOnConnectionFailure = true; // 連線失敗重試
      connectTimeout = 10_000; // 連線超時時間
      readTimeout = 10_000;  // 讀取超時時間
      writeTimeout = 10_000; // 寫入超時時間
      pingInterval = 0; // 命令間隔
    }
複製程式碼

從這裡看到,其實OkHttpClient的初始化已經幫我們配置了基本引數,我們也可以根據自身業務需求進行相應的引數設定(失敗重連,新增攔截器,cookie等等),一般遇到建立物件需要大量引數時,推薦使用Builider模式鏈式呼叫完成引數初始化,具體使用可以去Android原始碼中的AlertDialog、Notification中詳細瞭解; 這裡我們重點注意兩個核心,DispatcherConnectionPool,這兩點會在後面做詳細講解

Dispatcher:OkHttp請求的排程分發器,由它決定非同步請求線上程池中是直接處理還是快取等待,當然對於同步請求,只是將相應的同步請求放到請求佇列當中執行

ConnectionPool: 統一管理客戶端和伺服器之間連線的每一個Connection,作用在於

1)當你的Connection請求的URL相同時,可以選擇是否複用;2)控制Connection保持開啟狀態還是複用

第二步:建立 (Builder builder)

  public static class Builder {
    HttpUrl url; 
    String method;
    Headers.Builder headers;
    RequestBody body; //請求體
    Object tag; //標籤

    public Builder() {
      this.method = "GET";
      this.headers = new Headers.Builder(); //Headers內部類
    }
複製程式碼

這個構造方法很簡單,在Request.Builder模式下預設指定請求方式為GET請求,建立了Headers內部類來儲存頭部資訊,我們再來看build方法

 public Request build() {
      if (url == null) throw new IllegalStateException("url == null");
      return new Request(this);
    }

Request(Builder builder) {
    this.url = builder.url;
    this.method = builder.method;
    this.headers = builder.headers.build();
    this.body = builder.body;
    this.tag = builder.tag != null ? builder.tag : this;
  }
複製程式碼

Request的構造方法就是為其初始化指定需求的請求方式,請求URL,請求頭部資訊,這樣就完成同步請求的前兩步

第三步:呼叫OkHttpClient.newcall()封裝Call物件

 /**
   * Prepares the {@code request} to be executed at some point in the future.
   * 準備在將來某個時候執行{@code請求}
   */
  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }
複製程式碼

上面我們也提到過,Call是一個介面,所以它的實際操作是在RealCall類中實現的

final class RealCall implements Call {
  final OkHttpClient client;
  final RetryAndFollowUpInterceptor retryAndFollowUpInterceptor; //重定向攔截器

  /**
   * There is a cycle between the {@link Call} and {@link EventListener} that makes this awkward.
   * This will be set after we create the call instance then create the event listener instance.
   */
  private EventListener eventListener;

  /** The application's original request unadulterated by redirects or auth headers. */
  final Request originalRequest;
  final boolean forWebSocket;

  // Guarded by this.
  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) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
  }
}
複製程式碼

從這裡就可以看到,RealCall其實是持有之前初始化好的OkHttpClient和Request物件,同時賦值了RetryAndFollowUpInterceptor重定向攔截器,關於攔截器的內容,我們會後面具體講解OKhttp內部的5大攔截器;

第四步,呼叫call.exucte方法實現同步請求

 @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); // 排程器將call請求 加入到了同步執行佇列中
      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);
    }
  }
複製程式碼

首先, 加入了synchronized 同步鎖,判斷executed標識位是否為true,確保每個call只能被執行一次不能重複執行,然後開啟了eventListener監聽事件,接收相應的事件回撥,通過dispatcher將Call請求新增到同步佇列中

 public Dispatcher dispatcher() {
    return dispatcher;
  }

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

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
  // 同步請求佇列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
複製程式碼

每當呼叫executed同步方法時,dispather就會幫我們把同步請求新增到同步請求佇列中去,由此可以看出Dispather排程器的作用就是維持Call請求傳送狀態維護執行緒池並把Call請求新增到相應的執行佇列當中,由它決定當前Call請求是快取等待還是直接執行,流程如下

同步請求流程

getResponseWithInterceptorChain()是一個攔截器鏈,依次呼叫攔截器對返回的response進行相應的操作,我們在講解到責任鏈模式時會詳細介紹,如圖

Android OkHttp原始碼解析入門教程(一)

另外要特別注意下client.dispatcher().finished(this);

  /** Used by {@code Call#execute} to signal completion. */
  void finished(RealCall call) {
    finished(runningSyncCalls, call, false);  // 注意引數傳遞的值
  }

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

/***********************************************************************************************************************/
  public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }
複製程式碼

當同步請求完成後會呼叫finished()方法將佇列中的請求清除掉,runningCallsCount()計算返回正在執行同步請求和正在執行非同步請求的數量總和,最後判斷如果runningCallsCount 為0的時候,表示整個Dispatcher分發器中沒有可執行的請求,同時在滿足idleCallback不為空的情況下,就呼叫Run方法開啟閒置介面;這裡可以看出,在同步請求的方法中,dispatcher的作用只是呼叫 executed將Call請求新增到同步佇列中,執行完畢後呼叫 finished清除佇列中的請求,可見dispatcher更多的是為非同步服務

非同步請求執行流程

關於OkHttpClient和Request初始化流程上文已經講解,不清楚的可以返回去看看,所以直奔主題

第四步,呼叫call.enqueue方法實現非同步請求

//RealCall實現類
  @Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true; // executed用於表示Call請求是否執行過
    }
    captureCallStackTrace();// 捕捉異常堆疊資訊
    eventListener.callStart(this);// 開啟監聽事件
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
複製程式碼

有沒有發現和同步的excute方法很類似,都是先使用synchronized 防止請求重複執行,然後開啟監聽事件,最後在執行相應的方法,但奇怪的是同步在執行完excute方法後是直接通過getResponseWithInterceptorChain()返回資料,非同步又是如何返回資料的呢?AsyncCall又是幹什麼的?

final class AsyncCall extends NamedRunnable {
    private final Callback responseCallback;

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

    @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);
      }
    }
  }
複製程式碼

這裡的 AsyncCall 是 RealCall 的一個內部類,它繼承於NamedRunnable抽象類,NamedRunnable抽象類又實現了 Runnable,所以可以被提交到ExecutorService上執行,在execute方法裡,我們看到了熟悉的流程,上文也說到getResponseWithInterceptorChain是一個攔截器鏈,會依次執行相應的攔截器後返回資料,所以當返回資料後,通過retryAndFollowUpInterceptor重定向攔截器判斷請求是否正常執行,並且通過Callback介面返回相應資料資訊,最後呼叫finished方法清除佇列 這裡有個疑問,Dispatcher是通過什麼把非同步就緒佇列的請求排程分發到非同步執行佇列中的?

Android OkHttp原始碼解析入門教程(一)
還記得我們講client.dispatcher().finished(this)的時候,說到過promoteCalls方法,只是同步傳參的是false沒有呼叫,但非同步傳參是true,所以promoteCalls方法才真正在非同步中呼叫

  //Disaptcher

  /** 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<>();

  private void promoteCalls() {
    // maxRequests最大請求數量64
    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.
    }
  }
複製程式碼

原始碼是不是很清楚明瞭,原來非同步佇列就是在這裡進行排程的,在for迴圈中,Disaptcher首先對非同步就緒佇列進行遍歷,如果滿足runningCallsForHost(當前呼叫請求主機數)小於maxRequestsPerHost( 最大請求主機數5個)並且非同步併發數量沒有超過最大請求數量64的前提下,就把非同步就緒佇列中最後一個元素移除加入到非同步執行佇列中

Android OkHttp原始碼解析入門教程(一)
我們接著看enqueue方法具體做了哪些操作

//Disaptcher
synchronized void enqueue(AsyncCall call) {
    // 非同步併發請求數量不能超過最大請求數量64
    // 當前網路請求的host是否小於5個請求的host
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      // 加入執行佇列 並交給執行緒池執行
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      // 加入就緒佇列等待
      readyAsyncCalls.add(call);
    }
  }

***************************************************************************************************************
  public synchronized ExecutorService executorService() {
   // 核心執行緒 最大執行緒 非核心執行緒閒置60秒回收 任務佇列
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
複製程式碼

Disaptcher的enqueue方法只是做了一個非同步請求的邏輯判斷,即判斷當前非同步併發執行佇列的數量是否超過最大承載執行數量64和相同host主機最多允許5條執行緒同時執行請求,滿足以上條件,則將傳進來的AsyncCall新增到非同步執行佇列,同時啟動執行緒池執行,反之則新增到非同步就緒佇列中等待,executorService呼叫的就是AsyncCall的execute方法

Android OkHttp原始碼解析入門教程(一)

同步和非同步請求的原始碼就講到這裡,對過程還有不理解的可以在下方評論中提出問題 文章連結:

Android OkHttp原始碼解析入門教程:同步和非同步(一)

Android OkHttp原始碼解析入門教程:攔截器和責任鏈(二)

相關文章