okhttp源碼解析

前言:okhttp框架已經(jīng)使用了好多年了,本文基于3.12.13版本源碼分析,個人覺得里面的設計確實非常巧妙。目前官網(wǎng)最新已經(jīng)升級到4.12版本:https://github.com/square/okhttp 有興趣可以看下 能完美兼容3.x版本,主要變更為kt實現(xiàn)。

okhttp從設計上亮點

1.大量運用設計模式,調(diào)用方便可擴展性強
2.分發(fā)器實現(xiàn)比較巧妙,異步請求設計保證了高并發(fā)最大吞吐量
3.攔截器使用責任鏈模式,保證了各個功能模塊職責單一,暴露API方便用戶擴展
4.連接池實現(xiàn)也保證了整體框架鏈接的高性能和資源開銷

最簡單的使用

只需要下面四個步驟就可以發(fā)起網(wǎng)絡請求,但是真正去閱讀他源碼實現(xiàn) 會發(fā)現(xiàn)里面的很多細節(jié)很給力。

//建造者模式創(chuàng)建client
private OkHttpClient mOkHttpClient =mOkHttpClient.newBuilder()
              .connectTimeout()
              .readTimeout()
              .writeTimeout()
              .build();

//建造者模式創(chuàng)建request
Request.Builder builder = new Request.Builder().url(requestUrl);
builder.addHeader()
Request request = builder.build();

//把request給到 client
Call call = mOkHttpClient.newCall(request);

//執(zhí)行移步請求
call.enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {

                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {

                }
            });

client 和request都是用了建造者模式,方便用戶自定義擴展。思路也比較清晰,構(gòu)建請求然后給到okcliet對象,由對象調(diào)用newCall()返回一個Call對象,在調(diào)用Call對象的異步方法發(fā)起請求得到結(jié)果回調(diào)。Call對象是一個接口,里面定義同步、異步、是否取消、超時等方法,只有一個實現(xiàn)類:RealCall

執(zhí)行請求真正的實現(xiàn)類RealCall

上面的newCall方法調(diào)用如下,表明非websocket請求

@Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

調(diào)用異步請求方法:

 @Override public void enqueue(Callback responseCallback) {
    // 加同步限制保證同一個請求不會執(zhí)行多次
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      //可以看到這個標記沒有設置false的地方,每次請求發(fā)出去就不管了這個標記主要保證沒執(zhí)行
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
  // 核心方法 
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

核心方法: client.dispatcher().enqueue(new AsyncCall(responseCallback)); 調(diào)用到Dispatcher類。這里用到了門面模式,Call里面的邏輯全部指向了這個分發(fā)器,里面設計也非常精妙。new AsyncCall(responseCallback) AsyncCall其實就是一個Runnable

public final class Dispatcher {
  private int maxRequests = 64; // 最大請求數(shù)不能超過64個
  private int maxRequestsPerHost = 5; //同一個host連接數(shù)不超過5
  private @Nullable Runnable idleCallback;

  /** Executes calls. Created lazily. */
  private @Nullable ExecutorService executorService;

  /** Ready async calls in the order they'll be run. */
// 等待被執(zhí)行的請求雙端隊列
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

  /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
// 正在執(zhí)行的請求對列異步
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

  /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
// 正在執(zhí)行的請求對列同步
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  public Dispatcher(ExecutorService executorService) {
    this.executorService = executorService;
  }

  public Dispatcher() {
  }
}

從上面代碼分析,成員變量就定義了請求最大數(shù)量限制64,同一個host連接數(shù)限制5,里面定義了三個雙端隊列結(jié)構(gòu),兩個隊列用作異步請求,一個用作同步請求。還定義線程池,線程池的參數(shù)就比較有意思,能保證高并發(fā)下最大吞吐量。一個個來解釋下

1.執(zhí)行enqueue()時會調(diào)用如下邏輯,先把call加到等待隊列,在執(zhí)行promoteAndExcute方法

 void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
    }
    promoteAndExecute();
  }

promoteAndExecute方法實現(xiàn)如下,核心邏輯添加了注釋

private boolean promoteAndExecute() {
    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next(); //獲取當前請求call
        //正在運行的請求數(shù)量如果超過64 就break
        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        // 當前請求host連接數(shù)量如果超過5個則不執(zhí)行后續(xù)邏輯
        if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.
      // 把當前call移除等待隊列
        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;
  }

