OkHttpClient 源碼分析 2(基於3.9.0的源碼)

OkHttpClient框架 使用了大量的設計模式 builder ,好處是能夠減小框架的使用者傳入不少參數,使用 builder 內部的默認值便可完成網絡請求,對默認值進行簡單的一一分析:

public Builder() {
     dispatcher = new Dispatcher();
      protocols = OkHttpClient.DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
    }
複製代碼

new Dispatcher()

每次建立 okHttpClient 都會建立一個新的 Dispatcher,這裏有線程池,若是須要建立多個 okHttpClient 最好傳入這個參數,並複用線程池 這個類的主要做用是處理okHttpClient.newCall發送請求的。有異步的請求 enqueue 和同步的請求 executed 還有處理請求結束的 finished(AsyncCall/RealCall)html

synchronized void enqueue(AsyncCall call) {
   if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {//總最大請求數和同個 ip 最大請求數的限制
     runningAsyncCalls.add(call);
     executorService().execute(call);//執行請求
   } else {
     readyAsyncCalls.add(call);//放到隊列中等待調用 finish
   }
 }
 /** 處理異步請求,這裏設計到了1個方法4個全局變量,一個個說: @methord executorService() 建立了一個線程池無最大上限,閒置60秒留存0個核心線程 @filed maxRequests 最大同時發起請求的數量默認64個,實際能夠經過ExecutorService的maximumPoolSize和BlockingQueue控制,不知道框架爲何弄一個這個變量 @filed maxRequestsPerHost每一個ip最大的請求數,默認5個,設計這個字段多是爲了不某個服務器同時處理多個請求致使單個請求總時間變長。 @filed runningAsyncCalls若是請求數量未超出maxRequests和maxRequestsPerHost的限制,則加到這個隊列裏,保存了發送的請求,按照默認的executorService,沒有在隊列裏等待的請求 @filed readyAsyncCalls超出了限制,會加到這個隊列裏等待。 **/
複製代碼
synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }
 /**同步請求newCall.execute會調用到這裏,android中這個方法會在子線程調用 @filed runningSyncCalls 這個隊列裏保存了同步的請求,newCall.execute是有返回值的 **/
複製代碼
public synchronized void setIdleCallback(@Nullable Runnable idleCallback) //當全部請求執行完,會調用 idleCallback.run 複製代碼
public synchronized void cancelAll() //取消全部的請求,包含異步正在執行的runningAsyncCalls,異步隊列中的readyAsyncCalls,同步執行的隊列runningSyncCalls。 複製代碼
public synchronized List<Call> runningCalls() //獲取正在執行的隊列 runningAsyncCalls 複製代碼
public synchronized List<Call> queuedCalls() //獲取等待執行的隊列readyAsyncCalls 複製代碼
<!--內部處理隊列的三個主要方法-->
 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls)//當一個call 執行完調用promoteCalls,全部的 call 都執行完調用idleCallback.run private void promoteCalls()//內部調用runningCallsForHost判斷是否把readyAsyncCalls中的請求放到runningAsyncCalls private int runningCallsForHost(AsyncCall call)//獲取一個ip 下的請求數,在enqueue和promoteCalls中調用判斷是否超出maxRequestsPerHost的限制 複製代碼

OkHttpClient.DEFAULT_PROTOCOLS

Protocol是一個 enum (HTTP_1_0,HTTP_1_1,HTTP_2,SPDY_3)每次建立一個 OkHttpClient 默認都使用了同一個協議組,默認支持 http/1.1和h2。java

static final List<Protocol> DEFAULT_PROTOCOLS;
   DEFAULT_PROTOCOLS = Util.immutableList(new Protocol[]{Protocol.HTTP_2, Protocol.HTTP_1_1});
複製代碼

內部實現只有一個構造方法和一個 get 方法android

