okhttp_緩存分析_上

okhhtp緩存分析_上#

**注:**本文分析的基礎是在大概瞭解了okhhtp的基礎上分析的如果不瞭解的話建議看下okhhtp的網絡流程https://blog.csdn.net/wkk_ly/article/details/81004920

前言:
okhttp_緩存分析分爲上下兩部分,主要從以下幾個方面來分析okhhtp緩存

  1. okhttp緩存如何聲明使用緩存
  2. okhttp在什麼情況下緩存網絡響應(這個涉及我們更好的使用okhttp緩存)
  3. okhhtp在什麼情況下使用緩存而不是使用網絡請求(這個涉及我們更好的使用okhttp緩存)
  4. okhhtp如何存儲緩存的
  5. okhhtp是如何取出緩存的
  6. 總結okhhtp的緩存&&使用
  7. 附錄cache請求(響應)頭的各個字段及其含義

上篇分析 1,2,3,

下篇分析 4,5,6,7

如何使用okhhtp緩存

首先還是看下官方實例:

  private final OkHttpClient client;

public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
//需要指定一個私有目錄,以及緩存限制的大小,官方給出的限制是10MB
Cache cache = new Cache(cacheDirectory, cacheSize);

client = new OkHttpClient.Builder()
    .cache(cache)
    .build();
 }

嗯 okhttp實現緩存配置就是你需要在OkHttpClient配置中設置緩存,如上就是設置一個cache 這個cache類需要指定一個目錄以及一個緩存的文件大小,但是有個要求就是:
這個緩存目錄要是私有的,而且這個緩存目錄只能是單個緩存訪問否則就會異常,我理解的就是隻能是一個應用訪問或者說是隻能是一個具有相同配置的OkHttpClient訪問,

研究緩存前的準備

下面我們來看看okhttp內部是如何實現緩存的,因爲okhttp是責任鏈模式,所以我們直接看okhttp的緩存攔截器,即:CacheInterceptor就可以分析它的緩存實現
注:如果不瞭解.okhttp的責任鏈可以看下我的上篇文章https://blog.csdn.net/wkk_ly/article/details/81004920
不過我們需要先做一點準備工作:我們要看下緩存攔截器是如何設置的(換句話說是如何添加進攔截鏈的)
在RealCall類的getResponseWithInterceptorChain()是設置攔截鏈並獲取網絡響應的方法:

Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList<>();
....
//這行代碼是設置緩存攔截器,
interceptors.add(new CacheInterceptor(client.internalCache()));
....  
  }

ok我們在這裏知道了緩存攔截器的設置,那我們再來看看client的internalCache()方法是什麼,點進來後是下面的內容

InternalCache internalCache() {
//這個cache記得我們使用okhttp緩存時設置的代碼cache(cache)嗎 對這個cache就是我們設置的cache,也就是說我們需要傳入的cache的內部實現接口internalCache
//當我們沒有設置cache是就會取默認的internalCache,那這個internalCache是什麼呢,實際上okhttp.builder有如下的方法
// void setInternalCache(@Nullable InternalCache internalCache) {
//  this.internalCache = internalCache;
//  this.cache = null;
//}
//InternalCache是一個實現緩存策略的接口,就是說我們可以自定義緩存實現方法,只要實現InternalCache接口並設置進來就可以了,但是我們本文的主題是分析okhhtp的緩存策略,
//所以我們在這不在分析自定義的緩存策略
return cache != null ? cache.internalCache : internalCache;
 }

好了我們現在知道了,緩存攔截器需要初始化的時候穿進去換一個緩存實現策略,而這個策略就是我們在設置okhhtp緩存時設置進去的,下面粘貼下緩存策略接口InternalCache,注意我們在之前的設置的Cache類內部是實現了該接口的(他本是並沒有實現該接口,而是在內部聲明瞭一個內部類實現了該方法),關於Cache類的源碼在此先不粘貼了 我們後面會進行研究討論,

	public interface InternalCache {
	//根據請求獲取緩存響應
  Response get(Request request) throws IOException;
	//存儲網絡響應,並將原始的請求處理返回緩存請求
  CacheRequest put(Response response) throws IOException;

  /**
   * Remove any cache entries for the supplied {@code request}. This is invoked when the client
   * invalidates the cache, such as when making POST requests.
   */
//當緩存無效的時候移除緩存(),特別是不支持的網絡請求方法,okhhtp只支持緩存get請求方法,其他的都不支持,之後我們會在代碼中看到的
  void remove(Request request) throws IOException;

  /**
   * Handles a conditional request hit by updating the stored cache response with the headers from
   * {@code network}. The cached response body is not updated. If the stored response has changed
   * since {@code cached} was returned, this does nothing.
   */
  void update(Response cached, Response network);

  /** Track an conditional GET that was satisfied by this cache. */
  void trackConditionalCacheHit();

  /** Track an HTTP response being satisfied with {@code cacheStrategy}. */
  void trackResponse(CacheStrategy cacheStrategy);
}

okhttp在什麼情況下緩存網絡響應

我們這一小節的標題是分析存儲緩存 所以我們忽略其他的部分,我們首先默認我們設置了緩存策略但是此時沒有任何緩存存儲,這樣可以更方便我們分析
那我們直接看緩存攔截器的 攔截方法即是intercept(Chain chain)方法