當滿足條件后,會調(diào)用executeOn方法執(zhí)行,傳遞的參數(shù)很重要,參數(shù)是個線程池對象,為啥說這樣定義就可以保證高并發(fā) 最大吞吐量。

//同步線程池定義
  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;
  }

核心線程數(shù)0,最大線程數(shù)最大integer,存活時間60秒,隊列是SynchronousQueue?;氐骄€程池基礎。
1.當一個線程執(zhí)行提交任務時,首先判斷是否超過核心線程數(shù),如果不超過,會執(zhí)行任務,如果超過最大核心數(shù),則添加到隊列,
2.添加到隊列中如果如果隊列滿了添加不進去了,則會判斷是否超過最大線程數(shù),如果沒超過創(chuàng)建線程執(zhí)行任務,如果超過執(zhí)行拒絕策略

在看ok的設置,第一個提交發(fā)現(xiàn)核心線程為0,則把任務添加到隊列,而SynchronousQueue代表不存儲的隊列,也就會添加失敗,添加失敗后判斷正在運行的線程數(shù)量 是否超過最大線程數(shù),最大為integer最大值,不超過則創(chuàng)建線程執(zhí)行任務。每提交一次就會創(chuàng)建一個線程執(zhí)行。

這個不會有問題嗎?擴展一個線程池的知識點

阿里開發(fā)手冊為什么禁止使用線程池創(chuàng)建方式

newFixedThreadPool、newSingleThreadExecutor 方法創(chuàng)建線程
image.png

傳入的是一個無界的阻塞隊列,理論上可以無限添加任務到隊列中 很可能觸發(fā)OOM。
阻塞隊列有還有ArrayBlockingQueue,如果是這種隊列,需要指定大小,假如傳入一個固定數(shù)字1,表明只能往里面添加1個,如果任務執(zhí)行比較耗時,添加了一個任務到隊列,再來一個任務,發(fā)現(xiàn)沒有超過最大線程數(shù),則會創(chuàng)建一個線程執(zhí)行,這就導致先提交的任務后執(zhí)行。LinkedBlockingQueue還可以傳入數(shù)字就和ArrayBlockingQueue一樣了。

CachedThreadPool 和ok使用的一樣
image.png

任務會不斷被提交,當核心線程耗時很久,線程池會嘗試創(chuàng)建新的線程來執(zhí)行提交的任務,當內(nèi)存不足時就會報無法創(chuàng)建線程的錯誤而且安卓廠商一般還有限制,之前在做內(nèi)存優(yōu)化時候驗證華為手機有限制 mate20pro手機上超過500多個就可能出現(xiàn)OOM

okhttp實現(xiàn)則用到了最大吞吐量,配合兩個隊列限制最大請求數(shù)64和同個host最大連接數(shù)不超過5,來控制來保證不會一直創(chuàng)建線程

我們繼續(xù)看任務執(zhí)行方法:

void executeOn(ExecutorService executorService) {
      // 把任務給到線程池執(zhí)行
      try {
        executorService.execute(this);
        success = true;
      } catch (RejectedExecutionException e) {
       ..... 
      } finally {
       // 不管任務是否執(zhí)行成功,都執(zhí)行分發(fā)起的finish方法
        if (!success) {
          client.dispatcher().finished(this); // This call is no longer running!
        }
      }

不管任務是否執(zhí)行成功,都執(zhí)行分發(fā)起的finish方法

 private <T> void finished(Deque<T> calls, T call) {
    ...
    boolean isRunning = promoteAndExecute();
    ...
  }

可以看到都會執(zhí)行promoteAndExecute() 這樣繼續(xù)保證從等待隊列里面去取任務給到運行中隊列執(zhí)行的機制。有沒有感覺有點類似handler機制里面取消息。而把任務給到線程池調(diào)度會執(zhí)行到runnable的run方法,run方法會執(zhí)行RealCall的excute方法,

@Override protected void execute() {
     try {
     // 交給攔截器 請求并返回數(shù)據(jù) 再回調(diào)
       Response response = getResponseWithInterceptorChain();
       signalledCallback = true;
       responseCallback.onResponse(RealCall.this, response);
     } catch (IOException e) {
         eventListener.callFailed(RealCall.this, e);
         responseCallback.onFailure(RealCall.this, e);
     } catch (Throwable t) {
       cancel();
       if (!signalledCallback) {
         IOException canceledException = new IOException("canceled due to " + t);
         responseCallback.onFailure(RealCall.this, canceledException);
       }
       throw t;
     } finally {
       client.dispatcher().finished(this);
     }
   }

攔截器

上面的分析是整個分發(fā)器的實現(xiàn)。這里就到了攔截器的實現(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));//添加socket連接攔截器
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors()); // 如果不是websockect 添加網(wǎng)絡攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket)); // 添加與服務器通信協(xié)議

    // 傳入攔截器列表,返回攔截器鏈條
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());
     // 調(diào)用鏈條的proceed方法返回response
    Response response = chain.proceed(originalRequest);
    if (retryAndFollowUpInterceptor.isCanceled()) {
      closeQuietly(response);
      throw new IOException("Canceled");
    }
    return response;
  }

