談談OKHttp的幾道面試題

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

  • OKHttp有哪些攔截器,分別起什麼做用
  • OkHttp怎麼實現鏈接池
  • OkHttp裏面用到了什麼設計模式

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

OKHTTP的攔截器是把全部的攔截器放到一個list裏,而後每次依次執行攔截器,而且在每一個攔截器分紅三部分:web

  • 預處理攔截器內容
  • 經過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鏈接中能夠持續發送多份數據而不會斷開鏈接。因此鏈接的屢次使用,也就是複用就變得格外重要了,而複用鏈接就須要對鏈接進行管理,因而就有了鏈接池的概念。websocket

OkHttp中使用ConectionPool實現鏈接池,默認支持5個併發KeepAlive,默認鏈路生命爲5分鐘。cookie

  • 怎麼實現的?

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)其實這個線程池中只有一個線程,是用來清理鏈接的,也就是上述的cleanupRunnablesocket

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);
  }
  • 單例模式

這個就不舉例了,每一個項目都會有

  • 另外有的博客還說到了策略模式,門面模式等,這些你們能夠網上搜搜,畢竟每一個人的想法見解都會不一樣,細心找找可能就會發現。

拜拜

有一塊兒學習的小夥伴能夠關注下❤️個人公衆號——碼上積木,天天剖析一個知識點,咱們一塊兒積累知識。

相關文章
相關標籤/搜索