/**由此能夠看出,每一個枚舉都會有個 string值**/
  Protocol(String protocol) { 
    this.protocol = protocol;
  }
  
  /** 經過一個 string 串解析是哪一種協議類型 **/
    public static Protocol get(String protocol) throws IOException {
    // Unroll the loop over values() to save an allocation.
    if (protocol.equals(HTTP_1_0.protocol)) return HTTP_1_0;
    if (protocol.equals(HTTP_1_1.protocol)) return HTTP_1_1;
    if (protocol.equals(HTTP_2.protocol)) return HTTP_2;
    if (protocol.equals(SPDY_3.protocol)) return SPDY_3;
    throw new IOException("Unexpected protocol: " + protocol);
  }
複製代碼

OkHttpClient.DEFAULT_CONNECTION_SPECS

每次建立一個 OkHttpClient 默認都使用了同一個協議組,https 的安全認證,在 RetryAndFollowUpInterceptor 生成請求的必要參數 createAddress() 中調用,在源碼分析2的發送請求流程中具體分析 RetryAndFollowUpInterceptor。 ConnectionSpec 中有三個能夠直接使用的常亮 MODERN_TLS 中含有了 tls 1.0, 1.1, 1.2, 1.3和 ssl3; COMPATIBLE_TLS 比 MODERN_TLS多了; CLEARTEXT http 使用,數據不通過加密c++

static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS;
DEFAULT_CONNECTION_SPECS = Util.immutableList(new ConnectionSpec[]{ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT});
複製代碼
/**ConnectionSpec.Builder中 保存了變量 @params tls 是否支持數據加密, @params cipherSuites 加密算法類,使用到了類 CipherSuite,定義了一些算法名稱字符串常亮。 @params tlsVersions 支持的TLS協議版本 @params supportsTlsExtensions 是否支持擴展協議 **/
  public static final class Builder {
    boolean tls;
    @Nullable String[] cipherSuites;
    @Nullable String[] tlsVersions;
    boolean supportsTlsExtensions;
    ......
    }
複製代碼
public boolean isCompatible(SSLSocket socket) {
    if (!tls) {
      return false;
    }
    if (tlsVersions != null && !nonEmptyIntersection(Util.NATURAL_ORDER, tlsVersions, socket.getEnabledProtocols())) {
      return false;
    }
    if (cipherSuites != null && !nonEmptyIntersection(CipherSuite.ORDER_BY_NAME, cipherSuites, socket.getEnabledCipherSuites())) {
      return false;
    }
    return true;
  }
/**判斷 tlsVersions,cipherSuites是否支持服務器的加密版本和算法類型,若是支持 true 這個方法在類 ConnectionSpecSelector 中調用,因爲每一個 OKHttpClient 都有幾個 ConnectionSpec,發送請求的時候用 ConnectionSpecSelector 選擇一個支持的 ConnectionSpec,在 RealConnection.connectTls() 中調用方法apply 複製代碼
/**若是在isCompatible返回了 true,會進到這裏,經過supportedSpec()和服務器支持的版本和算法取交集,在設置給 sslSocket *@params sslSocket 發送請求的 socket。在SSLSocketFactory中設置 https://developer.android.com/reference/javax/net/ssl/SSLSocket.html *@params isFallback 是否由於 SSLHandshakeException 或 SSLProtocolException 失敗過,RealConnection.connect()中 connectionSpecSelector.connectionFailed(e) 判斷異常,若是是上述兩個異常會從新鏈接並isFallback=true **/
  void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);

    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
  }
複製代碼
/**使用intersect方法獲取sslSocket和這個 ConnectionSpec 共同支持的tlsVersions,cipherSuites 當 isFallback=true 時,使用 "TLS_FALLBACK_SCSV"解決重試鏈接失敗的問題(SSLv3有漏洞) @return 並 new 一個新的 ConnectionSpec,保證原有數據不被破壞,可是浪費內存 **/
 private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) 複製代碼

EventListener.factory(EventListener.NONE)

建立 EventListener 的工廠,在 EventListener 中監聽各類回調,好比說connect、request,response。每個 call 能夠建立一個對應的 EventListener。算法

public interface Factory {//建立 listener 的工廠方法
    EventListener create(Call call);
  }
  /**在類 EventListener 的靜態方法factory中 new 了一個內部類 *默認實現的參數 EventListener.NONE 是一個 new EventListener{ } 的空實現 **/
    static EventListener.Factory factory(final EventListener listener) {
    return new EventListener.Factory() {
      public EventListener create(Call call) {
        return listener;
      }
    };
  }
