吊打面試官——史上最詳細【OkHttp】一

簡介:大三學生黨一枚!主攻Android開發,對於Web和後端均有了解。
個人語錄取乎其上,得乎其中,取乎其中,得乎其下,以頂級態度寫好一篇的博客。

前言:OkHttp源碼是面試中常問的,在騰訊二面中,被面試官追着問Okhttp的原理,當時只是面試前看了幾篇Okhttp的分析博客,自然也就禁不住拷問,這次我深入底層源碼看了兩三遍,看完以後就一個感受,妙哉,不愧是頂級工程師寫出來的代碼!本章節將會分爲幾篇進行講解,希望諸君有所收穫!

在這裏插入圖片描述

一.從基礎說起

在官網的源碼中有這樣一段註釋

OkHttp performs best when you create a single 
{@code OkHttpClient} instance and reuse it for all of your HTTP calls. 
This is because each client holds its own connection pool and thread pools. 
Reusing connections and threads reduces latency and saves memory.
Conversely, creating a client for each request wastes resources on idle pools.

英文好的盆友應該知道了,官方建議我們使用Okhttp的單例模式,原因是每一個OkhttpClient內部都維護了一個連接池,使用單例模式可以減少延遲,節省內存。

作爲演示,我們就用最簡單的建造者模式吧!

 OkHttpClient okHttpClient=new OkHttpClient.Builder().build();
 Request request=new Request.Builder().url(Constant.SEARCH).get().build();
 Call call=okHttpClient.newCall(request);

上面涉及到三個對象,我們就從這三個對象展開我們對OkHttp源碼的解析!

1.1 OkHttpClient

OkHttpClientOkhttp網絡請求框架的客戶端類,可以認爲,所有的網絡請求都是通過這個類發出去的!結合我們實際項目的需求,可以通過設置各種參數定製最適合項目的OkHttpClient,來看看他內部的一些參數。

    //調度器,內部維護了三個請求隊列和一個線程池,負責調度任務的執行
    //執行請求,移除已經執行完的請求
    Dispatcher dispatcher;
    @Nullable Proxy proxy;
    //protocols默認支持的Http協議版本,Http 1.1,Http 2.0,不支持Http1.0
    List<Protocol> protocols;
    //okHttp連接配置,配置諸如TLS的版本號
    List<ConnectionSpec> connectionSpecs;
    //攔截器鏈,這裏暫時理解爲通過攔截器鏈就得到Response
    final List<Interceptor> interceptors = new ArrayList<>();
    //網絡攔截器鏈
    final List<Interceptor> networkInterceptors = new ArrayList<>();
    //一個Call的狀態監聽器
    EventListener.Factory eventListenerFactory;
    //使用默認的代理選擇器
    ProxySelector proxySelector;
    //默認是沒有cookie的
    CookieJar cookieJar;
    //緩存,Okhttp默認是不開啓緩存的,只可以緩存GET方法
    @Nullable Cache cache;
    // internalCache 用來操作cache,比如從cache中獲取和清除緩存
    @Nullable InternalCache internalCache;
    //使用默認的Socket工廠產生Socket
    SocketFactory socketFactory;
    @Nullable SSLSocketFactory sslSocketFactory;
    @Nullable CertificateChainCleaner certificateChainCleaner;
    //安全相關的配置
    HostnameVerifier hostnameVerifier;
    CertificatePinner certificatePinner;
    // 驗證身份的
    Authenticator proxyAuthenticator;
    Authenticator authenticator;
    //連接池,他來負責連接的銷燬和複用
    ConnectionPool connectionPool;
    //域名解析系統
    Dns dns;
    boolean followSslRedirects;
    boolean followRedirects;
    boolean retryOnConnectionFailure;
    //各種超時時間的設置,有默認值
    int callTimeout;
    int connectTimeout;
    int readTimeout;
    int writeTimeout;
    //WebSocket相關,爲了保持長連接,必須間隔一段時間發送一個ping指令進行保活
    int pingInterval;

看完上面的參數,是不是有點懵了?來捋一捋!

作爲優秀的網絡框架,必須要支持常用的協議http,https,而支持的協議版本都放在protocols中,當然與網絡協議相關的內容不止這一點,還需要通過connectionSpec進行配置更多連接的細節,如TLS的版本號等。有時候我們還需要支持Cookies,對於常用的GET請求需要考慮緩存,避免重複請求。其他的參數暫時無需關注,最重要的就下面三個內容

  • Dispatcher分發器:負責調度任務
  • Interceptor攔截器:通過一個個攔截器最終得到Response
  • ConnectionPool 連接池:負責複用連接和銷燬無用連接

