OkHttp原理8連問,我沒懵,面試官懵了……

OkHttp可以說是Android開發(fā)中最常見的網(wǎng)絡(luò)請求框架,OkHttp使用方便,擴(kuò)展性強(qiáng),功能強(qiáng)大,OKHttp源碼與原理也是面試中的???。

但是OKHttp的源碼內(nèi)容比較多,想要學(xué)習(xí)它的源碼往往千頭萬緒,一時(shí)抓不住重點(diǎn)。本文從幾個(gè)問題出發(fā)梳理OKHttp相關(guān)知識點(diǎn),以便快速構(gòu)建OKHttp知識體系,如果對你有用,歡迎點(diǎn)贊~

本文主要包括以下內(nèi)容:

  1. OKHttp請求的整體流程是怎樣的?
  2. OKHttp分發(fā)器是怎樣工作的?
  3. OKHttp攔截器是如何工作的?
  4. 應(yīng)用攔截器和網(wǎng)絡(luò)攔截器有什么區(qū)別?
  5. OKHttp如何復(fù)用TCP連接?
  6. OKHttp空閑連接如何清除?
  7. OKHttp有哪些優(yōu)點(diǎn)?
  8. OKHttp框架中用到了哪些設(shè)計(jì)模式

OKHttp請求整體流程介紹

首先來看一個(gè)最簡單的Http請求是如何發(fā)送的。

   val okHttpClient = OkHttpClient()
   val request: Request = Request.Builder()
       .url("https://www.google.com/")
       .build()

   okHttpClient.newCall(request).enqueue(object :Callback{
       override fun onFailure(call: Call, e: IOException) {
       }

       override fun onResponse(call: Call, response: Response) {
       }
   })

這段代碼看起來比較簡單,OkHttp請求過程中最少只需要接觸OkHttpClient、Request、Call、 Response,但是框架內(nèi)部會進(jìn)行大量的邏輯處理。

所有網(wǎng)絡(luò)請求的邏輯大部分集中在攔截器中,但是在進(jìn)入攔截器之前還需要依靠分發(fā)器來調(diào)配請求任務(wù)。

關(guān)于分發(fā)器與攔截器,我們在這里先簡單介紹下,后續(xù)會有更加詳細(xì)的講解。

  • 分發(fā)器:內(nèi)部維護(hù)隊(duì)列與線程池,完成請求調(diào)配;
  • 攔截器:五大默認(rèn)攔截器完成整個(gè)請求過程。

整個(gè)網(wǎng)絡(luò)請求過程大致如上所示:

  1. 通過建造者模式構(gòu)建OKHttpClient與 Request
  2. OKHttpClient通過newCall發(fā)起一個(gè)新的請求
  3. 通過分發(fā)器維護(hù)請求隊(duì)列與線程池,完成請求調(diào)配
  4. 通過五大默認(rèn)攔截器完成請求重試,緩存處理,建立連接等一系列操作
  5. 得到網(wǎng)絡(luò)請求結(jié)果

OKHttp分發(fā)器是怎樣工作的?

分發(fā)器的主要作用是維護(hù)請求隊(duì)列與線程池,比如我們有100個(gè)異步請求,肯定不能把它們同時(shí)請求,而是應(yīng)該把它們排隊(duì)分個(gè)類,分為正在請求中的列表和正在等待的列表,等請求完成后,即可從等待中的列表中取出等待的請求,從而完成所有的請求。

而這里同步請求各異步請求又略有不同。

同步請求

synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
}

因?yàn)橥秸埱蟛恍枰€程池,也不存在任何限制。所以分發(fā)器僅做一下記錄。后續(xù)按照加入隊(duì)列的順序同步請求即可。

異步請求

synchronized void enqueue(AsyncCall call) {
    //請求數(shù)最大不超過64,同一Host請求不能超過5個(gè)
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost)     {
        runningAsyncCalls.add(call);
        executorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
}