複製代碼
/**調用 new OkHttpClient().newCall(Request) 中會進入這個方法 *@params eventListener 在每一個 RealCall 中的回調。 **/
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
        RealCall call = new RealCall(client, originalRequest, forWebSocket);
        call.eventListener = client.eventListenerFactory().create(call);
        return call;
    }
複製代碼

ProxySelector.getDefault()

java 中對代理的默認處理,使用了手機廠商的默認實現,整個應用只有一個實例,若是不對手機的代理作特殊處理,這裏不要作任何改動。在 RouteSelector 中調用。數據庫

private static ProxySelector defaultSelector = new ProxySelectorImpl();//JDK 中的默認實現

    public static ProxySelector getDefault() {
        return defaultSelector;
    }
//若是想修改代理的處理在這裏設置一個ProxySelector會影響應用中全部的代理,好比想禁止應用使用代理在 select 方法返回 Proxy.NO_PROXY 
    public static void setDefault(ProxySelector selector) {
        defaultSelector = selector;
    }

複製代碼

CookieJar.NO_COOKIES

對 cookie 的處理,若是應用有設置 cookie 的要求須要調用 new OkHttpClient.Builder().cookieJar(CookieJar cookieJar) 設置保存和讀取 cookie 的實現。框架默認是不保存 cookie 的 NO_COOKIES只對接口作了空實現。設計模式

void saveFromResponse(HttpUrl url, List<Cookie> cookies);
  List<Cookie> loadForRequest(HttpUrl url);
複製代碼
/** *HttpHeaders的靜態方法中調用了 saveFromResponse . *@params HttpUrl保存了請求的地址 *@params Headers裏面的數組[key1,value1,key2,value2, ...]保存了請求頭的全部鍵值對. *@methord Cookie.parseAll中調用Cookie.parse解析全部的 Set-Cookie 字段。而後調用CookieJar的saveFromResponse public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) { if (cookieJar == CookieJar.NO_COOKIES) return;//若是不設置 cookie 就不進行解析了,響應速度快 List<Cookie> cookies = Cookie.parseAll(url, headers); if (cookies.isEmpty()) return; cookieJar.saveFromResponse(url, cookies); } 複製代碼
//BridgeInterceptor 的 intercept 方法中調用 loadForRequest ,並設置到請求頭上。BridgeInterceptor在源碼分析1中有詳細的講解
public Response intercept(Chain chain) throws IOException {
    ......
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    ......
}
複製代碼

SocketFactory.getDefault()

java 中對SocketFactory的默認處理,使用了手機廠商的默認實現,整個應用只有一個實例,若是不對手機的Socket作特殊處理,這裏不要作任何改動。數組

private static SocketFactory defaultFactory;
    /**和 proxySelector 不一樣的是隻能 get,不能 set *RetryAndFollowUpInterceptor中把 Socket 設置給了Address *在 RealConnection 使用了address.socketFactory().createSocket() **/
    public static synchronized SocketFactory getDefault() {
        if (defaultFactory == null) {
            defaultFactory = new DefaultSocketFactory();
        }
        return defaultFactory;
    }
複製代碼

OkHostnameVerifier.INSTANCE

若是是 https 的請求要驗證證書,okhttp 裏實現了一套使用 x509 證書格式 的認證。首先 verifyAsIpAddress 判斷是 ipAddress 仍是 hostName, 若是是 ip 證書中的 type 是7,若是是 host,證書中的 type 是2,而後分別判斷證書中是否包含對應的 ip 或者 host。緩存