核心代碼都加了注釋。里面實現(xiàn)是通過index+1,不斷取出下一個攔截器,并調(diào)用intercept方法。從鏈表頭部到尾部的攔截器執(zhí)行,等尾部攔截器返回response再一層一層返回到頭部的攔截器。

   Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

由上面源碼可以知道。第一個添加的是用戶自定義的攔截器列表到鏈條列表里。如果在里面做一些邏輯,可能會影響后續(xù)添加攔截器執(zhí)行。如果是非websockect 還會添加一個網(wǎng)絡攔截器,這兩個區(qū)別就是網(wǎng)絡攔截器不一定執(zhí)行,而用戶自定義的會執(zhí)行。網(wǎng)絡的最先拿到response。分別介紹下各個攔截器作用

重試重定向:

    主要調(diào)用recover方法判斷是否能重試、重定向
    判斷client是否設置過不允許重試,請求發(fā)送出沒請求體 是否是重試類型,比如證書失敗 這種就不會給重試了,網(wǎng)絡波動這種會重試
    判斷重定向邏輯也是先校驗用戶是否允許,匹配301 302 303這種取 頭部的“l(fā)ocation”繼續(xù)重定向。最大重定向次數(shù)20次,參考了瀏覽器的實現(xiàn)

橋接攔截器:

        這個攔截器主要是往頭部添加壓縮方式、內(nèi)容長度、編碼 cookie 添加UA 等處理頭部信息

緩存攔截器:

      這個攔截器的實現(xiàn)比較復雜,主要總結(jié)起來就是根據(jù)請求頭、響應頭決定當前的請求是否需要請求數(shù)據(jù)還是直接返回緩存,里面用到DiskLrucache策略,用戶可以自定義緩存策略

連接攔截器:

    RealConnection封裝了socket和一個socket連接池 主要就是獲取與目標服務器的連接,在連接上數(shù)據(jù)通信

關鍵代碼在ConnectionPool中 最多保存 5個 處于空閑狀態(tài)的連接,且連接的默認?;顣r間為 5分鐘。 加一個connection 執(zhí)行while (true) 執(zhí)行clean方法,如果多了或者有超過5分鐘異常最早的連接
主要工作就是負責建立 TCP 連接,建立 TCP 連接需要經(jīng)歷三次握手四次揮手等操作,如果每個 HTTP 請求都要新建一個 TCP 消耗資源比較多 而 Http1.1 已經(jīng)支持 keep-alive ,即多個 Http 請求復用一個 TCP 連接,OKHttp 也做了相應的優(yōu)化 這里用到享元模式

請求服務攔截器:

  主要完成請求報文的封裝與解析
框架常見問題總結(jié)

答案都在上面源碼分析里面解釋了,后面再繼續(xù)補充下問題
1.添加網(wǎng)絡攔截器和添加攔截器誰先執(zhí)行 有什么區(qū)別,為什么

2.okhttp的連接池問題、攔截器、分發(fā)器核心邏輯是什么
連接池出現(xiàn)的背景:
頻繁的進行建立Sokcet連接和斷開Socket是非常消耗網(wǎng)絡資源和浪費時間的,所以HTTP中的keepalive連接對于降低延遲和提升速度有非常重要的作用。keepalive機制是什么呢?也就是可以在一次TCP連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會斷開連接。
ConectionPool中維護了一個雙端隊列Deque 發(fā)起請求就會從當前連接池中get socket,如果沒有就會創(chuàng)建并加進去,加進去就會觸發(fā)清理 并返回下次清理的時間, 連接池最大可以存在5個連接 保留5分鐘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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