@Override public Response intercept(Chain chain) throws IOException {
.....
Response networkResponse = null;
try {
//這裏獲取網絡響應
  networkResponse = chain.proceed(networkRequest);
} finally {
  // If we're crashing on I/O or otherwise, don't leak the cache body.
  if (networkResponse == null && cacheCandidate != null) {
    closeQuietly(cacheCandidate.body());
  }
}
...
Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();
//判斷是否有緩存策略 我們默認是設置了緩存策略,所此時爲true,
if (cache != null) {
	//這個判斷是是否有響應體,我們首先假設是正常的請求並且獲得正常的響應(關於如何判斷是否有響應體,我們稍後研究),所以我們這裏主要研究 CacheStrategy.isCacheable(response, networkRequest)
	//這個是判斷是否緩存的主要依據
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Offer this request to the cache.
	//調用緩存策略,緩存網絡響應
    CacheRequest cacheRequest = cache.put(response);
	//返回響應,這個我們在下面分析
    return cacheWritingResponse(cacheRequest, response);
  }
//這裏是如果是不支持的網絡請求方法,則刪除緩存
  if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }
}

return response;
}

在上面的代碼中 我們知道了如果要緩存網絡請求主要是有3個依據
1.要有緩存策略(可以是自定義的,也可以是用okhttp提供的Cache),否則不緩存
2,要有響應體,否則不緩存
3,要滿足緩存條件否則不緩存,
第一個條件我們都知道,必須設置緩存策略否則肯定不緩存的,那我們重點來看下面這兩個條件

要有響應體否則不緩存

字面意思當然是很好理解了,沒有網絡響應體,的確沒有響應體自然沒有緩存的必要了 不過我們還是要看下這個判斷的(雖然我認爲沒什麼必要),我們看下這個方法

public static boolean hasBody(Response response) {
// HEAD requests never yield a body regardless of the response headers.
//如果網絡請求的方法是head方法則返回false,
//Head方法 在服務器的響應中只返回響應頭,不會返回響應體
if (response.request().method().equals("HEAD")) {
  return false;
}

int responseCode = response.code();
//HTTP_CONTINUE是100 HTTP_NO_CONTENT是204 HTTP_NOT_MODIFIED是304
//解釋看下面(*)
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
    && responseCode != HTTP_NO_CONTENT
    && responseCode != HTTP_NOT_MODIFIED) {
  return true;
}

// If the Content-Length or Transfer-Encoding headers disagree with the response code, the
// response is malformed. For best compatibility, we honor the headers.
//如果響應頭的Content-Length和Transfer-Encoding字段和返回狀態碼不一致的時候 按照響應頭爲準
//即是如果響應嗎不在上述的要求內,但是響應頭又符合又響應體的要求則返回true
//注:Content-Length:這是表示響應體的長度,contentLength(response)也就是獲取該字段的值
//Transfer-Encoding:分塊傳輸 就是將響應體分成多個塊進行傳輸(這個也就代表着肯定有響應體,關於詳細描述見下面頭部字段附錄)
if (contentLength(response) != -1
    || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
  return true;
}

return false;

}

(*) 我們首先大概描述下http各個響應碼

  1. 1XX: 100-199 的狀態碼代表着信息性狀態碼
  2. 2xx: 200-299 的狀態碼代表着成功狀態碼,但是204狀態碼代表着沒有實體僅僅有響應行和響應頭
  3. 3xx: 300-399 的狀態碼代表着重定向 但是304狀態碼代表着請求資源未修改,請使用緩存,只有響應行和響應頭沒有響應體
  4. 4xx: 400-499 的狀態碼代表着客戶端錯誤代碼
  5. 5xx: 500-599 的狀態碼代表着服務器錯誤代碼

這樣我們也就可以理解上面的判斷了, 響應嗎<100或者響應碼>=200,但是響應碼!=204而且響應碼!=304,
首先 http的響應碼是沒有100以下 關於小於100的判斷是什麼作用 暫時沒有搞懂 不過這個不影響

我們看完了根據響應碼判斷是否有響應體的判斷,我們接下來看是否符合緩存要求的判斷CacheStrategy.isCacheable(response, networkRequest),這個算是我們該小段的重點內容了,