當(dāng)正在執(zhí)行的任務(wù)未超過最大限制64,同時(shí)同一Host的請求不超過5個(gè),則會添加到正在執(zhí)行隊(duì)列,同時(shí)提交給線程池。否則先加入等待隊(duì)列。每個(gè)任務(wù)完成后,都會調(diào)用分發(fā)器的finished方法,這里面會取出等待隊(duì)列中的任務(wù)繼續(xù)執(zhí)行。

OKHttp攔截器是怎樣工作的?

經(jīng)過上面分發(fā)器的任務(wù)分發(fā),下面就要利用攔截器開始一系列配置了。

# RealCall
  override fun execute(): Response {
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }

我們再來看下RealCall的execute方法,可以看出,最后返回了getResponseWithInterceptorChain,責(zé)任鏈的構(gòu)建與處理其實(shí)就是在這個(gè)方法里面。

internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,interceptors = interceptors,index = 0
    )
    val response = chain.proceed(originalRequest)
  }

如上所示,構(gòu)建了一個(gè)OkHttp攔截器的責(zé)任鏈。

責(zé)任鏈,顧名思義,就是用來處理相關(guān)事務(wù)責(zé)任的一條執(zhí)行鏈,執(zhí)行鏈上有多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都有機(jī)會(條件匹配)處理請求事務(wù),如果某個(gè)節(jié)點(diǎn)處理完了就可以根據(jù)實(shí)際業(yè)務(wù)需求傳遞給下一個(gè)節(jié)點(diǎn)繼續(xù)處理或者返回處理完畢。

如上所示責(zé)任鏈添加的順序及作用如下表所示:

我們的網(wǎng)絡(luò)請求就是這樣經(jīng)過責(zé)任鏈一級一級的遞推下去,最終會執(zhí)行到CallServerInterceptor的intercept方法,此方法會將網(wǎng)絡(luò)響應(yīng)的結(jié)果封裝成一個(gè)Response對象并return。之后沿著責(zé)任鏈一級一級的回溯,最終就回到getResponseWithInterceptorChain方法的返回,如下圖所示:

應(yīng)用攔截器和網(wǎng)絡(luò)攔截器有什么區(qū)別?

從整個(gè)責(zé)任鏈路來看,應(yīng)用攔截器是最先執(zhí)行的攔截器,也就是用戶自己設(shè)置request屬性后的原始請求,而網(wǎng)絡(luò)攔截器位于ConnectInterceptor和CallServerInterceptor之間,此時(shí)網(wǎng)絡(luò)鏈路已經(jīng)準(zhǔn)備好,只等待發(fā)送請求數(shù)據(jù)。它們主要有以下區(qū)別。

  1. 首先,應(yīng)用攔截器在RetryAndFollowUpInterceptor和CacheInterceptor之前,所以一旦發(fā)生錯(cuò)誤重試或者網(wǎng)絡(luò)重定向,網(wǎng)絡(luò)攔截器可能執(zhí)行多次,因?yàn)橄喈?dāng)于進(jìn)行了二次請求,但是應(yīng)用攔截器永遠(yuǎn)只會觸發(fā)一次。另外如果在CacheInterceptor中命中了緩存就不需要走網(wǎng)絡(luò)請求了,因此會存在短路網(wǎng)絡(luò)攔截器的情況。
  2. 其次,除了CallServerInterceptor之外,每個(gè)攔截器都應(yīng)該至少調(diào)用一次realChain.proceed方法。實(shí)際上在應(yīng)用攔截器這層可以多次調(diào)用proceed方法(本地異常重試)或者不調(diào)用proceed方法(中斷),但是網(wǎng)絡(luò)攔截器這層連接已經(jīng)準(zhǔn)備好,可且僅可調(diào)用一次proceed方法。
  3. 最后,從使用場景看,應(yīng)用攔截器因?yàn)橹粫{(diào)用一次,通常用于統(tǒng)計(jì)客戶端的網(wǎng)絡(luò)請求發(fā)起情況;而網(wǎng)絡(luò)攔截器一次調(diào)用代表了一定會發(fā)起一次網(wǎng)絡(luò)通信,因此通??捎糜诮y(tǒng)計(jì)網(wǎng)絡(luò)鏈路上傳輸?shù)臄?shù)據(jù)。