1.2 Request

對Http協議熟悉的童鞋都知道,Request對應請求報文。

在這裏插入圖片描述
我們在Request中設置請求方法,請求的URL,使用的協議版本,還有請求體中的請求數據。瞭解了這些,再來看看Request內部的構造。

  final HttpUrl url;//請求的url
  final String method;//請求方法,默認爲get
  final Headers headers;//請求頭部信息,如Content-type,Content-length等
  final @Nullable RequestBody body;//請求體,如攜帶的數據存放在請求體中
  final boolean duplex;
  final Map<Class<?>, Object> tags;//使用此API將時序、調試或其他應用程序數據附加到請求中,以便可以在攔截器、事件偵聽器或回調中讀取它。
  private volatile @Nullable CacheControl cacheControl; // 緩存控制指令

Request簡單多了,簡而言之就是封裝了一個網絡請求所必要的一些參數。

1.3 Call

Call在官方文檔上的描述是:

A call is a request that has been prepared for execution. 
A call can be canceled. 
As this object represents a single request/response pair (stream),
it cannot be executed twice.

Call可以看做一個準備執行的請求,他可以被取消,但是他不能被執行兩次!!!
Call中有兩個重要的方法,同步執行與異步執行!

  Response execute() throws IOException;//同步執行,阻塞當前線程,知道返回結果或者出現異常
 void enqueue(Callback responseCallback);//異步執行,在子線程中執行,不確定會在什麼時候被執行,但是當執行成功或者出現異常時會回調結果。

前面的OkhttpClientRequest是所有網絡請求所通用的,但是到了Call這裏,發生了分叉,對於同步執行和異步執行,Okhttp做了不同的處理。

1.4 RealCall

Call只是一個接口,真正的實現類是RealCall,這裏重點分析RealCallexcute()enqueue()方法。

1.4.1 excute

excute同步執行,會阻塞當前線程,所以不可以在UI線程中執行!

 @Override
    public Response execute() throws IOException {
        synchronized (this) {
            //是否執行過,如果執行過,拋出異常,一個call對象只能被執行一次
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();//捕捉堆棧信息
        timeout.enter();
        eventListener.callStart(this);//開始監聽
        try {
            //調用Dispatcher的executed方法將該Call加入到一個隊列裏
            client.dispatcher().executed(this);
            Response result = getResponseWithInterceptorChain();//通過攔截器鏈獲取Response
            if (result == null) throw new IOException("Canceled");
              //返回本次的請求結果
            return result;
        } catch (IOException e) {
            e = timeoutExit(e);
            eventListener.callFailed(this, e);
            throw e;
        } finally {
            //從已經執行的雙端隊列中移除本次Call
            client.dispatcher().finished(this);
        }
    }

其中就兩句涉及到核心

 client.dispatcher().executed(this);
 client.dispatcher().finished(this);

第一句最後調用的是Dispatcher中的executed方法,

 synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);//runningSyncCalls是保存同步請求的隊列
  }

第二句最後吊用的是Dispatcher的finshed方法

 private <T> void finished(Deque<T> calls, T call) {
    Runnable idleCallback;
    synchronized (this) {
      //將請求Call從隊列中移除
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      idleCallback = this.idleCallback;
    }
    boolean isRunning = promoteAndExecute();
    if (!isRunning && idleCallback != null) {
      idleCallback.run();
    }
  }

一個請求的執行可以簡單描述爲入隊列——》立即執行——》得到返回Response——》移除隊列。

1.4.2 enqueue

