- 1.OkHttp源碼解析(一):OKHttp初階
- 2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HTTP的那些事
- 3 OkHttp源碼解析(三):OKHttp中階之線程池和消息隊列
- 4 OkHttp源碼解析(四):OKHttp中階之攔截器及調(diào)用鏈
- 5 OkHttp源碼解析(五):OKHttp中階之OKio簡介
- 6 OkHttp源碼解析(六):OKHttp中階之緩存基礎
- 7 OkHttp源碼解析(七):OKHttp中階之緩存機制
- 8 OkHttp源碼解析(八):OKHttp中階之連接與請求值前奏
- 9 OkHttp源碼解析(九):OKHTTP連接中三個"核心"RealConnection、ConnectionPool偏化、StreamAllocation
- 10 OkHttp源碼解析(十) OKHTTP中連接與請求
- 11 OkHttp的感謝
前面一節(jié)課主要講解了interceptor及其調(diào)用鏈养匈,本篇這主要講解http的緩存處理,大體流程如下:
1.什么是緩存
2.為什么要用緩存
3.HTTP緩存機制
4.CacheControl類詳解
5.CacheStrategy類詳解
6.CacheInterceptor類詳解
一、Cache緩存的簡介
緩存缅糟,顧名思義魔吐,也就是方便用戶快速獲取值的一種存儲方式综慎。小到CPU同頻的昂貴的緩存顆粒,內(nèi)緩存,硬盤塘秦,網(wǎng)絡,CDN反緩存叶组,DNS遞歸查詢侣监,OS頁面置換,Redis數(shù)據(jù)庫卿堂,都可以看作緩存。它有如下的特點:
- 1.緩存載體與持久載體總是相對的怀各,體量遠遠小于持久載體份名,成本高于之久體量辰如,但是速度卻高速持久體量桑滩。
- 2.需要實現(xiàn)排序依據(jù),比如在java中可以使用Comparable<T>作為排序的接口
- 3.需要一種頁面置換算法(page replacement algorithm)將舊頁面去掉換成新頁面听哭,如最久未使用算法(LRU)慢洋、先進先出算法(FIFO)塘雳、最緊最小使用算法(LFU)陆盘、非最緊使用算法(NMRU)等
- 4.可溯源普筹,如果沒有命中緩存,就需要從原始地址獲取隘马,這個步驟叫做"回源頭"太防,CDN廠商會標注"回源率"作為賣點
PS:在OKHTTP中,使用FileSystem作為緩存載體(磁盤相對于網(wǎng)絡緩存)酸员,使用LRU作為頁面置換算法(封裝了LinkedHashMap)蜒车。
HTTP作為客戶端與服務器溝通的重要協(xié)議,對從事android開發(fā)的同學來說是一個非常重要的環(huán)節(jié)幔嗦,其中網(wǎng)絡層優(yōu)化又是重中之重酿愧。今天主要是講解OKHTTP中的緩存處理,那么首先先簡單介紹下為什么要用緩存
二邀泉、為什么要用緩存
緩存對移動端非常重要嬉挡,使用緩存可以提高用戶體驗,用緩存的主要在于:
- 1 減少請求次數(shù)汇恤,較少服務器壓力
- 2本地數(shù)據(jù)讀取更快庞钢,讓頁面不會空白幾百毫秒
-
3在無網(wǎng)絡的情況下提供數(shù)據(jù)
HTTP緩存是最好的減少客戶端服務器往返次數(shù)的方案,緩存提供了一種機制來保證客戶端或者代理能夠存儲一些東西因谎,而這些東西將會在稍后的HTTP響應中用到的基括。(即第一次請求了,到了客戶端财岔,緩存起來风皿,下次如果請求還需要一些資源,就不用到服務器去取了)這樣匠璧,就不用讓一些資源再次跨越整個網(wǎng)絡了桐款。
請求與緩存.png
三、HTTP緩存機制
1患朱、HTTP報文
HTTP報文就是客戶端和服務器之間通信時發(fā)送及其響應的數(shù)據(jù)塊鲁僚。客戶端向服務器請求數(shù)據(jù)裁厅,發(fā)送請求(request)報文冰沙;服務器向客戶端下發(fā)返回數(shù)據(jù),返回響應(response)報文执虹,報文信息主要分為兩部分拓挥。
- 1包含屬性的頭部(header)-------------附加信息(cookie,緩存信息等)袋励,與緩存相關的規(guī)則信息侥啤,均包含在header中
- 2包含數(shù)據(jù)的主體部分(body)--------------HTTP請求真正想要傳輸?shù)牟糠?/li>
2当叭、緩存分類
(1)按照"端“”分類
緩存可以分為
- 1、服務器緩存盖灸,其中服務器緩存又可以分為服務器緩存和反向代理服務器緩存(也叫網(wǎng)關緩存蚁鳖,比如Nginx反向代理,Squid等)赁炎,其實廣泛使用的CSN也是一種服務端緩存醉箕,目的都是讓用戶的請求走"捷徑",并且都是緩存圖片徙垫、文件等靜態(tài)資源讥裤。
- 2、客戶端緩存
客戶端緩存則一般是只瀏覽器緩存姻报,目的就是加速各種靜態(tài)資源的訪問己英,想想淘寶,京東吴旋,百度隨便一個網(wǎng)頁都是上百請求损肛,每天PV都是上億的,如果沒有緩存邮府,用戶體驗會急劇下降荧关,同時服務器壓力巨大。
(2) 按照"是否想服務器發(fā)起請求褂傀,進行對比"分類
可以分為:
- 1 強制緩存(不對比緩存)
- 2 對比緩存
已存在緩存數(shù)據(jù)時,僅基于強制緩存忍啤,請求數(shù)據(jù)流程如下:
已存在緩存數(shù)據(jù)時,僅基于對比緩存仙辟,請求數(shù)據(jù)的流程如下:
我們可以看到兩類緩存規(guī)則的不同同波,強制緩存如果生效,則不再和服務器交互了叠国,而對比緩存不慣是否生效未檩,都需要和服務器發(fā)生交互。
通過上面了解到粟焊,在緩存數(shù)據(jù)未失效的情況下冤狡,可以直接使用緩存數(shù)據(jù),那么客戶端是怎么判斷數(shù)據(jù)是否失效的项棠?同理悲雳,什么時候采用強制緩存,而什么時候又采用對比緩存香追,這里面客戶端是怎么和服務器進行交互的合瓢?上面也說道,緩存規(guī)則是包含在響應header里面的透典。莫非所有的交互在header里面晴楔?
3顿苇、請求頭header中有關緩存的設置
3.1 expires
在HTTP/1.0中expires的值圍服務器端返回的到期時間,即下一次請求時税弃,請求時間小于服務器返回的到期時間纪岁,直接使用緩存數(shù)據(jù),這里面有個問題钙皮,由于到期時間是服務器生成的蜂科,但是客戶端的時間可能和服務器有誤差顽决,所以這就會導致誤差短条,所以到了HTTP1.1基本上不適用expires了,使用Cache-Control替代了expires
3.2 Cache-Control
Cache-Control 是最重要的規(guī)則才菠。常見的取值有private茸时、public
、no-cache赋访、max-age可都、no-store、默認是private蚓耽。
響應頭部 意義
Cache-Control:public 響應被公有緩存渠牲,移動端無用
響應頭部 | 意義 |
---|---|
Cache-Control:public | 響應被共有緩存,移動端無用 |
Cache-Control:private | 響應被私有緩存步悠,移動端無用 |
Cache-Control:no-cache | 不緩存 |
Cache-Control:no-store | 不緩存 |
Cache-Control:max-age=60 | 60秒之后緩存過期 |
(PS:在瀏覽器里面签杈,private 表示客戶端可以緩存,public表示客戶端和服務器都可以緩存)
舉個例子鼎兽。入下圖:
圖中Cache-Control僅指定了max-age所以默認是private答姥。緩存時間是31536000,也就是說365內(nèi)的再次請求這條數(shù)據(jù)谚咬,都會直接獲取緩存數(shù)據(jù)庫中的數(shù)據(jù)鹦付,直接使用。
3.3 Last-Modified/If-Modified-Since
上面提到了對比緩存择卦,顧名思義敲长,需要進行比較判斷是否可以使用緩存,客戶端第一次發(fā)起請求時秉继,服務器會將緩存標志和數(shù)據(jù)一起返回給客戶端祈噪,客戶端當二者緩存至緩存數(shù)據(jù)庫中。再次其你去數(shù)據(jù)時秕噪,客戶端將備份的緩存標志發(fā)送給服務器钳降,服務器根據(jù)標志來進行判斷腌巾,判斷成功后铲觉,返回304狀態(tài)碼,通知客戶端比較成功撵幽,可以使用緩存數(shù)據(jù)礁击。
上面說到了對比緩存的流程,那么具體又是怎么實現(xiàn)的那哆窿?
Last-Modified
是通過Last-Modified/If-Modified-Since來實現(xiàn)的,服務器在響應請求時挚躯,告訴瀏覽器資源的最后修改時間强衡。
If-Modified-Since
再次請求服務器時,通過此字段通知服務器上次請求時码荔,服務器返回最遠的最后修改時間漩勤。服務器收到請求后發(fā)現(xiàn)有If-Modified-Since則與被請求資源的最后修改時間進行對比。若資源的最后修改時間大于If-Modified-Since缩搅,說明資源又被改動過越败,則響應整個內(nèi)容,返回狀態(tài)碼是200.如果資源的最后修改時間小于或者等于If-Modified-Since硼瓣,說明資源沒有修改究飞,則響應狀態(tài)碼為304,告訴客戶端繼續(xù)使用cache.
3.4 ETag/If-None-Match(優(yōu)先級高于Last-Modified/If-Modified-Since)
Etag:
服務響應請求時巨双,告訴客戶端當前資源在服務器的唯一標識(生成規(guī)則由服務器決定)
If-None-Match:
再次請求服務器時噪猾,通過此字段通知服務器客戶端緩存數(shù)據(jù)的唯一標識。服務器收到請求后發(fā)現(xiàn)有頭部If-None-Match則與被請求的資源的唯一標識進行對比筑累,不同則說明資源被改過袱蜡,則響應整個內(nèi)容,返回狀態(tài)碼是200慢宗,相同則說明資源沒有被改動過坪蚁,則響應狀態(tài)碼304,告知客戶端可以使用緩存
正式使用時按需求也許只包含其中部分字段镜沽,客戶端要根據(jù)這些信息存儲這次請求信息敏晤,然后在客戶端發(fā)起的時間內(nèi)檢查緩存,遵循下面的步驟
四.Cache-Control類詳解
CacheControl 對應HTTP里面的CacheControl
public final class CacheControl {
private final boolean noCache;
private final boolean noStore;
private final int maxAgeSeconds;
private final int sMaxAgeSeconds;
private final boolean isPrivate;
private final boolean isPublic;
private final boolean mustRevalidate;
private final int maxStaleSeconds;
private final int minFreshSeconds;
private final boolean onlyIfCached;
private final boolean noTransform;
/**
* Cache control request directives that require network validation of responses. Note that such
* requests may be assisted by the cache via conditional GET requests.
*/
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
/**
* Cache control request directives that uses the cache only, even if the cached response is
* stale. If the response isn't available in the cache or requires server validation, the call
* will fail with a {@code 504 Unsatisfiable Request}.
*/
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
}
CacheControl類是對HTTP的Cache-Control頭部的描述缅茉。CacheControl沒有公共的構(gòu)造方法,內(nèi)部通過一個Build進行設置值译打,獲取值可以通過CacheControl對象進行獲取。
Builder具體有如下設置方法:
- 1乔询、noCache()
對應于“no-cache”竿刁,如果出現(xiàn)在 響應 的頭部搪缨,不是表示不允許對響應進行緩存,而是表示客戶端需要與服務器進行再次驗證监婶,進行一個額外的GET請求得到最新的響應齿桃;如果出現(xiàn)請求頭部短纵,則表示不適用緩存響應香到,即記性網(wǎng)絡請求獲取響應报破。 - 2、noStore()
對應于"no-store"梗脾,如果出現(xiàn)在響應頭部盹靴,則表明該響應不能被緩存 - 3稿静、maxAge(int maxAge,TimeUnit timeUnit)
對應"max-age",設置緩存響應的最大存貨時間控漠。如果緩存響滿足了到了最大存活時間悬钳,那么將不會再進行網(wǎng)絡請求 - 4柬脸、maxStale(int maxStale,TimeUnit timeUnit)
對應“max-stale”倒堕,緩存響應可以接受的最大過期時間垦巴,如果沒有指定該參數(shù)铭段,那么過期緩存響應將不會被使用 - 5、minFresh(int minFresh,TimeUnit timeUnit)
對應"min-fresh"憔披,設置一個響應將會持續(xù)刷新最小秒數(shù)芬膝,如果一個響應當minFresh過去后過期了形娇,那么緩存響應不能被使用,需要重新進行網(wǎng)絡請求 - 6癣缅、onlyIfCached()
對應“onlyIfCached”哄酝,用于請求頭部陶衅,表明該請求只接受緩存中的響應。如果緩存中沒有響應侠驯,那么返回一個狀態(tài)碼為504的響應奕巍。
CacheControl類中還有其他方法,這里就不一一介紹了檩坚。想了解的可以去API文檔查看匾委。
對于常用的緩存控制,CacheControl中提供了兩個常用的于修飾請求薯鳍,F(xiàn)ORCE_CACHE表示只使用緩存中的響應挨措,哪怕這個緩存過期了,F(xiàn)ORCE_NETWORK這個表示只能使用網(wǎng)絡響應
五斩松、CacheStrategy類詳解
CacheStrategy 緩存策略類
OKHTTP使用了CacheStrategy實現(xiàn)了上面的流程圖惧盹,它根據(jù)之前緩存的結(jié)果與當前將要發(fā)送Request的header進行策略瞪讼,并得出是否進行請求的結(jié)果。
(一)演侯、策略原理
根據(jù)輸出的networkRequest和cacheResponse的值是否為null給出不同的策略,如下:
networkRequest | cacheResponse | result 結(jié)果 |
---|---|---|
null | null | only-if-cached (表明不進行網(wǎng)絡請求悬赏,且緩存不存在或者過期,一定會返回503錯誤) |
null | non-null | 不進行網(wǎng)絡請求盾戴,直接返回緩存尖啡,不請求網(wǎng)絡 |
non-null | null | 需要進行網(wǎng)絡請求剩膘,而且緩存不存在或者過去,直接訪問網(wǎng)絡 |
non-null | non-null | Header中包含ETag/Last-Modified標簽畏梆,需要在滿足條件下請求奠涌,還是需要訪問網(wǎng)絡 |
以上是對networkRequest/cacheResponse進行的switch查詢得出的,下面我們就詳細講解下
(二)捏卓、CacheStrategy類的構(gòu)造
CacheStrategy使用Factory模式進行構(gòu)造慈格,通過Factory的get()方法獲取CacheStrategy的對象,參數(shù)如下:
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();
//獲取cacheReposne中的header中值
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);
}
}
}
}
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
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;
}
/**
* Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
*/
public CacheStrategy get() {
//獲取當前的緩存策略
CacheStrategy candidate = getCandidate();
//如果是網(wǎng)絡請求不為null并且請求里面的cacheControl是只用緩存
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;
}
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
//如果沒有緩存響應,返回一個沒有響應的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//如果是https物邑,丟失了握手滔金,返回一個沒有響應的策略
// 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.
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//獲取請求頭里面的CacheControl
CacheControl requestCaching = request.cacheControl();
//如果請求里面設置了不緩存,則不緩存
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//獲取響應的年齡
long ageMillis = cacheResponseAge();
//獲取上次響應刷新的時間
long freshMillis = computeFreshnessLifetime();
//如果請求里面有最大持久時間要求科阎,則兩者選擇最短時間的要求
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;
//響應緩存控制器
CacheControl responseCaching = cacheResponse.cacheControl();
//如果響應(服務器)那邊不是必須驗證并且存在最大驗證秒數(shù)
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//更新最大驗證時間
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//響應支持緩存
//持續(xù)時間+最短刷新時間<上次刷新時間+最大驗證時間 則可以緩存
//現(xiàn)在時間(now)-已經(jīng)過去的時間(sent)+可以存活的時間<最大存活時間(max-age)
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;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
//緩存響應
return new CacheStrategy(null, builder.build());
}
//如果想緩存request锣笨,必須要滿足一定的條件
// 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;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
//沒有條件則返回一個定期的request
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
//返回有條件的緩存request策略
return new CacheStrategy(conditionalRequest, cacheResponse);
}
通過上面分析错英,我們可以發(fā)現(xiàn)椭岩,OKHTTP實現(xiàn)的緩存策略實質(zhì)上就是大量的if/else判斷璃赡,這些其實都是和RFC標準文檔里面寫死的。
上面說了這么多塌计,那么咱們要開始今天的主題了----CacheInterceptor類
六豆励、 CacheInterceptor 類詳解
BridgeInterceptor :負責將請求和返回 關聯(lián)的保存到緩存中〖级螅客戶端和服務器根據(jù)一定的機制(策略CacheStrategy ),在需要的時候使用緩存的數(shù)據(jù)作為網(wǎng)絡響應窍箍,節(jié)省了時間和寬帶丽旅。
老規(guī)矩上源碼:
//CacheInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
//如果存在緩存,則從緩存中取出邪狞,有可能為null
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);
}
//緩存策略不為null并且緩存響應是null
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//禁止使用網(wǎng)絡(根據(jù)緩存策略)米丘,緩存又無效,直接返回
// 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();
}
//緩存有效吁津,不使用網(wǎng)絡
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//緩存無效碍脏,執(zhí)行下一個攔截器
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());
}
}
//本地有緩存稍算,根據(jù)條件選擇使用哪個響應
// 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());
}
}
//使用網(wǎng)絡響應
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;
}
簡單的說下上述流程:
1邪蛔、如果配置緩存侧到,則從緩存中取一次淤击,不保證存在
2、緩存策略
3污抬、緩存監(jiān)測
4绳军、禁止使用網(wǎng)絡(根據(jù)緩存策略)门驾,緩存又無效多柑,直接返回
5、緩存有效聂沙,不使用網(wǎng)絡
6初嘹、緩存無效,執(zhí)行下一個攔截器
7坷随、本地有緩存漫贞,根具條件選擇使用哪個響應
8、使用網(wǎng)絡響應
9芍殖、 緩存到本地
大體流程分析完谴蔑,那么咱們再詳細分析下。
首先說到了緩存就不得不提下OKHttp里面的Cache.java類和InternalCache.java那么咱們就簡單的聊下這兩個類
(一)窃躲、Cache.java類
1钦睡、基本特征
private final DiskLruCache cache;
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
通過上面代碼可知
- 1、Cache對象擁有一個DiskLruCache引用洒琢。
- 2褐桌、Cache構(gòu)造器接受兩個參數(shù),意味著如果我們想要創(chuàng)建一個緩存必須指定緩存文件存儲的目錄和緩存文件的最大值
2呛踊、既然是Cache,那么一定會有"增"汪厨、"刪"蜻底、"改"、"查"要拂。
(1) ”增“操作——put()方法
CacheRequest put(Response response) {
String requestMethod = response.request().method();
//判斷請求如果是"POST"站楚、"PATCH"、"PUT"拉一、"DELETE"旧乞、"MOVE"中的任何一個則調(diào)用DiskLruCache.remove(urlToKey(request));將這個請求從緩存中移除出去。
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//判斷請求如果不是Get則不進行緩存嫡纠,直接返回null延赌。官方給的解釋是緩存get方法得到的Response效率高,其它方法的Response沒有緩存效率低者蠕。通常通過get方法獲取到的數(shù)據(jù)都是固定不變的的掐松,因此緩存效率自然就高了。其它方法會根據(jù)請求報文參數(shù)的不同得到不同的Response泻仙,因此緩存效率自然而然就低了量没。
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;
}
//判斷請求中的http數(shù)據(jù)包中headers是否有符號"*"的通配符突想,有則不緩存直接返回null
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//由Response對象構(gòu)建一個Entry對象,Entry是Cache的一個內(nèi)部類
Entry entry = new Entry(response);
//通過調(diào)用DiskLruCache.edit();方法得到一個DiskLruCache.Editor對象究抓。
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
//把這個entry寫入
//方法內(nèi)部是通過Okio.buffer(editor.newSink(ENTRY_METADATA));獲取到一個BufferedSink對象刺下,隨后將Entry中存儲的Http報頭數(shù)據(jù)寫入到sink流中稽荧。
entry.writeTo(editor);
//構(gòu)建一個CacheRequestImpl對象,構(gòu)造器中通過editor.newSink(ENTRY_BODY)方法獲得Sink對象
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
總結(jié)一下上述步驟
第一步畅卓,先判斷是不是一個正常的請求(get,post等)
第二步蟋恬,由于只支持get請求,非get請求直接返回
第三步拜马,通配符過濾
第四步沐绒,通過上述檢驗后開始真正的緩存流程,new一個Entry
第五步扮超,獲取一個DiskLruCache.Editor對象
第六步申眼,通過DiskLruCache.Edito寫入數(shù)據(jù)
第七步,返回數(shù)據(jù)
PS:關于key()方法在remove里面詳解
上面使用到了remove方法濒翻,莫非就是"刪"的操作,那咱們來看下
(2) ”刪“操作——remove()方法
void remove(Request request) throws IOException {
cache.remove(key(request.url()));
}
public static String key(HttpUrl url) {
return ByteString.encodeUtf8(url.toString()).md5().hex();
}
果然remove就是傳說中的"刪除"操作淌喻,
key()這個方法原來就說獲取url的MD5和hex生成的key
(3) ”改“操作——update()方法
void update(Response cached, Response network) {
//用response構(gòu)造一個Entry對象
Entry entry = new Entry(network);
//從命中緩存中獲取到的DiskLruCache.Snapshot
DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
//從DiskLruCache.Snapshot獲取DiskLruCache.Editor()對象
DiskLruCache.Editor editor = null;
try {
editor = snapshot.edit(); // Returns null if snapshot is not current.
if (editor != null) {
//將entry寫入editor中
entry.writeTo(editor);
editor.commit();
}
} catch (IOException e) {
abortQuietly(editor);
}
}
根據(jù)上述代碼大體流程如下:
第一步裸删,首先要獲取entry對象
第二步阵赠,獲取DiskLruCache.Editor對象
第三步肌稻,寫入entry對象
(4) ”查“操作——get()方法
Response get(Request request) {
//獲取url經(jīng)過MD5和HEX的key
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//根據(jù)key來獲取一個snapshot爹谭,由此可知我們的key-value里面的value對應的是snapshot
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
//利用前面的Snapshot創(chuàng)建一個Entry對象榛搔。存儲的內(nèi)容是響應的Http數(shù)據(jù)包Header部分的數(shù)據(jù)。snapshot.getSource得到的是一個Source對象 (source是okio里面的一個接口)
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//利用entry和snapshot得到Response對象腹泌,該方法內(nèi)部會利用前面的Entry和Snapshot得到響應的Http數(shù)據(jù)包Body(body的獲取方式通過snapshot.getSource(ENTRY_BODY)得到)創(chuàng)建一個CacheResponseBody對象童本;再利用該CacheResponseBody對象和第三步得到的Entry對象構(gòu)建一個Response的對象,這樣該對象就包含了一個網(wǎng)絡響應的全部數(shù)據(jù)了绑蔫。
Response response = entry.response(snapshot);
//對request和Response進行比配檢查配深,成功則返回該Response嫁盲。匹配方法就是url.equals(request.url().toString()) && requestMethod.equals(request.method()) && OkHeaders.varyMatches(response, varyHeaders, request);其中Entry.url和Entry.requestMethod兩個值在構(gòu)建的時候就被初始化好了,初始化值從命中的緩存中獲取缸托。因此該匹配方法就是將緩存的請求url和請求方法跟新的客戶請求進行對比瘾蛋。最后OkHeaders.varyMatches(response, varyHeaders, request)是檢查命中的緩存Http報頭跟新的客戶請求的Http報頭中的鍵值對是否一樣。如果全部結(jié)果為真佩抹,則返回命中的Response取董。
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
總結(jié)上面流程大體是:
第一步 獲取key
第一步 獲取DiskLruCache.Snapshot對象
第三步 獲取Entry對象
第四步 獲取response
第五步 檢查是response
通過對上述增刪改查的分析,我們可以得出如下結(jié)論
方法 | 返回值 |
---|---|
DiskLruCache.get(String) | 可以獲取DiskLruCache.Snapshot |
DiskLruCache.remove(String) | 可以移除請求 |
DiskLruCache.edit(String) | 可以獲得一個DiskLruCache.Editor對象 |
DiskLruCache.Editor.newSink(int) | 可以獲得一個sink流 |
DiskLruCache.Snapshot.getSource(int) | 可以獲取一個Source對象枢里。 |
DiskLruCache.Snapshot.edit() | 可以獲得一個DiskLruCache.Editor對象 |
DiskLruCache是OKHTTP的緩存的精髓,由于篇幅限制梭灿,在下一章講解