如下爲isCacheable()方法:判斷是否符合緩存要求

	public static boolean isCacheable(Response response, Request request) {
// Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
switch (response.code()) {
  case HTTP_OK://200 成功返回狀態碼
  case HTTP_NOT_AUTHORITATIVE://203 實體首部包含的信息來至與資源源服務器的副本而不是來至與源服務器
  case HTTP_NO_CONTENT://204 只有首部沒有實體,但是還記得根據響應碼判斷是否有響應體的判斷嗎 那時已經排除了204 和304的狀態碼
  case HTTP_MULT_CHOICE://300
  case HTTP_MOVED_PERM://301
  case HTTP_NOT_FOUND://404 找不到資源
  case HTTP_BAD_METHOD://405 請求方法不支持
  case HTTP_GONE://410 和404類似 只是服務器以前擁有該資源但是刪除了
  case HTTP_REQ_TOO_LONG://414 客戶端發出的url長度太長
  case HTTP_NOT_IMPLEMENTED://501 服務器發生一個錯誤 無法提供服務
  case StatusLine.HTTP_PERM_REDIRECT://308
    // These codes can be cached unless headers forbid it.
	//以上的狀態碼都可以被緩存除非 首部不允許
    break;

  case HTTP_MOVED_TEMP://302 請求的url已被移除,
  case StatusLine.HTTP_TEMP_REDIRECT://307 和302類似
    // These codes can only be cached with the right response headers.
    // http://tools.ietf.org/html/rfc7234#section-3
    // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
	//這些響應碼也可以被緩存但是對應的頭部 包含以下條件 需要包含 Expires(資源過期字段) 字段 且不爲空
	//或者cacheControl的maxAge
    if (response.header("Expires") != null
        || response.cacheControl().maxAgeSeconds() != -1
        || response.cacheControl().isPublic()
        || response.cacheControl().isPrivate()) {
      break;
    }
    // Fall-through.
//注意此處 默認返回false 也就是說只有符合要求的狀態碼纔可以被緩存 ,這個也就解決了上面我們提出的那個問題 
  default:
    // All other codes cannot be cached.
    return false;
}

// A 'no-store' directive on request or response prevents the response from being cached.
return !response.cacheControl().noStore() && !request.cacheControl().noStore();
 }

爲了更好的理解上面的代碼 我們先說下上面用到頭部字段
下圖是我用Fiddler隨便抓取的一個網絡請求包,我們看紅色框中的幾個字段
這裏寫圖片描述
Expires 指的是請求資源的過期時間源於http1.0
Cache-Control :這個是目前在做緩存時最重要的一個字段,用來指定緩存規則,他有很多的值,不同的值代表着不同的意義,而且這個值是可以組合的 如下圖
這裏寫圖片描述

說下 Cache-Control各個字段的不同含義(首部字段不區分大小寫):

  1. no-cache :在使用該請求緩存時必須要先到服務器驗證 寫法舉例 Cache-Control:no-cache
  2. no-store :不緩存該請求,如果緩存了必須刪除緩存 寫法舉例 Cache-Control:no-store
  3. max-age :該資源有效期的最大時間 寫法舉例 Cache-Control:max-age=3600
  4. s-maxage :和max-age 類似 不過該字段是用於共享(公共)緩存 寫法舉例 Cache-Control:s-maxage=3600
  5. private :表明響應只可以被單個用戶緩存 不能被代理緩存 寫法舉例 Cache-Control:private
  6. public :表明響應可以被任何用戶緩存 是共享(例如 客戶端 代理服務器等等) 寫法舉例 Cache-Control:public
  7. must-revalidate:緩存必須在使用之前驗證舊資源的狀態,並且不可使用過期資源。 寫法舉例 Cache-Control:must-revalidate
  8. max-stale:表明緩存在資源過期後的max-stale指定的時間內還可以繼續使用,但是超過這個時間就必須請求服務器 寫法舉例 Cache-Control:max-stale(代表着資源永不過期) Cache-Control:max-stale=3600(表明在緩存過期後的3600秒內還可以繼續用)
  9. min-fresh:最小要保留的新鮮度 ,即是假如緩存設置的最大新鮮時間爲max-age=500 最小新鮮度爲min-fresh=300 則該緩存的真正的新鮮時間 是max-age-min-fresh=200 也就是說緩存在200秒內有效 超過200秒就必須要請求服務器驗證
  10. only-if-cached:如果緩存存在就使用緩存 無論服務器是否更新(無論緩存是否過期) 寫法舉例 Cache-Control:only-if-cached
  11. no-transform:不得對資源進行轉換或轉變。Content-Encoding, Content-Range, Content-Type等HTTP頭不能由代理修改。例如,非透明代理可以對圖像格式進行轉換,以便節省緩存空間或者減少緩慢鏈路上的流量。 no-transform指令不允許這樣做。 寫法舉例 Cache-Control:no-transform
  12. immutable:表示響應正文不會隨時間而改變。資源(如果未過期)在服務器上不發生改變,因此客戶端不應發送重新驗證請求頭(例如If-None-Match或If-Modified-Since)來檢查更新,即使用戶顯式地刷新頁面 寫法舉例 Cache-Control:immutable

好了 看完http對Cache-Control 字段的說明 我們再來看下面的代碼(CacheInterceptor類中intercept方法)應該大概可以猜測到時是什麼意思了,先說下 這裏的response是服務器返回的響應,其獲取的cacheControl也是服務器響應的頭部
這裏不對內部代碼進行查看了 要不然文章太冗雜了

	//判斷是否有首部Expires
 if (response.header("Expires") != null
		//判斷 Cache-Control是否含有max-age字段的值 如果沒有則爲-1
        || response.cacheControl().maxAgeSeconds() != -1
		//判斷 Cache-Control是否含有public 默認爲false
        || response.cacheControl().isPublic()
		//判斷 Cache-Control是否含有private 默認爲false
        || response.cacheControl().isPrivate()) {
      break;
    }

所以如果響應碼是302或者是307的時候 必須含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段纔可以繼續往下走

return !response.cacheControl().noStore() && !request.cacheControl().noStore();

這行代碼是指如果請求頭 或者響應頭的Cache-Control 含有no-store 字段則肯定不緩存

