缓存机制
请求头缓存设置
- Cache-Control 常见的取值有private、public、no-cache、max-age、no-store、默认是 private。
在浏览器里面,private 表示客户端可以缓存,public表示客户端和服务器都可以缓存。 - Last-Modified 服务器告诉浏览器资源的最后修改时间。
- If-Modified-Since 客户端再次请求服务器时,通过此字段通知服务器上次服务器返回的最后修改时间。
资源被改动过,则响应内容返回的状态码是200;资源没有修改,则响应状态码为304,告诉客户端继续使用cache。 - Etag 服务响应请求时,告诉客户端当前资源在服务器的唯一标识
- If-None-Match 客户端再次请求服务器时,通过此字段通知服务器上次服务器返回的数据标识。
同修改过返回200,可以使用cache 返回304.
CacheStrategy类
CacheStrategy
根据输出的networkRequest和cacheResponse的值是否为null给出不同的策略
networkRequest | cacheResponse | result 结果 |
---|---|---|
null | null | only-if-cached (表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误) |
null | non-null | 不进行网络请求,直接返回缓存,不请求网络 |
non-null | null | 需要进行网络请求,而且缓存不存在或者过去,直接访问网络 |
non-null | non-null | Header中包含ETag/Last-Modified标签,需要在满足条件下请求,还是需要访问网络 |
Cachestrategy
通过如下方式构建
CacheStrategy strategy = new CacheStrategy.Factory(
now,
chain.request(),
cacheCandidate)
.get();
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
Headers headers = cacheResponse.headers();
//获取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() {
//获取当前的缓存策略
CacheStrategy candidate = getCandidate();
//如果是网络请求不为null并且请求里面的cacheControl是只用缓存
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
//使用只用缓存的策略
return new CacheStrategy(null, null);
}
return candidate;
}
private CacheStrategy getCandidate() {
//如果没有缓存响应,返回一个没有响应的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
//如果是https,丢失了握手,返回一个没有响应的策略
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// 响应不能被缓存
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();
//如果响应(服务器)那边不是必须验证并且存在最大验证秒数
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
//更新最大验证时间
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//响应支持缓存
//持续时间+最短刷新时间<上次刷新时间+最大验证时间 则可以缓存
//现在时间(now)-已经过去的时间(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,必须要满足一定的条件
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 类
负责将Request和Response 关联的保存到缓存中。客户端和服务器根据一定的机制(策略CacheStrategy ),在需要的时候使用缓存的数据作为网络响应,节省了时间和宽带。
//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.
}
//禁止使用网络(根据缓存策略),缓存又无效,直接返回
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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//缓存无效,执行下一个拦截器
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//本地有缓存,根据条件选择使用哪个响应
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());
}
}
//使用网络响应
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;
}
大致流程如下:
- 如果配置缓存,则从缓存中取一次
- 获取缓存策略
- 根据缓存策略获取缓存
- 没有网络并且缓存为空,直接返回
- 没有网络,直接根据缓存的response返回
- 执行下一个拦截器
- 存在缓存,根据response的相应头选择缓存
- 不存在缓存,直接使用网络 response
- 根据缓存策略缓存到本地
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 内部类
Entry
实际用于存储的缓存数据的实体类,每一个url对应一个Entry实体。同时,每个Entry对应两个文件,key.1存储的是Response的headers,key.2文件存储的是Response的body
Snapshot
一个Entry对象一一对应一个Snapshot对象
Editor
编辑entry类的
初始化
DiskLruCache
包含三个日志文件,在执行任何成员函数之前,都需要 initialize()
方法先进行初始化,虽然都调用,但整个生命周期只会被执行一次。
在执行 readJournalLine ()
的时候我们会根据不同的头部做出不同的操作
- 如果是CLEAN的话,对这个entry的文件长度进行更新
- 如果是DIRTY,说明这个值正在被操作,还没有commit,于是给entry分配一个Editor。
- 如果是READ,说明这个值被读过了,什么也不做。
journal 文件
libcore.io.DiskLruCache // MAGIC
1 // VERSION
100 // appVersion
2 // valueCount 每个entry的 value 数量
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
在执行 rebuildJournal ()
的时候
- 获取一个写入流,将lruEntries集合中的Entry对象写入tmp文件中,根据Entry的currentEditor的值判断是CLEAN还是DIRTY,来决定写入该Entry的key。如果是CLEAN还需要写入文件的大小bytes。
- 把journalFileTmp更名为journalFile
- 将journalWriter跟文件绑定,通过它来向journalWrite写入数据,最后设置一些属性即可。
其实 rebuild 操作是以lruEntries为准,把DIRTY和CLEAN的操作都写回到journal中。其实这个操作没有改动真正的value,只不过重写了一些事务的记录。事实上,lruEntries和journal文件共同确定了cache数据的有效性。lruEntries是索引,journal是归档。
总结:
- 通过LinkedHashMap实现LRU替换
- 通过本地维护Cache操作日志保证Cache原子性与可用性,同时为防止日志过分膨胀定时执行日志精简。
- 每一个Cache项对应两个状态副本:DIRTY,CLEAN。CLEAN表示当前可用的Cache。外部访问到cache快照均为CLEAN状态;DIRTY为编辑状态的cache。由于更新和创新都只操作DIRTY状态的副本,实现了读和写的分离。
- 每一个url请求cache有四个文件。首先是两个状态(DIRY,CLEAN),而每个状态又对应两个文件:一个(key.0, key.0.tmp)文件对应存储meta数据,一个(key.1, key.1.tmp)文件存储body数据。
CallServerInterceptor
连接与请求
OkHttp 中,ConnectionSpec用于描述HTTP数据经由socket时的socket连接配置。由 OkHttpClient 管理。
还提供了ConnectionSpecSelector,用以从ConnectionSpec几个中选择与SSLSocket匹配的ConnectionSpec,并对SSLSocket做配置操作。
在RetryAndFollowUpInterceptor这个拦截器中,需要创建Address,从OkHttpClient中获取ConnectionSpec集合,交给Address配置。
接着在ConnectInterceptor 这个拦截器中,newExchange()
-> find()
-> findHealthyConnection()
-> findConnection()
-> connect()
的时候,ConnectionSpec集合就会从Address中取出来,用于构建连接过程。
接着往下是 connect()
-> establishProtocol()
-> connectTls()
-> configureSecureSocket()
->OkHttpClient.apply()
-> supoortedSpec()
,这就是重新构建一个兼容的 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;
}
}
写完请求头和请求体会调用 sink.flush()
接下来是读取相应头和响应体
@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));
}