OKHttp如何復(fù)用TCP連接?

ConnectInterceptor的主要工作就是負(fù)責(zé)建立TCP連接,建立TCP連接需要經(jīng)歷三次握手四次揮手等操作,如果每個(gè)HTTP請求都要新建一個(gè)TCP消耗資源比較多。

而Http1.1已經(jīng)支持keep-alive,即多個(gè)Http請求復(fù)用一個(gè)TCP連接,OKHttp也做了相應(yīng)的優(yōu)化,下面我們來看下OKHttp是怎么復(fù)用TCP連接的。

ConnectInterceptor中查找連接的代碼會最終會調(diào)用到ExchangeFinder.findConnection方法,具體如下:

# ExchangeFinder
//為承載新的數(shù)據(jù)流 尋找 連接。尋找順序是 已分配的連接、連接池、新建連接
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
  synchronized (connectionPool) {
    // 1.嘗試使用 已給數(shù)據(jù)流分配的連接.(例如重定向請求時(shí),可以復(fù)用上次請求的連接)
    releasedConnection = transmitter.connection;
    result = transmitter.connection;

    if (result == null) {
      // 2. 沒有已分配的可用連接,就嘗試從連接池獲取。(連接池稍后詳細(xì)講解)
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
        result = transmitter.connection;
      }
    }
  }

  synchronized (connectionPool) {
    if (newRouteSelection) {
      //3. 現(xiàn)在有了IP地址,再次嘗試從連接池獲取??赡軙?yàn)檫B接合并而匹配。(這里傳入了routes,上面的傳的null)
      routes = routeSelection.getAll();
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, false)) {
        foundPooledConnection = true;
        result = transmitter.connection;
      }
    }

  // 4.第二次沒成功,就把新建的連接,進(jìn)行TCP + TLS 握手,與服務(wù)端建立連接. 是阻塞操作
  result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
      connectionRetryEnabled, call, eventListener);

  synchronized (connectionPool) {
    // 5. 最后一次嘗試從連接池獲取,注意最后一個(gè)參數(shù)為true,即要求 多路復(fù)用(http2.0)
    //意思是,如果本次是http2.0,那么為了保證 多路復(fù)用性,(因?yàn)樯厦娴奈帐植僮鞑皇蔷€程安全)會再次確認(rèn)連接池中此時(shí)是否已有同樣連接
    if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
      // 如果獲取到,就關(guān)閉我們創(chuàng)建里的連接,返回獲取的連接
      result = transmitter.connection;
    } else {
      //最后一次嘗試也沒有的話,就把剛剛新建的連接存入連接池
      connectionPool.put(result);
    }
  }

  return result;
}

上面精簡了部分代碼,可以看出,連接攔截器使用了5種方法查找連接。

  1. 首先會嘗試使用 已給請求分配的連接。(已分配連接的情況例如重定向時(shí)的再次請求,說明上次已經(jīng)有了連接)
  2. 若沒有 已分配的可用連接,就嘗試從連接池中匹配獲取。因?yàn)榇藭r(shí)沒有路由信息,所以匹配條件:address一致——host、port、代理等一致,且匹配的連接可以接受新的請求。
  3. 若從連接池沒有獲取到,則傳入routes再次嘗試獲取,這主要是針對Http2.0的一個(gè)操作,Http2.0可以復(fù)用square.com與square.ca的連接
  4. 若第二次也沒有獲取到,就創(chuàng)建RealConnection實(shí)例,進(jìn)行TCP + TLS握手,與服務(wù)端建立連接。
  5. 此時(shí)為了確保Http2.0連接的多路復(fù)用性,會第三次從連接池匹配。因?yàn)樾陆⒌倪B接的握手過程是非線程安全的,所以此時(shí)可能連接池新存入了相同的連接。
  6. 第三次若匹配到,就使用已有連接,釋放剛剛新建的連接;若未匹配到,則把新連接存入連接池并返回。

以上就是連接攔截器嘗試復(fù)用連接的操作,流程圖如下:

OKHttp空閑連接如何清除?