/**若是是 ip 經過方法 getSubjectAltNames 讀取證書中包含的全部ip。在判斷請求的 ip 是其中一個。 *@params X509Certificate 使用getSubjectAlternativeNames方法得到一個數組,數組中每一個條目包含了 type=7 和 ip 或者 type=2和 host。 private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME); for (int i = 0, size = altNames.size(); i < size; i++) { if (ipAddress.equalsIgnoreCase(altNames.get(i))) { return true; } } return false; } 複製代碼
/** *和判斷 ip 的相似,這個方法是判斷 hostname 的 *@methord verifyHostname 因爲證書中存的多是帶*號的二級域名,可是 hostname 是一個三級或者四級域名,匹配規則又不能夠用正則。就有了這個方法 *下面有一段特殊的判斷證書含有 cn 的狀況,因爲對ssl 不是特別瞭解,看不懂。 **/
  private boolean verifyHostname(String hostname, X509Certificate certificate) {
    hostname = hostname.toLowerCase(Locale.US);
    boolean hasDns = false;
    List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
    for (int i = 0, size = altNames.size(); i < size; i++) {
      hasDns = true;
      if (verifyHostname(hostname, altNames.get(i))) {
        return true;
      }
    }
    if (!hasDns) {
      X500Principal principal = certificate.getSubjectX500Principal();
      // RFC 2818 advises using the most specific name for matching.
      String cn = new DistinguishedNameParser(principal).findMostSpecific("cn");
      if (cn != null) {
        return verifyHostname(hostname, cn);
      }
    }
    return false;
  }
複製代碼

CertificatePinner.DEFAULT

安全性處理,若是客戶端發送給服務器的數據有很高的保密要求,不但願被任何假裝服務器接受或者代理層攔截,可使用 new OkHttpClient.Builder().certificatePinner(CertificatePinner certificatePinner) 客戶端裏的對正式的合法性校驗,這個方法在RealConnection的connectTls中調用,當完成 ssl 的握手,拿到服務器的正式。對證書 check,若是證書 check 失敗拋出異常。安全

//默認不添加任何限制
public static final CertificatePinner DEFAULT = new Builder().build();
複製代碼
/**全部的驗證限制在全局變量pins中,這裏是一個數組,只要有一個命中則驗證經過。 *pin中保存了加密的數據和hostname *@methord findMatchingPins 找到含有 hostname 的 pins。 *@methord sha25六、sha1 至支持這兩種算法,對 x509中 publickey 加密,防止 publickey 直接被代碼編寫者看到 **/
 public void check(String hostname, List<Certificate> peerCertificates) throws SSLPeerUnverifiedException {
    List<Pin> pins = findMatchingPins(hostname);
    if (pins.isEmpty()) return;//若是是空的則正常進行請求
    if (certificateChainCleaner != null) {//選定某幾個x509證書,由於有可能返回的正式裏包含了多個域名的認證
      peerCertificates = certificateChainCleaner.clean(peerCertificates, hostname);
    }

    for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
      X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);

      // Lazily compute the hashes for each certificate.
      ByteString sha1 = null;
      ByteString sha256 = null;
    /**主要的驗證邏輯,第一層 for 是證書,第二層 for 是 pin, *若是有任何一個證書和 pin 拼配成功,則能夠進行後面的請求, *不然拋出異常AssertionError **/
      for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
        Pin pin = pins.get(p);
        if (pin.hashAlgorithm.equals("sha256/")) {
          if (sha256 == null) sha256 = sha256(x509Certificate);
          if (pin.hash.equals(sha256)) return; // Success!
        } else if (pin.hashAlgorithm.equals("sha1/")) {
          if (sha1 == null) sha1 = sha1(x509Certificate);
          if (pin.hash.equals(sha1)) return; // Success!
        } else {
          throw new AssertionError("unsupported hashAlgorithm: " + pin.hashAlgorithm);
        }
      }
    }
    //這裏官方的註釋很到位,只是在拋出異常的時候,打個很詳細的 log 
    // If we couldn't find a matching pin, format a nice exception.
    StringBuilder message = new StringBuilder()
        .append("Certificate pinning failure!")
        .append("\n Peer certificate chain:");
    for (int c = 0, certsSize = peerCertificates.size(); c < certsSize; c++) {
      X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(c);
      message.append("\n ").append(pin(x509Certificate))
          .append(": ").append(x509Certificate.getSubjectDN().getName());
    }
    message.append("\n Pinned certificates for ").append(hostname).append(":");
    for (int p = 0, pinsSize = pins.size(); p < pinsSize; p++) {
      Pin pin = pins.get(p);
      message.append("\n ").append(pin);
    }
    throw new SSLPeerUnverifiedException(message.toString());
  }
