Okhttp 的缓存机制
CacheStrategy
主要用于判断是否使用缓存数据
public final class CacheStrategy {
public Factory(long nowMillis, Request request, Response cacheResponse) {this.nowMillis = nowMillis;//网络请求和缓存响应 this.request = request;this.cacheResponse = cacheResponse;
if (cacheResponse != null) {//找到缓存响应的响应头信息 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 = HeaderParser.parseSeconds(value, -1);} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {sentRequestMillis = Long.parseLong(value);} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {receivedResponseMillis = Long.parseLong(value);}}}}
public CacheStrategy get() {//获取判定的缓存策略 CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {// 如果判定的缓存策略的网络请求不为空,但是只使用缓存,则返回两者都为空的缓存策略。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);}
// Drop the cached response if it's missing a required handshake.//如果请求是 https,而缓存响应的握手信息为空,则返回没有缓存响应的策略 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();//如果请求头中的 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 信息 CacheControl responseCaching = cacheResponse.cacheControl();//如果缓存响应不是必须要再验证,并且请求有最大过期时间,则用请求的最大过期时间作为最大过期时间 if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());}
//如果支持缓存,并且持续时间+最短刷新时间<上次刷新时间+最大验证时间 则可以缓存 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,添加 If-None-Match,If-Modified-Since 等信息 Request.Builder conditionalRequestBuilder = request.newBuilder();
if (etag != null) {conditionalRequestBuilder.header("If-None-Match", etag);} else if (lastModified != null) {conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);} else if (servedDate != null) {conditionalRequestBuilder.header("If-Modified-Since", servedDateString);}
Request conditionalRequest = conditionalRequestBuilder.build();//根据是否有 If-None-Match,If-Modified-Since 信息,返回不同的缓存策略 return hasConditions(conditionalRequest)? new CacheStrategy(conditionalRequest, cacheResponse): new CacheStrategy(conditionalRequest, null);}
/**
Returns true if t
he request contains conditions that save the server from sending a response
that the client has locally. When a request is enqueued with its own conditions, the built-in
response cache won't be used.*/private static boolean hasConditions(Request request) {return request.header("If-Modified-Since") != null || request.header("If-None-Match") != null;}}
Cache
对外开放的缓存类,类似数据库能够增删改查
增添缓存
CacheRequest put(Response response) {String requestMethod = response.request().method();//如果请求是"POST","PUT","PATCH","PROPPATCH","REPORT"则移除这些缓存
if (HttpMethod.invalidatesCache(response.request().method())) {try {remove(response.request());} catch (IOException ignored) {}return null;}//仅支持 GET 的请求缓存,其他请求不缓存 if (!requestMethod.equals("GET")) {return null;}//判断请求中的 http 数据包中 headers 是否有符号"*"的通配符,有则不缓存
if (HttpHeaders.hasVaryAll(response)) {return null;}//把 response 构建成一个 Entry 对象 Entry entry = new Entry(response);DiskLruCache.Editor editor = null;try {//生成 DiskLruCache.Editor 对象 editor = cache.edit(key(response.request().url()));if (editor == null) {return null;}//对缓存进行写入 entry.writeTo(editor);//构建一个 CacheRequestImpl 类,包含 Ok.io 的 Sink 对象 return new CacheRequestImpl(editor);} catch (IOException e) {abortQuietly(editor);return null;}}
查找缓存
Response get(Request request) {//获取 url 转换过来的 keyString key = key(request.url());DiskLruCache.Snapshot snapshot;Entry entry;try {//根据 key 获取对应的 snapshotsnapshot = cache.get(key);if (snapshot == null) {return null;}} catch (IOException e) {return null;}try {//创建一个 Entry 对象,并由 snapshot.getSource()获取 Sinkentry = new Entry(snapshot.getSource(ENTRY_METADATA));} catch (IOException e) {Util.closeQuietly(snapshot);return null;}//通过 entry 和 response 生成 respson,通过 Okio.buffer 获取请求体,然后封装各种请求信息 Response response = entry.response(snapshot);if (!entry.matches(request, response)) {//对 request 和 Response 进行比配检查,成功则返回该 Response。Util.closeQuietly(response.body());return null;}return response;}
更新缓存
void update(Response cached, Response network) {//用 Respon 构建一个 EntryEntry entry = new Entry(network);//从缓存中获取 DiskLruCache.SnapshotDiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;DiskLruCache.Editor editor = null;try {//获取 DiskLruCache.Snapshot.edit 对象 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);}}
删除缓存
主体位于 DiskLruCache 之中
void remove(Request request) throws IOException {//通过 url 转化成的 key 去删除缓存 cache.remove(key(request.url()));}
writeTo ok.io
public void writeTo(DiskLruCache.Editor editor) throws IOException {BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url).writeByte('\n');sink.writeUtf8(requestMethod).writeByte('\n');sink.writeDecimalLong(varyHeaders.size()).writeByte('\n');for (int i = 0, size = varyHeaders.size(); i < size; i++) {sink.writeUtf8(varyHeaders.name(i)).writeUtf8(": ").writeUtf8(varyHeaders.value(i)).writeByte('\n');}
sink.writeUtf8(new StatusLine(protocol, code, message).toString()).writeByte('\n');sink.writeDecimalLong(responseHeaders.size() + 2).writeByte('\n');for (int i = 0, size = responseHeaders.size(); i < size; i++) {sink.writeUtf8(responseHeaders.name(i)).writeUtf8(": ").writeUtf8(responseHeaders.value(i)).writeByte('\n');}sink.writeUtf8(SENT_MILLIS).writeUtf8(": ").writeDecimalLong(sentRequestMillis).writeByte('\n');sink.writeUtf8(RECEIVED_MILLIS).writeUtf8(": ").writeDecimalLong(receivedResponseMillis).writeByte('\n');
if (isHttps()) {sink.writeByte('\n');sink.writeUtf8(handshake.cipherSuite().javaName()).writeByte('\n');writeCertList(sink, handshake.peerCertificates());writeCertList(sink, handshake.localCertificates());sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');}sink.close();}
DiskLruCache
真实存储(文件格式)的缓存功能类,使用了基于 LinkedHashedMap。可以看到除了一些关键的方法之外其主要包括了三个重要的内部类。
Entry
用于存储缓存数据的实体类,一个 url 对应一个实体,在 Entry 还有 Snapshot 对象
private final class Entry {final String key;
/** Lengths of this entry's files. */final long[] lengths;final File[] cleanFiles;final File[] dirtyFiles;
/** True if this entry has ever been published. */boolean readable;
/** The ongoing edit or null if this entry is not being edited. */Editor currentEditor;
/** The sequence number of the most recently committed edit to this entry. */long sequenceNumber;
Entry(String key) {this.key = key;
lengths = new long[valueCount];cleanFiles = new File[valueCount];dirtyFiles = new File[valueCount];
// The names are repetitive so re-use the same builder to avoid allocations.StringBuilder fileBuilder = new StringBuilder(key).append('.');int truncateTo = fileBuilder.length();for (int i = 0; i < valueCount; i++) {fileBuilder.append(i);cleanFiles[i] = new File(directory, fileBuilder.toString());fileBuilder.append(".tmp");dirtyFiles[i] = new File(directory, fileBuilder.toString());fileBuilder.setLength(truncateTo);}}
/** Set lengths using decimal numbers like "10123". */void setLengths(String[] strings) throws IOException {if (strings.length != valueCount) {throw invalidLengths(strings);}
try {for (int i = 0; i < strings.length; i++) {lengths[i] = Long.parseLong(strings[i]);}} catch (NumberFormatException e) {throw invalidLengths(strings);}}
/** Append space-prefixed lengths to {@code writer}. */void writeLengths(BufferedSink writer) throws IOException {for (long length : lengths) {writer.writeByte(' ').writeDecimalLong(length);}}
private IOException invalidLengths(String[] strings) throws IOException {throw new IOException("unexpected journal line: " + Arrays.toString(strings));}
/**
Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
single published snapshot. If we opened streams lazily then the streams could come from
different edits.*/Snapshot snapshot() {if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
Source[] sources = new Source[valueCount];long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.try {for (int i = 0; i < valueCount; i++) {sources[i] = fileSystem.source(cleanFiles[i]);}return new Snapshot(key, sequenceNumber, sources, lengths);} catch (FileNotFoundException e) {// A file must have been deleted manually!for (int i = 0; i < valueCount; i++) {if (sources[i] != null) {Util.closeQuietly(sources[i]);} else {break;}}// Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache// size.)try {removeEntry(this);} catch (IOException ignored) {}return null;}}}
Snapshot
public final class Snapshot implements Closeable {private final String key;private final long sequenceNumber;private final Source[] sources;private final long[] lengths;
Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {this.key = key;this.sequenceNumber = sequenceNumber;this.sources = sources;this.lengths = lengths;}
public String key() {return key;}
/**
Returns an editor for this snapshot's entry, or null if either the entry has changed since
this snapshot was created or if another edit is in progress.*/public @Nullable Editor edit() throws IOException {return DiskLruCache.this.edit(key, sequenceNumber);}
/** Returns the unbuffered stream with the value for {@code index}. */public Source getSource(int index) {return sources[index];}
/** Returns the byte length of the value for {@code index}. */public long getLength(int index) {return lengths[index];}
public void close() {for (Source in : sources) {Util.closeQuietly(in);}}}
Editor
在 Editor 的初始化中要传入 Editor,其实 Editor 就是编辑 entry 的类
public final class Editor {final Entry entry;final boolean[] written;private boolean done;
Editor(Entry entry) {this.entry = entry;this.written = (entry.readable) ? null : new boolean[valueCount];}
/**
Prevents this editor from completing normally. This is necessary either when the edit causes
an I/O error, or if the target entry is evicted while this editor is active. In either case
we delete the editor's created files and prevent new files from being created. Note that once
an editor has been detached it is possible for another editor to edit the entry.*/void detach() {if (entry.currentEditor == this) {for (int i = 0; i < valueCount; i++) {try {fileSystem.delete(entry.dirtyFiles[i]);} catch (IOException e) {// This file is potentially leaked. Not much we can do about that.}}entry.currentEditor = null;}}
/**
Returns an unbuffered input stream to read the last committed value, or null if no value has
been committed.*/public Source newSource(int index) {synchronized (DiskLruCache.this) {if (done) {
评论