走到這裏我們看到 okhttp緩存的條件是

1,首先請求頭或者響應頭的Cache-Control 不能含有no-store(一旦含有必定不緩存)

2,在滿足條件1的情況下

(1)如果響應碼是是302或307時 必須含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段

(2)響應碼爲200 203 300 301 308 404 405 410 414 501 時緩存

但是我們繼續看下面代碼(CacheInterceptor類中intercept方法)

//如果請求方法不是支持的緩存方法則刪除緩存
if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }

那我們繼續看HttpMethod.invalidatesCache(networkRequest.method())方法,如下

 public static boolean invalidatesCache(String method) {
return method.equals("POST")
    || method.equals("PATCH")
    || method.equals("PUT")
    || method.equals("DELETE")
    || method.equals("MOVE");     // WebDAV

}

也就是說如果請求方法是 post put delete move patch 則刪除請求,我們應該還能想起 前面在判斷是否有響應體的時候有這麼一行代碼

//Head方法 在服務器的響應中只返回響應頭,不會返回響應體
if (response.request().method().equals("HEAD")) {
  return false;
}

也就是說緩存也是不支持head方法的

綜上 我們可以得到okhttp支持存儲的緩存 只能是get方法請求的響應,好了 我們記住這一條結論 後面我們還能得到這一印證(後面我們也會得到爲什麼緩存只支持get方法的原因)

在這裏我們基本上已經完成了okhhtp緩存條件的分析 不過還有一些小細節我們可能沒有分析到 我們繼續看 緩存存儲的方法put(也就是我們前面設置的緩存策略的put方法),這裏的cache是個接口 但是它的真正實現是Cache類
所以我們直接看Cache類的 put方法就可以了

 CacheRequest cacheRequest = cache.put(response);

Cache的put方法

@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
//這裏再次對請求方法進行檢驗
if (HttpMethod.invalidatesCache(response.request().method())) {
  try {
    remove(response.request());
  } catch (IOException ignored) {
    // The cache cannot be written.
  }
  return null;
}
//再次驗證如果不是get方法則返回,這裏也解釋了 爲什麼只緩存get方法的請求而不緩存其他方法的請求響應,
//這裏的解釋是 技術上可以做到 但是花費的精力太大 而且效率太低 所以再次不做其他方法的緩存
if (!requestMethod.equals("GET")) {
  // Don't cache non-GET responses. We're technically allowed to cache
  // HEAD requests and some POST requests, but the complexity of doing
  // so is high and the benefit is low.
  return null;
}
//此處是檢查 響應頭部是否含有 * 如果含有* 也不緩存 這裏我就不繼續分析了 大家可以自己看看 
if (HttpHeaders.hasVaryAll(response)) {
  return null;
}

Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
  editor = cache.edit(key(response.request().url()));
  if (editor == null) {
    return null;
  }
  entry.writeTo(editor);
  return new CacheRequestImpl(editor);
} catch (IOException e) {
  abortQuietly(editor);
  return null;
}

}


好了 這裏我們徹底的完成了 我們這一小節的主題 okhttp在什麼情況下緩存網絡響應 下面我們總結下我們得到的結論:

1、Okhhtp只緩存Get請求

2、如果請求頭或者響應頭的Cache-Control 含有no-store 則一定不緩存

3、如果響應頭含有*字符則不緩存

4、在滿足 1、2、3 的條件下 okhttp對以下的條件進行緩存

(1)響應碼是200 203 300 301 308 404 405 410 414 501 時緩存

(2)如果響應碼是是302或307時 必須含有首部Expires 或者首部Cache-Control 有max-age public或者是private字段


好了上面我們分析完"okhttp在什麼情況下緩存網絡響應" 下面我們分析"okhhtp在什麼情況下使用緩存而不是使用網絡請求"

okhhtp在什麼情況下使用緩存而不是使用網絡請求

我們還是要研究緩存攔截器,研究的前提自然是我們的請求符合okhhtp存儲緩存的要求 而且已經緩存成功了

好了我們先粘出緩存攔截器intercept方法全部內容

 @Override public Response intercept(Chain chain) throws IOException {
//前面我們已經知道了 這個cache就是我們設置的緩存策略
//這裏是利用緩存策略根據請求獲取緩存的響應(我們的前提是get請求並且已經成功緩存了,所以我們這裏成功的獲取了緩存響應)
//(1)
Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;

long now = System.currentTimeMillis();

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

if (cache != null) {
  cache.trackResponse(strategy);
}

if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
  return new Response.Builder()
      .request(chain.request())
      .protocol(Protocol.HTTP_1_1)
      .code(504)
      .message("Unsatisfiable Request (only-if-cached)")
      .body(Util.EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
}

// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

Response networkResponse = null;
try {
  networkResponse = chain.proceed(networkRequest);
} finally {
  // If we're crashing on I/O or otherwise, don't leak the cache body.
  if (networkResponse == null && cacheCandidate != null) {
    closeQuietly(cacheCandidate.body());
  }
}

// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
  if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    Response response = cacheResponse.newBuilder()
        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
        .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
        .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    networkResponse.body().close();

    // Update the cache after combining headers but before stripping the
    // Content-Encoding header (as performed by initContentStream()).
    cache.trackConditionalCacheHit();
    cache.update(cacheResponse, response);
    return response;
  } else {
    closeQuietly(cacheResponse.body());
  }
}
//下面代碼我們已經分析過了
Response response = networkResponse.newBuilder()
    .cacheResponse(stripBody(cacheResponse))
    .networkResponse(stripBody(networkResponse))
    .build();

