Okhttp的源碼分析
Okhttp的線程池和高并發(fā)
Okhttp鏈接池的使用
Okhttp的緩存機(jī)制
Okhttp的責(zé)任鏈模式
Okhttp的緩存機(jī)制
緩存目的:減少用戶向服務(wù)器發(fā)送請求的次數(shù)仁锯,從而加快響應(yīng)的速度福也,降低服務(wù)器的負(fù)載
Http協(xié)議下的緩存機(jī)制
強(qiáng)制緩存
通過http協(xié)議所傳送的數(shù)據(jù),會被保存到緩存數(shù)據(jù)庫中旨剥,強(qiáng)制緩存的意思是指抄瑟,若緩存數(shù)據(jù)庫中的數(shù)據(jù)仍未失效凡泣,則直接通過緩存數(shù)據(jù)庫獲得數(shù)據(jù),不再通過http向服務(wù)器發(fā)送請求锐借。其中有兩個比較重要的字段用于控制是否失效:
Expires
指過期的時間问麸,其值由服務(wù)器所決定。當(dāng)在緩存數(shù)據(jù)庫取得相應(yīng)的數(shù)據(jù)時钞翔,通過比較當(dāng)前時間與Expires來決定是否直接使用緩存數(shù)據(jù)庫中的數(shù)據(jù)严卖。然而值得注意的是,服務(wù)端和客戶端之間存在著延時布轿,沒有統(tǒng)一的時間標(biāo)準(zhǔn)哮笆,因此隨著Http協(xié)議的發(fā)展来颤,使用的機(jī)會也就越來越少
Cache-Control
字面理解為緩存的控制,其實(shí)際意思是緩存的屬性稠肘。類似于java中的作用域福铅,http中的緩存分為如下幾種類型:
- public:表示其中的數(shù)據(jù)完全可以被存儲,包括密碼等隱私信息项阴,且所有人就可以訪問滑黔,其安全性也較低
- private: 存儲到用戶的私有cache中去,只有用戶本身可以訪問(默認(rèn))
- no-cache 僅在客戶端與服務(wù)端建立認(rèn)證后环揽,才可以緩存(用于對比緩存)
- no-store 代表其中的請求和響應(yīng)等信息都不會被緩存
- max-age 表示所返回的數(shù)據(jù)已經(jīng)過期或失效
對比緩存
使用前要與服務(wù)器的緩存進(jìn)行對比略荡。通過服務(wù)器返回的狀態(tài)碼決定是否使用
304: 使用對比緩存的數(shù)據(jù)
200:使用服務(wù)器的最新數(shù)據(jù)
判定字段
Etag
資源的唯一標(biāo)識,類似于人們的身份證號碼歉胶,資源的內(nèi)容一旦發(fā)生改動汛兜,Etag就會發(fā)生改變⊥ń瘢客戶端發(fā)送請求的時候格式為 If-None-Match +Etag粥谬,服務(wù)器收到后則會與緩存的Etag進(jìn)行比對
Last-Modified
字面意思,最近修改的時間辫塌,由服務(wù)器所決定漏策,客戶端在發(fā)送請求的時候使用If-Modified-Since + 指定時間,若客戶端所存的數(shù)據(jù)≤該時間則說明資源沒有改動
To put into a nutshell :
Okhttp的緩存機(jī)制
可以看到璃氢,http本身協(xié)議的緩存機(jī)制較為簡單哟玷,不能很好的滿足實(shí)際的需求。okhttp相對來說便更加復(fù)雜一也。先說結(jié)論:
- 緩存基于文件存儲
- 內(nèi)部維護(hù)基于LRU算法的緩存清理線程
Okhttp讀取緩存流程
Okhttp 存儲緩存流程
源碼解析
CacheControl
用于指定緩存的規(guī)則
public final class CacheControl {
//表示這是一個優(yōu)先使用網(wǎng)絡(luò)驗證巢寡,驗證通過之后才可以使用緩存的緩存控制,設(shè)置了noCache
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
//表示這是一個優(yōu)先先使用緩存的緩存控制椰苟,設(shè)置了onlyIfCached和maxStale的最大值
public static final CacheControl FORCE_CACHE = new Builder()
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
//以下的字段都是HTTP中Cache-Control字段相關(guān)的值
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;
//解析頭文件中的相關(guān)字段抑月,得到該緩存控制類
public static CacheControl parse(Headers headers) {
...
}
}
CacheStrategy
主要用于判斷是否使用緩存數(shù)據(jù)
public final class CacheStrategy {
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
//網(wǎng)絡(luò)請求和緩存響應(yīng)
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
//找到緩存響應(yīng)的響應(yīng)頭信息
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {
//查看響應(yīng)頭信息中是否有以下字段信息
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()) {
// 如果判定的緩存策略的網(wǎng)絡(luò)請求不為空,但是只使用緩存舆蝴,則返回兩者都為空的緩存策略谦絮。
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.
//如果沒有緩存響應(yīng),則返回沒有緩存響應(yīng)的策略
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
//如果請求是https洁仗,而緩存響應(yīng)的握手信息為空层皱,則返回沒有緩存響應(yīng)的策略
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.
//如果請求對應(yīng)的響應(yīng)不能被緩存,則返回沒有緩存響應(yīng)的策略
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//獲取請求頭中的CacheControl信息
CacheControl requestCaching = request.cacheControl();
//如果請求頭中的CacheControl信息是不緩存的赠潦,則返回沒有緩存響應(yīng)的策略
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
//獲取響應(yīng)的年齡
long ageMillis = cacheResponseAge();
//計算上次響應(yīng)刷新的時間
long freshMillis = computeFreshnessLifetime();
//如果請求里有最大持續(xù)時間要求叫胖,則取較小的值作為上次響應(yīng)的刷新時間
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;
//獲取緩存響應(yīng)頭中的CacheControl信息
CacheControl responseCaching = cacheResponse.cacheControl();
//如果緩存響應(yīng)不是必須要再驗證她奥,并且請求有最大過期時間瓮增,則用請求的最大過期時間作為最大過期時間
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//如果支持緩存怎棱,并且持續(xù)時間+最短刷新時間<上次刷新時間+最大驗證時間 則可以緩存
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());
}
//構(gòu)造一個新的有條件的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();
//根據(jù)是否有If-None-Match拳恋,If-Modified-Since信息,返回不同的緩存策略
return hasConditions(conditionalRequest)
? new CacheStrategy(conditionalRequest, cacheResponse)
: new CacheStrategy(conditionalRequest, null);
}
/**
* Returns true if the 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
對外開放的緩存類砸捏,類似數(shù)據(jù)庫能夠增刪改查
- 增添緩存
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數(shù)據(jù)包中headers是否有符號"*"的通配符,有則不緩存
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//把response構(gòu)建成一個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;
}
//對緩存進(jìn)行寫入
entry.writeTo(editor);
//構(gòu)建一個CacheRequestImpl類垦藏,包含Ok.io的Sink對象
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
- 查找緩存
Response get(Request request) {
//獲取url轉(zhuǎn)換過來的key
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//根據(jù)key獲取對應(yīng)的snapshot
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
return null;
}
try {
//創(chuàng)建一個Entry對象,并由snapshot.getSource()獲取Sink
entry = 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進(jìn)行比配檢查膝藕,成功則返回該Response。
Util.closeQuietly(response.body());
return null;
}
return response;
}
- 更新緩存
void update(Response cached, Response network) {
//用Respon構(gòu)建一個Entry
Entry entry = new Entry(network);
//從緩存中獲取DiskLruCache.Snapshot
DiskLruCache.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轉(zhuǎn)化成的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
真實(shí)存儲(文件格式)的緩存功能類咐扭,使用了基于LinkedHashedMap芭挽。可以看到除了一些關(guān)鍵的方法之外其主要包括了三個重要的內(nèi)部類蝗肪。
- Entry
用于存儲緩存數(shù)據(jù)的實(shí)體類袜爪,一個url對應(yīng)一個實(shí)體,在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,其實(shí)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) {
throw new IllegalStateException();
}
if (!entry.readable || entry.currentEditor != this) {
return null;
}
try {
return fileSystem.source(entry.cleanFiles[index]);
} catch (FileNotFoundException e) {
return null;
}
}
}
/**
* Returns a new unbuffered output stream to write the value at {@code index}. If the underlying
* output stream encounters errors when writing to the filesystem, this edit will be aborted
* when {@link #commit} is called. The returned output stream does not throw IOExceptions.
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor != this) {
return Okio.blackhole();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.dirtyFiles[index];
Sink sink;
try {
sink = fileSystem.sink(dirtyFile);
} catch (FileNotFoundException e) {
return Okio.blackhole();
}
return new FaultHidingSink(sink) {
@Override protected void onException(IOException e) {
synchronized (DiskLruCache.this) {
detach();
}
}
};
}
}
/**
* Commits this edit so it is visible to readers. This releases the edit lock so another edit
* may be started on the same key.
*/
public void commit() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, true);
}
done = true;
}
}
/**
* Aborts this edit. This releases the edit lock so another edit may be started on the same
* key.
*/
public void abort() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, false);
}
done = true;
}
}
public void abortUnlessCommitted() {
synchronized (DiskLruCache.this) {
if (!done && entry.currentEditor == this) {
try {
completeEdit(this, false);
} catch (IOException ignored) {
}
}
}
}
}
- 刪除
boolean removeEntry(Entry entry) throws IOException {
if (entry.currentEditor != null) {
entry.currentEditor.detach(); // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
fileSystem.delete(entry.cleanFiles[i]);
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\\n');
lruEntries.remove(entry.key);
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return true;
}