複製代碼

Authenticator.NONE

proxyAuthenticator,authenticator 都是默認值 NONE,接口中只有一個方法。 authenticator 當服務器返回的響應碼是401的時候,須要對服務器進行登陸受權。若是須要繼續執行登陸的操做,就複寫authenticate,返回一個調用登陸的 request。 proxyAuthenticator當服務器返回的響應碼是407的時候,須要對代理服務器進行登陸受權。若是須要繼續執行登陸代理服務器的操做,就複寫authenticate,返回一個登陸代理的 request

public interface Authenticator {
  //返回一個去受權的請求,替換原有請求
  Request authenticate(Route route, Response response) throws IOException;
}
  Authenticator NONE = new Authenticator() {
    @Override public Request authenticate(Route route, Response response) {
      return null;
    }
  };

複製代碼

new ConnectionPool()

全部方法都在 Internal 中調用,無論有多少個 OkHttpClient,Internal只有一個實例 public static Internal instance;在 OkHttpClient 有一個靜態方法塊 Internal.instance = new Internal() {...各個方法的實現...},不知道爲何這樣設計,Internal和工具類沒差異。

/**在StreamAllocation.findConnection 中建立了 RealConnection 鏈接成功後調用put *@field cleanupRunning 若是以前沒有請求在執行 cleanupRunning 是 false,調用 put 後賦值 true,而且開始在一個單獨的線程執行清理的操做 **/
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
複製代碼
/**這裏使用了cleanupRunning、cleanupRunnable共同控制了清理線程的執行 *當 put 的時候調用execute而且putcleanupRunning=true,全部的任務執行完cleanup返回了-1,中止清理線程,並cleanupRunning=false,等下次 put並執行execute,若是put的時候有任務在執行不調用execute **/
  private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
      /** *cleanup返回-1終止線程,大於0的值就等待waitNanos的時間再次執行 **/
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1) return;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
  
  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // pruneAndGetAllocationCount方法中清理已經被回收的 StreamAllocation 並返回當前存活的數量
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // 計算空置時間最長的一個 RealConnection 的時長.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
    //上面的代碼給4個變量賦值,下面再拿這幾個變量計算出改怎樣執行清理
      if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {
        // 若是超出最大緩存數或者超出最大保活時長,清理掉這個RealConnection
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 再過 return 的時間就要清理掉這個 connection 了
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 若是在執行的立刻能執行完,再過 return 的時間就要清理掉這個 connection 了
        return keepAliveDurationNs;
      } else {
        // 沒有 connect 了,不在執行清理
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());
//走到了第一個 if 中,清理了一個。還須要再次清理一遍
    return 0;
  }
複製代碼

Dns.SYSTEM

hostname 轉 ip 的接口,默認實現Arrays.asList(InetAddress.getAllByName(hostname)),沒有什麼好說的。

List<InetAddress> lookup(String hostname) throws UnknownHostException;
複製代碼

followSslRedirects = true; followRedirects = true;

這兩個變量控制若是有301,302,303重定向,或者 http 轉 https 的接口是否要繼續請求重定向後的Location

retryOnConnectionFailure = true;

當使用鏈接池並無和服務器連通,是否要進行重試。

interceptors,networkInterceptors

每一個 okHttpClient 中都有兩個list 分別保存了這兩個攔截器,在 RealCall 的Response getResponseWithInterceptorChain()方法中調用 interceptors 在請求發起先加到隊列裏,再添加錯誤處理、拼接http協議、緩存處理、創建鏈接的框架中已有的攔截器這時候已經準備好了,又添加了client.networkInterceptors() 最終寫入數據到socket中並獲取服務器返回的流,造成了一條數據調用鏈。

Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//OkHttpClient.Builder.addInterceptor(Interceptor interceptor)
    interceptors.add(retryAndFollowUpInterceptor);//錯誤處理,和其餘的攔截器相比 這個攔截器先初始化了,這樣設計我以爲是處理 cancel 用的
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//拼接http協議 拼接header和body
    interceptors.add(new CacheInterceptor(client.internalCache()));//緩存處理
    interceptors.add(new ConnectInterceptor(client));//使用鏈接池創建鏈接
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//OkHttpClient.Builder.addNetworkInterceptor(Interceptor interceptor)
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//想服務器發起請求
    ......
  }
