緩存機(jī)制
請求頭緩存設(shè)置
- Cache-Control 常見的取值有private、public、no-cache、max-age、no-store、默認(rèn)是 private。
在瀏覽器里面,private 表示客戶端可以緩存,public表示客戶端和服務(wù)器都可以緩存。 - Last-Modified 服務(wù)器告訴瀏覽器資源的最后修改時(shí)間。
- If-Modified-Since 客戶端再次請求服務(wù)器時(shí),通過此字段通知服務(wù)器上次服務(wù)器返回的最后修改時(shí)間。
資源被改動過,則響應(yīng)內(nèi)容返回的狀態(tài)碼是200;資源沒有修改,則響應(yīng)狀態(tài)碼為304,告訴客戶端繼續(xù)使用cache。 - Etag 服務(wù)響應(yīng)請求時(shí),告訴客戶端當(dāng)前資源在服務(wù)器的唯一標(biāo)識
- If-None-Match 客戶端再次請求服務(wù)器時(shí),通過此字段通知服務(wù)器上次服務(wù)器返回的數(shù)據(jù)標(biāo)識。
同修改過返回200,可以使用cache 返回304.
CacheStrategy類
CacheStrategy 根據(jù)輸出的networkRequest和cacheResponse的值是否為null給出不同的策略
| networkRequest | cacheResponse | result 結(jié)果 |
|---|---|---|
| null | null | only-if-cached (表明不進(jìn)行網(wǎng)絡(luò)請求,且緩存不存在或者過期,一定會返回503錯誤) |
| null | non-null | 不進(jìn)行網(wǎng)絡(luò)請求,直接返回緩存,不請求網(wǎng)絡(luò) |
| non-null | null | 需要進(jìn)行網(wǎng)絡(luò)請求,而且緩存不存在或者過去,直接訪問網(wǎng)絡(luò) |
| non-null | non-null | Header中包含ETag/Last-Modified標(biāo)簽,需要在滿足條件下請求,還是需要訪問網(wǎng)絡(luò) |
Cachestrategy 通過如下方式構(gòu)建
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();
//獲取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);
}
}
}
}
public CacheStrategy get() {
//獲取當(dāng)前的緩存策略
CacheStrategy candidate = getCandidate();
//如果是網(wǎng)絡(luò)請求不為null并且請求里面的cacheControl是只用緩存
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
//使用只用緩存的策略
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
//如果沒有緩存響應(yīng),返回一個沒有響應(yīng)的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//如果是https,丟失了握手,返回一個沒有響應(yīng)的策略
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 響應(yīng)不能被緩存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//獲取請求頭里面的CacheControl
CacheControl requestCaching = request.cacheControl();
//如果請求里面設(shè)置了不緩存,則不緩存
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//獲取響應(yīng)的年齡
long ageMillis = cacheResponseAge();
//獲取上次響應(yīng)刷新的時(shí)間
long freshMillis = computeFreshnessLifetime();
//如果請求里面有最大持久時(shí)間要求,則兩者選擇最短時(shí)間的要求
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
//如果請求里面有最小刷新時(shí)間的限制
if (requestCaching.minFreshSeconds() != -1) {
//用請求中的最小更新時(shí)間來更新最小時(shí)間限制
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
//最大驗(yàn)證時(shí)間
long maxStaleMillis = 0;
//響應(yīng)緩存控制器
CacheControl responseCaching = cacheResponse.cacheControl();
//如果響應(yīng)(服務(wù)器)那邊不是必須驗(yàn)證并且存在最大驗(yàn)證秒數(shù)
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//更新最大驗(yàn)證時(shí)間
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//響應(yīng)支持緩存
//持續(xù)時(shí)間+最短刷新時(shí)間<上次刷新時(shí)間+最大驗(yàn)證時(shí)間 則可以緩存
//現(xiàn)在時(shí)間(now)-已經(jīng)過去的時(shí)間(sent)+可以存活的時(shí)間<最大存活時(shí)間(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\"");
}
//緩存響應(yīng)
return new CacheStrategy(null, builder.build());
}
//如果想緩存request,必須要滿足一定的條件
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);
}
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);
}
CacheInterceptor 類
負(fù)責(zé)將Request和Response 關(guān)聯(lián)的保存到緩存中。客戶端和服務(wù)器根據(jù)一定的機(jī)制(策略CacheStrategy ),在需要的時(shí)候使用緩存的數(shù)據(jù)作為網(wǎng)絡(luò)響應(yīng),節(jié)省了時(shí)間和寬帶。
//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;
//策略中的響應(yīng)
Response cacheResponse = strategy.cacheResponse;
//緩存非空判斷,
if (cache != null) {
cache.trackResponse(strategy);
}
//緩存策略不為null并且緩存響應(yīng)是null
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
//禁止使用網(wǎng)絡(luò)(根據(jù)緩存策略),緩存又無效,直接返回
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)絡(luò)
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//緩存無效,執(zhí)行下一個攔截器
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//本地有緩存,根據(jù)條件選擇使用哪個響應(yīng)
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//使用網(wǎng)絡(luò)響應(yīng)
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
//緩存到本地
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
}
大致流程如下:
- 如果配置緩存,則從緩存中取一次
- 獲取緩存策略
- 根據(jù)緩存策略獲取緩存
- 沒有網(wǎng)絡(luò)并且緩存為空,直接返回
- 沒有網(wǎng)絡(luò),直接根據(jù)緩存的response返回
- 執(zhí)行下一個攔截器
- 存在緩存,根據(jù)response的相應(yīng)頭選擇緩存
- 不存在緩存,直接使用網(wǎng)絡(luò) response
- 根據(jù)緩存策略緩存到本地
Cache
public final class Cache implements Closeable, Flushable {
final InternalCache internalCache = new InternalCache() {
@Override public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
}
DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
Executor executor) {
this.fileSystem = fileSystem;
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
this.executor = executor;
}
DiskLruCache 內(nèi)部類
Entry 實(shí)際用于存儲的緩存數(shù)據(jù)的實(shí)體類,每一個url對應(yīng)一個Entry實(shí)體。同時(shí),每個Entry對應(yīng)兩個文件,key.1存儲的是Response的headers,key.2文件存儲的是Response的body
Snapshot 一個Entry對象一一對應(yīng)一個Snapshot對象
Editor 編輯entry類的
初始化
DiskLruCache包含三個日志文件,在執(zhí)行任何成員函數(shù)之前,都需要 initialize() 方法先進(jìn)行初始化,雖然都調(diào)用,但整個生命周期只會被執(zhí)行一次。
在執(zhí)行 readJournalLine () 的時(shí)候我們會根據(jù)不同的頭部做出不同的操作
- 如果是CLEAN的話,對這個entry的文件長度進(jìn)行更新
- 如果是DIRTY,說明這個值正在被操作,還沒有commit,于是給entry分配一個Editor。
- 如果是READ,說明這個值被讀過了,什么也不做。
journal 文件
libcore.io.DiskLruCache // MAGIC
1 // VERSION
100 // appVersion
2 // valueCount 每個entry的 value 數(shù)量
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
在執(zhí)行 rebuildJournal () 的時(shí)候
- 獲取一個寫入流,將lruEntries集合中的Entry對象寫入tmp文件中,根據(jù)Entry的currentEditor的值判斷是CLEAN還是DIRTY,來決定寫入該Entry的key。如果是CLEAN還需要寫入文件的大小bytes。
- 把journalFileTmp更名為journalFile
- 將journalWriter跟文件綁定,通過它來向journalWrite寫入數(shù)據(jù),最后設(shè)置一些屬性即可。
其實(shí) rebuild 操作是以lruEntries為準(zhǔn),把DIRTY和CLEAN的操作都寫回到j(luò)ournal中。其實(shí)這個操作沒有改動真正的value,只不過重寫了一些事務(wù)的記錄。事實(shí)上,lruEntries和journal文件共同確定了cache數(shù)據(jù)的有效性。lruEntries是索引,journal是歸檔。
總結(jié):
- 通過LinkedHashMap實(shí)現(xiàn)LRU替換
- 通過本地維護(hù)Cache操作日志保證Cache原子性與可用性,同時(shí)為防止日志過分膨脹定時(shí)執(zhí)行日志精簡。
- 每一個Cache項(xiàng)對應(yīng)兩個狀態(tài)副本:DIRTY,CLEAN。CLEAN表示當(dāng)前可用的Cache。外部訪問到cache快照均為CLEAN狀態(tài);DIRTY為編輯狀態(tài)的cache。由于更新和創(chuàng)新都只操作DIRTY狀態(tài)的副本,實(shí)現(xiàn)了讀和寫的分離。
- 每一個url請求cache有四個文件。首先是兩個狀態(tài)(DIRY,CLEAN),而每個狀態(tài)又對應(yīng)兩個文件:一個(key.0, key.0.tmp)文件對應(yīng)存儲meta數(shù)據(jù),一個(key.1, key.1.tmp)文件存儲body數(shù)據(jù)。
CallServerInterceptor
連接與請求
OkHttp 中,ConnectionSpec用于描述HTTP數(shù)據(jù)經(jīng)由socket時(shí)的socket連接配置。由 OkHttpClient 管理。
還提供了ConnectionSpecSelector,用以從ConnectionSpec幾個中選擇與SSLSocket匹配的ConnectionSpec,并對SSLSocket做配置操作。
在RetryAndFollowUpInterceptor這個攔截器中,需要創(chuàng)建Address,從OkHttpClient中獲取ConnectionSpec集合,交給Address配置。
接著在ConnectInterceptor 這個攔截器中,newExchange() -> find() -> findHealthyConnection() -> findConnection() -> connect() 的時(shí)候,ConnectionSpec集合就會從Address中取出來,用于構(gòu)建連接過程。
接著往下是 connect() -> establishProtocol() -> connectTls() -> configureSecureSocket() ->OkHttpClient.apply() -> supoortedSpec() ,這就是重新構(gòu)建一個兼容的 ConnectionSpec,并配置到 SSLSocket 上
請求頭
@Override public void writeRequestHeaders(Request request) throws IOException {
String requestLine = RequestLine.get(
request, streamAllocation.connection().route().proxy().type());
writeRequest(request.headers(), requestLine);
}
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
for (int i = 0, size = headers.size(); i < size; i++) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
.writeUtf8("\r\n");
}
sink.writeUtf8("\r\n");
state = STATE_OPEN_REQUEST_BODY;
}
請求體
@Override
public Sink createRequestBody(Request request, long contentLength) {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
return newChunkedSink();
}
...
}
private final class ChunkedSink implements Sink {
private final ForwardingTimeout timeout = new ForwardingTimeout(sink.timeout());
private boolean closed;
ChunkedSink() {
}
@Override public Timeout timeout() {
return timeout;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
if (byteCount == 0) return;
sink.writeHexadecimalUnsignedLong(byteCount);
sink.writeUtf8("\r\n");
sink.write(source, byteCount);
sink.writeUtf8("\r\n");
}
@Override public synchronized void flush() throws IOException {
if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
sink.flush();
}
@Override public synchronized void close() throws IOException {
if (closed) return;
closed = true;
sink.writeUtf8("0\r\n\r\n");
detachTimeout(timeout);
state = STATE_READ_RESPONSE_HEADERS;
}
}
寫完請求頭和請求體會調(diào)用 sink.flush()
接下來是讀取相應(yīng)頭和響應(yīng)體
@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
throw new IllegalStateException("state: " + state);
}
try {
StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
.message(statusLine.message)
.headers(readHeaders());
if (expectContinue && statusLine.code == HTTP_CONTINUE) {
return null;
}
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
@Override public ResponseBody openResponseBody(Response response) throws IOException {
Source source = getTransferStream(response);
return new RealResponseBody(response.headers(), Okio.buffer(source));
}