if (cache != null) {
  if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
    // Offer this request to the cache.
    CacheRequest cacheRequest = cache.put(response);
    return cacheWritingResponse(cacheRequest, response);
  }

  if (HttpMethod.invalidatesCache(networkRequest.method())) {
    try {
      cache.remove(networkRequest);
    } catch (IOException ignored) {
      // The cache cannot be written.
    }
  }
}

return response;
  }

我們先根據結果然後推論原因,這樣可能更好的理解

首先我們正確獲取了緩存的網絡響應cacheCandidate 代碼位置爲(1)

然後初始化得到一個CacheStrategy類 看名字緩存策略大概可以猜到這個是根據傳入的請求和緩存響應 判斷到底是使用緩存還是進行網絡請求(其實我們之前研究存儲緩存的時候就已經看過了,這裏我們詳細研究下)
這裏我們詳細看下 看下這個類CacheStrategy內部以及這個兩個返回值(注:這兩個返回值是否爲空是緩存策略判斷的結果,爲空則不支持 不爲空則支持)

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

我們先看下 new CacheStrategy.Factory(now, chain.request(), cacheCandidate);也就是CacheStrategy的內部類Factory的構造方法

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
//這裏將傳入的網絡請求和緩存響應設置爲Factory內部成員
  this.request = request;
  this.cacheResponse = cacheResponse;
	//cacheResponse不爲空 所以執行
  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
	//這裏主要是解析緩存響應頭部 並將解析到的字段賦值給Factory的成員變量 這裏主要是獲取和緩存相關的字段
    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);
      }
    }
  }
}

上面的代碼主要是解析緩存頭部字段 並存儲在Factory類的成員變量中,下面解釋下上面需要解析的和緩存相關的頭部字段(首部 不區分大小寫)

  1. Age 告訴接受端 響應已經產生了多長時間(這個是和max-age一起實現緩存的),單位是秒
  2. Date 報文創建的時間和日期(響應和報文產生的時間是不一樣的概念 時間也不一定相等)
  3. ETag 報文中包含的實體(響應體)的標誌 實體標識是標誌資源的一種方式,用老確定資源是否有修改
  4. Last-Modified 實體最後一次唄修改的時間 舉例說明 : Last-Modified : Fri , 12 May 2006 18:53:33 GMT

我們會繼續看Factory的get方法

//獲取CacheStrategy實例
public CacheStrategy get() {
//獲取CacheStrategy實例
  CacheStrategy candidate = getCandidate();

  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }

  return candidate;
}

我們繼續看getCandidate(),這個方法實現了基本上的緩存策略
—注:爲了大家更好的理解下面的緩存策略,我先說下okhttp緩存判斷依據結論 後面我們再做驗證

這裏是CacheStrategy的構造方法
CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
 }

重要這裏說下緩存判斷依據

1如果networkRequest爲null cacheResponse不爲null 則使用緩存

2如果networkRequest不爲null cacheResponse爲null 則進行網絡請求

3其他情況我們下面再解釋 我們先記住這2個結論 這個能更好幫助我們理解下面的緩存策略判斷 大家可以先看這一小節的結論好 請求頭的註釋以及說明 然後再看下面的緩存策略 這樣可能更好理解些(當然如果對http的Cache
比較瞭解的話,則不必了)

 private CacheStrategy getCandidate() {
  // No cached response.
	//如果緩存爲空 則初始化一個CacheStrategy返回 
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }
	//如果請求方式是https而且緩存的沒有TLS握手(注:這個我沒有深入的研究 如果大家有結論可以告訴我)
  // Drop the cached response if it's missing a required handshake.
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // If this response shouldn't have been stored, it should never be used
  // as a response source. This check should be redundant as long as the
  // persistence store is well-behaved and the rules are constant.
	//此時再次判讀是否應該緩存 如果不應該緩存則將cacheResponse設爲null
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
	//獲取請求的CacheControl字段的信息類
  CacheControl requestCaching = request.cacheControl();
	//如果請求含有nocache字段並且如果請求頭部含有If-Modified-Since或者If-None-Match字段(下面會介紹這些條件緩存字段的含義) 則將cacheResponse設爲null
	//hasConditions(request)的方法詳情如下:
	//private static boolean hasConditions(Request request) {
 	// return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;
	//}
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
//獲取緩存響應的頭部Cache-Control字段的信息
  CacheControl responseCaching = cacheResponse.cacheControl();
//Cache-Control字段是否含有immutable  前面我們已經說過immutable 是表明 響應不會隨着時間而變化  這裏將netrequest設置爲null 並return(意思是使用緩存)
  if (responseCaching.immutable()) {
    return new CacheStrategy(null, cacheResponse);
  }
//獲取緩存裏面響應的age 注意該age不是響應的真實age
  long ageMillis = cacheResponseAge();