複製代碼

Cache cache; InternalCache internalCache;

這兩個都是處理緩存的邏輯的,一個是接口,一個是實現類的包裝。設置任何一個均可以使用緩存,框架默認是不使用緩存的。在 new CacheInterceptor(client.internalCache() 中使用了InternalCache,優先使用cache,若是沒有設置cache纔去調用找 InternalCache 個接口。

InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }
複製代碼

Cache中 InternalCache internalCache = new InternalCache(){}這個變量保存了一個實現類,接口的每一個方法都直接調用了cache的方法,接口中主要方法有put、get、remove、update、和兩個計數方法。保存文件的邏輯在DiskLruCache中,這兩個類經過Source和Sink傳遞數據,因爲okio把流的讀寫轉換爲了Source和Sink這兩個接口。因此這樣設計有很好的擴展性,涉及到的類有:

CacheDiskLruCache

用journalFile文件保存了全部緩存的文件key和key對應文件狀態READ、DIRTY、CLEAN、REMOVE,有點像管理緩存文件的數據庫,這樣作的好處是,管理緩存文件是否超過maxSize,文件讀寫佔用時間 判斷狀態防止讀寫混亂。

/**把journalFile文件轉換爲HashMap,文件做爲持久化下次打開初始化數據用。計算數據使用HashMap速度快。 *JournalFile有當前使用文件、一個臨時文件和一個備份文件 三個 *本次初始化後的請求寫到臨時文件和hashMap中**/
  public synchronized void initialize() throws IOException {
    if (initialized) {//防止重複初始化
      return; // Already initialized.
    }
    if (fileSystem.exists(journalFileBackup)) {
      // If journal file also exists just delete backup file.
      if (fileSystem.exists(journalFile)) {
        fileSystem.delete(journalFileBackup);
      } else {
        fileSystem.rename(journalFileBackup, journalFile);//正式文件丟失,把備份文件恢復
      }
    }
    if (fileSystem.exists(journalFile)) {//正式文件存在
      try {
        readJournal();//判斷文件的有效性,把文件中的每一條讀出來添加到HashMap中
        processJournal();//才判斷是否是超出maxSize,若是超出了要刪除多餘的文件
        initialized = true;
        return;
      } catch (IOException journalIsCorrupt) {
        Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
            + journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
      }
      try {
        delete();
      } finally {
        closed = false;
      }
    }
    rebuildJournal();//把此次寫入的文件命名爲臨時文件,上次保留的正式文件改成備份文件,上次寫入的改成正式文件,
    initialized = true;
  }
複製代碼

completeEdit是保存文件和修改文件名的核心方法,裏面沒有什麼複雜的邏輯,DiskLruCache中比較難理解的就是文件狀態處理,只要抓住文件 生成臨時-寫入數據-重命名或刪除 這個順序的線就容易理解了。

/**當本次請求結束的時候,把dirty文件改成clean狀態,標識緩存數據可使用了 *若是請求失敗把dirty文件刪掉 *而且同時保存到journalFile和hashMap *判斷有沒有超出最大致積限制並清理**/
completeEdit(Editor editor, boolean success);
複製代碼

DiskLruCache.Entry 一個請求要保存爲響應體文件,其餘數據通過Cache.Entry轉換的文件,兩個文件。可是讀寫文件的時間不是瞬間完成的,爲了防止產生阻塞,每一個文件都會有clean和dirty兩個狀態,clean文件能夠直接轉換爲Snapshot.source,因此裏面有個方法 snapshot()。