上面說到我們會建立一個(gè)TCP連接池,但如果沒有任務(wù)了,空閑的連接也應(yīng)該及時(shí)清除,OKHttp是如何做到的呢?

  # RealConnectionPool
  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce(): Long = cleanup(System.nanoTime())
  }

  long cleanup(long now) {
    int inUseConnectionCount = 0;//正在使用的連接數(shù)
    int idleConnectionCount = 0;//空閑連接數(shù)
    RealConnection longestIdleConnection = null;//空閑時(shí)間最長的連接
    long longestIdleDurationNs = Long.MIN_VALUE;//最長的空閑時(shí)間

    //遍歷連接:找到待清理的連接, 找到下一次要清理的時(shí)間(還未到最大空閑時(shí)間)
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //若連接正在使用,continue,正在使用連接數(shù)+1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
        //空閑連接數(shù)+1
        idleConnectionCount++;

        // 賦值最長的空閑時(shí)間和對應(yīng)連接
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      //若最長的空閑時(shí)間大于5分鐘 或 空閑數(shù) 大于5,就移除并關(guān)閉這個(gè)連接
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // else,就返回 還剩多久到達(dá)5分鐘,然后wait這個(gè)時(shí)間再來清理
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        //連接沒有空閑的,就5分鐘后再嘗試清理.
        return keepAliveDurationNs;
      } else {
        // 沒有連接,不清理
        cleanupRunning = false;
        return -1;
      }
    }
    //關(guān)閉移除的連接
    closeQuietly(longestIdleConnection.socket());

    //關(guān)閉移除后 立刻 進(jìn)行下一次的 嘗試清理
    return 0;
  }

思路還是很清晰的:

  1. 在將連接加入連接池時(shí)就會啟動定時(shí)任務(wù)。
  2. 有空閑連接的話,如果最長的空閑時(shí)間大于5分鐘或空閑數(shù)大于5,就移除關(guān)閉這個(gè)最長空閑連接;如果空閑數(shù)不大于5且最長的空閑時(shí)間不大于5分鐘,就返回到5分鐘的剩余時(shí)間,然后等待這個(gè)時(shí)間再來清理。
  3. 沒有空閑連接就等5分鐘后再嘗試清理。
  4. 沒有連接不清理。

流程如下圖所示:

OKHttp有哪些優(yōu)點(diǎn)?

  1. 使用簡單,在設(shè)計(jì)時(shí)使用了外觀模式,將整個(gè)系統(tǒng)的復(fù)雜性給隱藏起來,將子系統(tǒng)接口通過一個(gè)客戶端OkHttpClient統(tǒng)一暴露出來。
  2. 擴(kuò)展性強(qiáng),可以通過自定義應(yīng)用攔截器與網(wǎng)絡(luò)攔截器,完成用戶各種自定義的需求
  3. 功能強(qiáng)大,支持Spdy、Http1.X、Http2、以及WebSocket等多種協(xié)議
  4. 通過連接池復(fù)用底層TCP(Socket),減少請求延時(shí)
  5. 無縫的支持GZIP減少數(shù)據(jù)流量
  6. 支持?jǐn)?shù)據(jù)緩存,減少重復(fù)的網(wǎng)絡(luò)請求
  7. 支持請求失敗自動重試主機(jī)的其他ip,自動重定向

OKHttp框架中用到了哪些設(shè)計(jì)模式?

  1. 構(gòu)建者模式:OkHttpClient與Request的構(gòu)建都用到了構(gòu)建者模式。
  2. 外觀模式:OkHttp使用了外觀模式,將整個(gè)系統(tǒng)的復(fù)雜性給隱藏起來,將子系統(tǒng)接口通過一個(gè)客戶端OkHttpClient統(tǒng)一暴露出來。
  3. 責(zé)任鏈模式:OKHttp的核心就是責(zé)任鏈模式,通過5個(gè)默認(rèn)攔截器構(gòu)成的責(zé)任鏈完成請求的配置。
  4. 享元模式:享元模式的核心即池中復(fù)用,OKHttp復(fù)用TCP連接時(shí)用到了連接池,同時(shí)在異步請求中也用到了線程池。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容