//獲取緩存響應裏聲明的新鮮時間,如果沒有設置則爲0
  long freshMillis = computeFreshnessLifetime();
	//如果請求中的含有最大響應新鮮時間 則和緩存中的最大響應新鮮時間進行比較 取最小值並賦值給freshMillis
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }
//獲取最小新鮮時間(前面我們提到過這個概念)
  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }
//獲取緩存過期後還能使用的時間
  long maxStaleMillis = 0;
//判斷Cache-Control 是否含有must-revalidate字段 該字段我們前面說到了是 使用緩存前必須對資源進行驗證 不可使用過期時間,換句話說如果設置該字段了 那過期後還能使用時間就沒有意義了
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }
//如果緩存頭部Cache-Control 不含有nocache,並且 (緩存響應年齡+最小新鮮時間)<(響應新鮮時間+響應過期後還可用的時間) 則使用緩存,不過根據響應時間還要加上一些警告 Warning
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
	//如果(緩存響應年齡+最小新鮮時間)<響應新鮮時間 則加上警告頭部 響應已經過期
    if (ageMillis + minFreshMillis >= freshMillis) {
      builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
	//獲取一天的時間(毫秒)
    long oneDayMillis = 24 * 60 * 60 * 1000L;
	//如果響應年齡大於一天 並且響應使用啓發式過期時間
	//我們看下isFreshnessLifetimeHeuristic()就知道什麼是啓發式過期了
	//private boolean isFreshnessLifetimeHeuristic() {
  	//return cacheResponse.cacheControl().maxAgeSeconds() == -1 && expires == null;
	//}
	//好吧 啓發式過期 就是沒有設置獲取過期時間字段
	//如果是這樣的 則需要添加警告頭部字段 這是試探性過期(不過如果使用緩存的話 不建議這麼做)
	//關於試探性過期 我們下面介紹 (稍安勿躁) 因爲篇幅較多 這些寫不下
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
      builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    return new CacheStrategy(null, builder.build());
  }

  // Find a condition to add to the request. If the condition is satisfied, the response body
  // will not be transmitted.
  //獲取請求的條件頭部 並根據條件來判斷是否使用緩存
  String conditionName;
  String conditionValue;
//判斷緩存響應的資源 實體標識是否爲空
  if (etag != null) {
    conditionName = "If-None-Match";
    conditionValue = etag;
	//緩存響應頭部 lastModified 最後一次修改時間
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
	//緩存響應頭部 servedDate Date字段的值 就是 原始服務器發出響應時的服務器時間
  } else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
  } else {
	//如果上述字段都沒有則發出原始的網絡請求 不使用緩存
    return new CacheStrategy(request, null); // No condition! Make a regular request.
  }
	//如果上述的條件之一滿足 則添加條件頭部 返回含有條件頭部的請求和緩存響應
	//複製一份和當前request的header
  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
	//在header中添加條件頭部, 注:Internal是個抽象方法 該方法的唯一實現是在okhhtpclient的內部實現 okhhtpclient本身沒有實現它
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
		//刪除原有的所有header 將現在的header添加上 注意上面已經說過了 添加的header 是複製原來的header 並在它的基礎上添加一個條件頭部
      .headers(conditionalRequestHeaders.build())
      .build();
//返回處理過的 請求和 緩存響應
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

okhhtp的緩存策略分析完了 但是大家可能對上面出現的頭部字段和條件字段 以及新鮮度什麼的比較迷糊,這裏我們先說下這些東西 也好大家更好的理解

好吧我們先說下新鮮度( 這裏說明:以下所說的服務器都是源服務器就是最初發出響應報文的服務器,代理服務器會特別說明是代理服務器)

http爲了實現緩存 提出了新鮮度的概念 那什麼是新鮮度呢?比如一個西紅柿 剛從地裏摘下拿回家放着,那它就是新鮮的,而且是最新鮮,那過了3天后 這個西紅柿壞掉了 那它就是不新鮮的了 不能用了,
從地裏摘掉到壞掉的這3天時間內 就是他的新鮮時間,這個在http中 土地相當於服務器,家裏存放相當於緩存,客戶端使用就相當於要吃掉這個西紅柿

好了新鮮度的大概意思我們瞭解了 我們在詳細的說說 http針對新鮮度設置的各個頭部
age :年齡 從響應產生的時間(單獨字段 不屬於Cache-Control)
max-age:最大年齡 響應可以產生的最大年齡 只看該字段的話 我們可以說 該響應的新鮮時間是max-age 也就是說只要 age<max-age 該響應就是新鮮的 是可以用的不用請求服務器客戶已直接使用緩存(屬於 Cache-Control的命令)
上面2個字段在計算新鮮度時具有最高優先級
expires :過期時間 這個字段是說明響應過期的時間點(單獨字段 不屬於Cache-Control)
這個字段的優先級其次
date :日期 這個是指服務器產生響應的時間
注:如果上面3個字段都沒有設置 可以用這個字段計算新鮮時間 這個時間也叫做試探性過期時間 計算方式 採用LM_Factor算法計算
方式如下 time_since_modify = max(0,date-last-modified); 
		freshness_time = (int)(time_since_modify*lm_factor);在okhttp中這個lm_factor值是10%,就是0.1;