異步執行,Dispatcher分發器會根據當前的情況決定如何調度!

 @Override
    public void enqueue(Callback responseCallback) {
        synchronized (this) {
            //首選檢查是否執行,如果執行過,拋出異常。
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();
        eventListener.callStart(this);
        //沒有執行過,調用enqueue,將任務放進隊列中
        client.dispatcher().enqueue(new AsyncCall(responseCallback));
    }

上面的代碼就一句是關鍵

client.dispatcher().enqueue(new AsyncCall(responseCallback));
//首先把Call請求封裝成Runnable,然後調用Dispatcher的enqueue方法
 void enqueue(AsyncCall call) {
    synchronized (this) {
      //加入準備隊列
      readyAsyncCalls.add(call);
    }
    promoteAndExecute();//調度執行
  }

這裏插一段簡單介紹Dispatcher中的三個隊列和連接池,後面會有詳細介紹

 //最大併發數。
  private int maxRequests = 64;
  //每個主機的最大請求數。
  private int maxRequestsPerHost = 5;
  //Runnable對象,在刪除任務時執行
  private @Nullable Runnable idleCallback;
  //消費者池(也就是線程池)
  /** Executes calls. Created lazily. */
  private @Nullable 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<>();

可見enqueue方法先把call封裝的Runnable對象加入了準備執行的異步隊列
最後調用promoteAndExecute()去調度執行,接着看這個方法做了什麼。

 /**
   * Promotes eligible calls from {@link #readyAsyncCalls} to {@link #runningAsyncCalls} and runs
   * them on the executor service. Must not be called with synchronization because executing calls
   * can call into user code.
   *
   * 將符合條件的calls從readyAsyncCalls移動到runningAsyncCalls裏面,在executor service上執行它們。
   * @return true if the dispatcher is currently running calls.
   */
  private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      //循環判斷準備請求隊列是否還有請求
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();
        //如果執行請求的隊列數量大於等於最大併發請求數,中止循環
        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        //大於最大主機請求限制,該請求不能被加入執行隊列,繼續遍歷下一個
        if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
        //如果兩個條件都不滿足,表示目前可以執行該請求,先從準備隊列裏移除自身。
        i.remove();
        
        executableCalls.add(asyncCall);
        //添加到執行隊列中。
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }
    //對新添加的任務嘗試進行異步調用。
    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());//執行該請求
    }

    return isRunning;
  }

經過一些判斷以後,重點還是這句

 asyncCall.executeOn(executorService());//執行該請求

executrorService()方法返回一個線程池

 public synchronized ExecutorService executorService() {
    if (executorService == null) {
    //該線程池,核心線程爲0,最大線程數不限制,線程空閒60秒會被關閉
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }

所以這個異步請求命運多舛,終於要被線程池執行了!!!越是重要時刻,越要保持思路清醒!!!

asyncCall是一個RunnableRunable內部有run()方法,當executorService執行該Runnable時實際上就是執行這個run()方法,而AsyncCall是繼承自NamedRunnable,AsyncCall內部並沒有run()方法,所以還要看它的父類做了什麼!

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();//在run方法內部調用了execute方法,而execute是抽象方法,子類必須要實現,所以也就是調用了AsyncCall內部的execute方法!
    } finally {
      Thread.currentThread().setName(oldName);
    }
  }

  protected abstract void execute();
}

現在我們終於知道,異步任務兜兜轉轉以後也來到execute方法這裏了

 (step 1)  asyncCall.executeOn(executorService());

 (step 2)  void executeOn(ExecutorService executorService) {
            assert (!Thread.holdsLock(client.dispatcher()));
            boolean success = false;
            try {
                //執行任務
                executorService.execute(this);
				//執行一個Runnable,回去執行它的run方法,
				//執行任務,返回結果在哪
                success = true;
            } catch (RejectedExecutionException e) {
                InterruptedIOException ioException = new InterruptedIOException("executor rejected");
                ioException.initCause(e);
                eventListener.callFailed(RealCall.this, ioException);
                responseCallback.onFailure(RealCall.this, ioException);
            } finally {
                if (!success) {
                    client.dispatcher().finished(this); // This call is no longer running!
                }
            }
        }

        @Override
  (step 3)  protected void execute() {
            boolean signalledCallback = false;
            timeout.enter();
            try {
                //返回本次的Response對象
                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) {
                e = timeoutExit(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);
            }
        }
    }

原來如此,同步任務和異步任務最終都是要經過execute方法得到Response的!你男朋友恍然大悟,慶幸有你這麼優秀的女朋友!!!

最後還是用兩一張圖總結一下執行同步任務與異步任務的流程

call.execute
在這裏插入圖片描述





call.enqueue

在這裏插入圖片描述

1.5 小結

第一講主要介紹四大基本類的作用和同步異步請求的源碼解析,下一篇博客開始介紹攔截器鏈的第一個重定向和重試攔截器

先別走,我有一個資源學習羣要推薦給你,它是白嫖黨的樂園,小白的天堂!
在這裏插入圖片描述
別再猶豫,一起來學習!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章