private final class Entry {
    final String key;//請求的惟一標識key
    final long[] lengths;//cleanFiles文件的長度
    final File[] cleanFiles;//數據寫入完了的dirtyFiles會被重命名爲cleanFiles
    final File[] dirtyFiles;//在newSink方法中把這個文件轉換爲sink,讓cache寫入數據
    boolean readable;//可讀狀態 journal文件中保存的key是CLEAN ,值爲true
    Editor currentEditor;//當前編輯這個類的Editor
    }
複製代碼

DiskLruCache.Snapshot 和Entry一一對應,方便和CacheResponseBody的類傳遞Source。每一個請求都會有一個對應的 Snapshot,保存了要實例化的Source,和每一個source的對應的文件長度length(計算緩存體積是否超過maxSize,若是每次都讀取文件的大小太耗性能了)。

public final class Snapshot implements Closeable {
    private final String key;//和entity對應的key
    private final Source[] sources;//和entity對應的cleanFiles
    private final long[] lengths;//和entity的length一致
    }
複製代碼

DiskLruCache.Editor 和Entry一一對應,是Entry的操做類,和SharedPreferences.edit是一個做用,把entity轉換爲流和Cache傳遞數據,主要方法有兩個:

public Source newSource(int index) {
        ......
        return fileSystem.source(entry.cleanFiles[index]);//把cleanFiles文件提供爲source讀取
        ......
    }
     public Sink newSink(int index) {
        ......
        File dirtyFile = entry.dirtyFiles[index];//把dirtyFiles文件提供爲sink寫入,不是cleanFiles,由於數據存完了會把dirty重命名爲clean
        sink = fileSystem.sink(dirtyFile);
        ......
    }
複製代碼

Cache.Entry 除了響應體的部分實現類和string的相互轉換,寫入Sink或者讀取Source的持久化爲文件,主要方法Entry(Source in)根據流初始化,writeTo(DiskLruCache.Editor editor)把裏面的數據寫入到文件中。 和android.os.Parcelable是很相似的,這樣設計是OkHttpClient爲全部的java項目設計的不單獨爲android。大部分代碼是按照順序寫入字符串的,就不粘代碼了,沒什麼邏輯

Cache

remove只是刪除文件的操做,track的兩個方法都是打點計數用,並無實質的意義,這裏不作分析,只分析put,update,get三個主要方法

CacheRequest put(Response response) {
    String requestMethod = response.request().method();
    if (HttpMethod.invalidatesCache(response.request().method())) {//這裏post方法是不支持緩存的注意了
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {//這裏post方法是不支持緩存的注意了
      return null;
    }
    if (HttpHeaders.hasVaryAll(response)) {//查找Vary變量,若是裏面含有 * 不支持緩存
      return null;
    }
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));//根據response的url生成一個md5獲取一個DiskLruCache.Editor,準備寫入數據用
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);//把response寫入到editor中,這裏只寫入了header+line
      return new CacheRequestImpl(editor);//把response寫入到editor中,這裏只寫入了body,並commit
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }
複製代碼
/**這個方法是返回響應碼是304的時候調用,只更新頭信息不用更新body void update(Response cached, Response network) { Entry entry = new Entry(network); DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot; DiskLruCache.Editor editor = null; try { editor = snapshot.edit(); // Returns null if snapshot is not current. if (editor != null) { entry.writeTo(editor);//沒用調用new CacheRequestImpl(editor),只把頭信息更新了 editor.commit(); } } catch (IOException e) { abortQuietly(editor); } } 複製代碼
Response get(Request request) {
    String key = key(request.url());//計算md5
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      snapshot = cache.get(key);//獲取保存緩存的流
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {//當前可能正在寫入,不可讀,返回空
      // Give up because the cache cannot be read.
      return null;
    }
    try {
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));//獲取header+line
    } catch (IOException e) {//能夠正常拿到響應信息
      Util.closeQuietly(snapshot);
      return null;
    }
    Response response = entry.response(snapshot);//實例化響應體,造成一個完整的response
    if (!entry.matches(request, response)) {//判斷緩存的請求和法國來的請求是否相同
      Util.closeQuietly(response.body());
      return null;
    }
    return response;
  }
複製代碼

create by dingshaoran

相關文章
相關標籤/搜索