詳細解釋下:time_since_modify =  max(0,date - last-modified) 就是將服務器響應時間(date) 減去 服務器最後一次修改資源的時間(last-modified) 得到一個時間差 用這個時間差和0比較取最大值得到time_since_modify,
freshness_time = (int)(time_since_modify*lm_factor); 前面我們已經得到time_since_modify 這裏我們取其中一小段時間作爲過期時間,lm_factor就是這一小段的比例,在okhttp中比例是10%
注:date時間比修改使勁越大說明資源修改越不頻繁(新鮮度越大) ,

在前面分析緩存策略的時候有這麼一行代碼 我當時的解釋是獲取新鮮度 但是並沒有詳細的分析這個方法,這裏我們進去看看 這個計算新鮮度的算法是否和我們上面分析的一致
long freshMillis = computeFreshnessLifetime();

computeFreshnessLifetime()方法如下:
	
 private long computeFreshnessLifetime() {
  CacheControl responseCaching = cacheResponse.cacheControl();
	//這個是我們前面說的 max-age是優先級最高的計算新鮮時間的方式 和我們說的一致
  if (responseCaching.maxAgeSeconds() != -1) {
    return SECONDS.toMillis(responseCaching.maxAgeSeconds());
	//當沒有max-age字段時 使用expires - date 獲取新鮮度時間 和我們說的也一致
  } else if (expires != null) {
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : receivedResponseMillis;
    long delta = expires.getTime() - servedMillis;
    return delta > 0 ? delta : 0;
	//當上述2個字段都不存在時 進行試探性過期計算 還是和我們說的一致
  } else if (lastModified != null
      && cacheResponse.request().url().query() == null) {
    // As recommended by the HTTP RFC and implemented in Firefox, the
    // max age of a document should be defaulted to 10% of the
    // document's age at the time it was served. Default expiration
    // dates aren't used for URIs containing a query.
    long servedMillis = servedDate != null
        ? servedDate.getTime()
        : sentRequestMillis;
    long delta = servedMillis - lastModified.getTime();
    return delta > 0 ? (delta / 10) : 0;
  }
	//當上述方式3種方式都不可取時返回新鮮時間0 也就是說不能獲取緩存 一定要進行網絡請求
  return 0;
}

好了經過上面的討論 我們知道了新鮮度 以及如何計算一個響應的新鮮時間,接下來緩存真正的可用的時間 上面我們看到了Cache-control的這兩個字段

min-fresh:最小要保留的新鮮度 ,即是假如緩存設置的最大新鮮時間爲max-age=500 最小新鮮度爲min-fresh=300 則該緩存的真正的新鮮時間 是max-age-min-fresh=200 也就是說緩存在200秒內有效 超過200秒就必須要請求服務器驗證

max-stale:緩存過期後還可以用的時間

也就是緩存真正的可用的新鮮時間是 realFreshTime =(freshTime+max-stale) - (min-fresh);

現在我們應該對上面分析的緩存策略更明白點了,下面我們分析上面我們說過的 緩存的命令頭部

  1. If-Modify-since:date , 條件判斷 如果在date時間之後服務器沒有修改該資源 就返回304 並且在頭部添加新的資源過期時間,如果修改了則返回200並且正確返回正確的請求資源以及必要響應頭部,注意這個date我們一般設置爲服務器上次修改資源的時間即是:Last-Modified,因爲有些服務器在處理這個條件緩存判斷是是將該時間當做字符串和上次修改的時間進行比對,如果相同則返回304否則返回200,不過這也違反了該條件字段設計的初衷,
  2. If-None-Match:ETag ,條件判斷 ,我們前面解釋了什麼是ETag,即實體標識,如果客戶端發出的請求帶有這個條件判斷,服務器會根據服務器當前的ETag和客戶端給出的ETag進行比對如果相同,則返回304即是服務器資源沒變,繼續使用緩存,如果不相同,即是服務器資源發生改變,這是服務器返回200,以及新的ETag實體標識,還有請求的資源(就是當做正常請求一樣),

我們現在將okhttp涉及到的http知識都說完了 那麼我們繼續上面的分析

上面我們分析完 private CacheStrategy getCandidate() {} 我之前說過這個是okhttp緩存策略的真正體現 那麼我們對getCandidate()方法從頭到尾的總結一遍

  1. 首先根據緩存的響應碼判斷是否可以緩存 不可以返回 cacheResponse爲null
  2. 上述條件都不滿足 判斷請求頭部是否含有no-cache以及頭部是否含有條件頭部If-Modify-since或者If-None-Match,三個條件滿足一個 cacheResponse爲null 返回
  3. 上述條件都不滿足 判斷cache-control 是否有immutable 有的話 使用緩存 networkRequest爲null 返回
  4. 上述條件都不滿足 計算age 最小新鮮時間minFresh 新鮮時間Fresh 過期可使用時間stale, 如果age+minfresh < fresh+stale 說明當前響應滿足新鮮度 networkRequest爲null 返回
  5. 判斷緩存存儲的響應是否含有響應頭ETag,date,Last-Modified其中的一個或多個字段如果沒有則返回 cacheResponse爲null 返回 ,如果有,則先判斷ETag是否存在如果存在 則在networkRequest請求頭部添加If-None-Match:ETag,返回(cacheResponse,networkRequest)都不爲空,如果ETag不存在 則date,Last-Modified(Last-Modified優先級大於date)是否存在,存在則在networkRequest請求頭部添加If-Modify-since:date並返回

