談談OKHttp的幾道面試題

jimuzz發表於2020-11-06

來吧,今天說說常用的網路框架OKHttp,也是現在Android所用的原生網路框架(Android 4.4開始,HttpURLConnection的底層實現被Google改成了OkHttp),GOGOGO!

  • OKHttp有哪些攔截器,分別起什麼作用
  • OkHttp怎麼實現連線池
  • OkHttp裡面用到了什麼設計模式

OKHttp有哪些攔截器,分別起什麼作用

OKHTTP的攔截器是把所有的攔截器放到一個list裡,然後每次依次執行攔截器,並且在每個攔截器分成三部分:

  • 預處理攔截器內容
  • 通過proceed方法把請求交給下一個攔截器
  • 下一個攔截器處理完成並返回,後續處理工作。

這樣依次下去就形成了一個鏈式呼叫,看看原始碼,具體有哪些攔截器:

  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));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

根據原始碼可知,一共七個攔截器:

  • addInterceptor(Interceptor),這是由開發者設定的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共引數,Header都可以在這裡新增。
  • RetryAndFollowUpInterceptor,這裡會對連線做一些初始化工作,以及請求失敗的充實工作,重定向的後續請求工作。跟他的名字一樣,就是做重試工作還有一些連線跟蹤工作。
  • BridgeInterceptor,這裡會為使用者構建一個能夠進行網路訪問的請求,同時後續工作將網路請求回來的響應Response轉化為使用者可用的Response,比如新增檔案型別,content-length計算新增,gzip解包。
  • CacheInterceptor,這裡主要是處理cache相關處理,會根據OkHttpClient物件的配置以及快取策略對請求值進行快取,而且如果本地有了可⽤的Cache,就可以在沒有網路互動的情況下就返回快取結果。
  • ConnectInterceptor,這裡主要就是負責建立連線了,會建立TCP連線或者TLS連線,以及負責編碼解碼的HttpCodec
  • networkInterceptors,這裡也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由於位置不同,所以用處也不同。這個位置新增的攔截器可以看到請求和響應的資料了,所以可以做一些網路除錯。
  • CallServerInterceptor,這裡就是進行網路資料的請求和響應了,也就是實際的網路I/O操作,通過socket讀寫資料。

OkHttp怎麼實現連線池

  • 為什麼需要連線池?

頻繁的進行建立Sokcet連線和斷開Socket是非常消耗網路資源和浪費時間的,所以HTTP中的keepalive連線對於降低延遲和提升速度有非常重要的作用。keepalive機制是什麼呢?也就是可以在一次TCP連線中可以持續傳送多份資料而不會斷開連線。所以連線的多次使用,也就是複用就變得格外重要了,而複用連線就需要對連線進行管理,於是就有了連線池的概念。

OkHttp中使用ConectionPool實現連線池,預設支援5個併發KeepAlive,預設鏈路生命為5分鐘。

  • 怎麼實現的?

1)首先,ConectionPool中維護了一個雙端佇列Deque,也就是兩端都可以進出的佇列,用來儲存連線。
2)然後在ConnectInterceptor,也就是負責建立連線的攔截器中,首先會找可用連線,也就是從連線池中去獲取連線,具體的就是會呼叫到ConectionPool的get方法。

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

也就是遍歷了雙端佇列,如果連線有效,就會呼叫acquire方法計數並返回這個連線。

3)如果沒找到可用連線,就會建立新連線,並會把這個建立的連線加入到雙端佇列中,同時開始執行執行緒池中的執行緒,其實就是呼叫了ConectionPool的put方法。

public final class ConnectionPool {
    void put(RealConnection connection) {
        if (!cleanupRunning) {
        	//沒有連線的時候呼叫
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}

3)其實這個執行緒池中只有一個執行緒,是用來清理連線的,也就是上述的cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //執行清理,並返回下次需要清理的時間。
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout時間內釋放鎖
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

這個runnable會不停的呼叫cleanup方法清理執行緒池,並返回下一次清理的時間間隔,然後進入wait等待。

怎麼清理的呢?看看原始碼:

long cleanup(long now) {
    synchronized (this) {
      //遍歷連線
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //檢查連線是否是空閒狀態,
        //不是,則inUseConnectionCount + 1
        //是 ,則idleConnectionCount + 1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      //如果超過keepAliveDurationNs或maxIdleConnections,
      //從雙端佇列connections中移除
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {      
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {      //如果空閒連線次數>0,返回將要到期的時間
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 連線依然在使用中,返回保持連線的週期5分鐘
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

也就是當如果空閒連線maxIdleConnections超過5個或者keepalive時間大於5分鐘,則將該連線清理掉。

4)這裡有個問題,怎樣屬於空閒連線?

其實就是有關剛才說到的一個方法acquire計數方法:

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

RealConnection中,有一個StreamAllocation虛引用列表allocations。每建立一個連線,就會把連線對應的StreamAllocationReference新增進該列表中,如果連線關閉以後就將該物件移除。

5)連線池的工作就這麼多,並不負責,主要就是管理雙端佇列Deque<RealConnection>,可以用的連線就直接用,然後定期清理連線,同時通過對StreamAllocation的引用計數實現自動回收。

OkHttp裡面用到了什麼設計模式

  • 責任鏈模式

這個不要太明顯,可以說是okhttp的精髓所在了,主要體現就是攔截器的使用,具體程式碼可以看看上述的攔截器介紹。

  • 建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用處是將物件的建立與表示相分離,用Builder組裝各項配置。
比如Request:

public class Request {
  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
    public Request build() {
      return new Request(this);
    }
  }
}
  • 工廠模式

工廠模式和建造者模式類似,區別就在於工廠模式側重點在於物件的生成過程,而建造者模式主要是側重物件的各個引數配置。
例子有CacheInterceptor攔截器中又個CacheStrategy物件:

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

  • 觀察者模式

之前我寫過一篇文章,是關於Okhttp中websocket的使用,由於webSocket屬於長連線,所以需要進行監聽,這裡是用到了觀察者模式:

  final WebSocketListener listener;
  @Override public void onReadMessage(String text) throws IOException {
    listener.onMessage(this, text);
  }

  • 單例模式

這個就不舉例了,每個專案都會有

  • 另外有的部落格還說到了策略模式,門面模式等,這些大家可以網上搜搜,畢竟每個人的想法看法都會不同,細心找找可能就會發現。

拜拜

有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。

相關文章