上面就是getCandidate()的全部流程,我們繼續分析public CacheStrategy get() {}方法

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
	//就是如果緩存策略要進行網絡請求,但是請求中又設置要使用緩存則將cacheResponse,networkRequest全部爲null,好了這個判斷我們可以在上面分析getCandidate()方法中加上一條
	//6 ,就是如果緩存策略要進行網絡請求,但是請求中又設置要使用緩存onlyIfCache則將cacheResponse,networkRequest全部爲nul
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }

  return candidate;
}

到此我們算是徹底的完成了CacheStrategy的策略分析 那我們繼續回到okhttp的網絡緩存攔截器CacheInterceptor分析我們沒有分析完的代碼

 @Override public Response intercept(Chain chain) throws IOException {
   ....
//我們上面已經分析完CacheStrategy的初始化方法 也知道了networkRequest,cacheResponse爲空的條件和含義
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//這個是設置追蹤和我們要分析的無關
if (cache != null) {
  cache.trackResponse(strategy);
}
//如果我們設置了緩存策略 但是卻使用緩存則關閉緩存
if (cacheCandidate != null && cacheResponse == null) {
  closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// If we're forbidden from using the network and the cache is insufficient, fail.
//此處就是我們上面總結的第六條,如果我們想要進行網絡請求但是卻在cache-control設置了 only-if-cache(如果有緩存就使用緩存),則返回一個空的響應體並且響應碼是504
if (networkRequest == null && cacheResponse == null) {
  return new Response.Builder()
      .request(chain.request())
      .protocol(Protocol.HTTP_1_1)
      .code(504)
      .message("Unsatisfiable Request (only-if-cached)")
      .body(Util.EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(System.currentTimeMillis())
      .build();
}
//如果networkRequest爲null 我們就將緩存的響應返回,也就是我們使用了緩存,到此我們的分析算是大部分結束了,
// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

Response networkResponse = null;
try {
	//進行網絡請求獲取網絡響應 但是需要注意的是這個networkRequest是經過okhhtp改變過的 就是可能加上了條件緩存頭字段
	//所以這個請求可能是304 就是服務器資源沒有改變,沒有返回響應體
  networkResponse = chain.proceed(networkRequest);
} finally {
  // If we're crashing on I/O or otherwise, don't leak the cache body.
  if (networkResponse == null && cacheCandidate != null) {
    closeQuietly(cacheCandidate.body());
  }
}

// If we have a cache response too, then we're doing a conditional get.
//如果cacheResponse不爲null
if (cacheResponse != null) {
//如果網絡響應碼是304的話 則返回緩存響應並更新緩存(如果緩存頭部和請求得到的頭部不一致則使用請求得到的頭部)
  if (networkResponse.code() == HTTP_NOT_MODIFIED) {
    Response response = cacheResponse.newBuilder()
        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
        .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
        .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    networkResponse.body().close();

    // Update the cache after combining headers but before stripping the
    // Content-Encoding header (as performed by initContentStream()).
    cache.trackConditionalCacheHit();
	//更新緩存
    cache.update(cacheResponse, response);
    return response;
  } else {
    closeQuietly(cacheResponse.body());
  }
}
......
}	

到此我們這一小節的分析徹底的結束了,下面我們總結下整個流程


++++++++++++++++++++++這個很重要+++++++++++++++++++++++++++++++++
我們總結下okhhtp在什麼請求下使用緩存

  1. 首先我們要設置緩存策略Cache,(否則肯定不會緩存響應,當然也不會使用緩存響應,也不會設置緩存條件頭部, 關於這個緩存策略我們也可以定義自己實現InternalCache接口 然後在okhttpclient.build中設置,注意自定義的緩存策略和Cache只能有一個,) 如果沒有設置返回 ->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  2. 判斷緩存中是否含有該網絡請求的緩存如果沒有返回->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  3. 判斷請求方法是否支持,如果不支持返回->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  4. 判斷請求頭cache-control是否含有nocache,如果有返回->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  5. 判斷請求頭是否含有條件判斷字段If-Modified-Since或者是If-None-Match,如果有返回->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  6. 判斷請求頭cache-control是否含有immutable字段 如果有則返回緩存
  7. 判斷緩存新鮮度是否滿足 滿足返回緩存
  8. 判斷請求頭部是否含有Date,ETag,Last-Modified 其中一個或者多個字段 沒有則返回->如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求
  9. 根據ETag>Last-Modified>Date的優先級生成 If-None-Match:ETag>If-Modified-Since:Last-Modified>If-Modified-Since:Date的條件驗證字段,但是隻能生成一個 返回 >如果cachecontrol 設置了only-if-cached則返回504 沒有設置則進行網絡請求,
  10. 含有條件首部的網絡請求返回304的時候返回緩存,並更新緩存

注:
1.上述判斷的執行順序是從1到10只有上面的條件滿足纔可以執行下面的邏輯

2.上述的3中請求方法只支持get和head方法

3.詳細上述的7中的情況我在上面已經解釋了 不明白可以往上翻翻

okhttp整個緩存流程圖如下:

這裏寫圖片描述

++++++++++++++++++++++++++


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章