在講解CacheInterceptor之前,我們先了解一下OkHttp的快取機制,主要是Cache這個類,演示下如何使用OkHttp的快取:
private void cacheOkHttpRequest(){
OkHttpClient okHttpClient = new OkHttpClient
.Builder()
.cache(new Cache(new File(Environment.getExternalStorageDirectory()+ "/okttp_caches"),24*1024*1024))
.build();
Request request = new Request
.Builder()
.url("http://www.ifeng.com")
.build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
Log.e(TAG,"response: " + response.body().string());
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
上述程式碼中在OkHttpClient的Builder中配置了cache,傳入快取目錄的File物件以及快取最大容量(單位位元組),這裡請求了鳳凰網,請求成功後,OkHttp會將請求的相關資料進行快取,當下次請求無法連結到網路的時候,它會讀取快取並將資料返回。
根據快取的流程,我們可以猜測到Cache類會有儲存和讀取快取的方法,我們通過檢視Cache類的原始碼,果然發現了put()和get()方法,分別用於儲存快取和讀取快取:
@Nullable CacheRequest put(Response response) {
//獲取請求方法
String requestMethod = response.request().method();
//一、傳入請求方法,判斷是否要快取
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//二、非GET請求的方法不進行快取
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;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//三、建立快取實體
Entry entry = new Entry(response);
//四、使用 DiskLruCache快取策略
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
複製程式碼
在分析這個方法之前,需要先知道的是,OkHttp預設只會對get請求進行快取,post請求是不會進行快取,這也是有道理的,因為get請求的資料一般是比較持久的,而post一般是互動操作,沒太大意義進行快取;當然,也可以自己實現post請求的快取操作,這個需要根據自己的專案需求來。
回到put()方法的分析,上述程式碼中,先是對請求方法進行了判斷,是否需要快取下來。
註釋一處,呼叫了HttpMethod的invalidatesCache():
public final class HttpMethod {
public static boolean invalidatesCache(String method) {
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
|| method.equals("DELETE")
|| method.equals("MOVE"); // WebDAV
}
...
}
複製程式碼
如果請求方法是POST、PATCH、PUT、DELETE以及MOVE中一個,就會將當前請求的快取移除。
接著判斷是否是非GET請求,如果是則不進行快取。
最後,開始進行快取的操作,註釋三處建立了一個Entry類,傳入Response物件,檢視其構造方法:
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
複製程式碼
Entry會將請求url、頭部、請求方式、協議、響應碼等一系列引數儲存下來。
註釋四處,建立快取策略,OkHttp的快取策略使用的是DiskLruCache,DiskLruCache是用於磁碟快取的一套解決框架,OkHttp對DiskLruCache稍微做了點修改,並且OkHttp內部維護著清理記憶體的執行緒池,通過這個執行緒池完成快取的自動清理和管理工作,本篇不做過多介紹。
獲取到DiskLruCache的Editor物件後,通過它的edit方法建立快取檔案,傳入快取的檔名,檔名的生成是通過key()方法將請求url進行MD5加密並獲取它的十六進位制表示形式。
接著執行Entry物件的writeTo()方法並傳入Editor物件,writeTo()方法是將快取資訊儲存在本地:
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
//快取請求的url
sink.writeUtf8(url)
.writeByte('\n');
//快取請求方法
sink.writeUtf8(requestMethod)
.writeByte('\n');
//快取請求頭部大小
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
//快取請求頭部資訊
//快取協議,響應碼
//快取響應頭部資訊
//快取傳送請求時間以及響應時間
...
//判斷是否是https請求
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();
}
複製程式碼
上述程式碼中並沒有對響應主體的body進行快取,在呼叫Entry的writeTo()方法之後,返回了一個CacheRequestImpl物件:
private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
private Sink cacheOut;
private Sink body;
boolean done;
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
複製程式碼
這裡我們可以看到Repsonse的body是在這裡被寫入快取的,CacheRequestImpl實現CacheRequest介面,用於暴露給快取攔截器,快取攔截器可以通過這個類來寫入或更新快取的資料。
接下來分析獲取快取的get()方法:
@Nullable Response get(Request request) {
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
複製程式碼
顯示通過請求的url獲取到快取對應的檔名key,然後建立一個DiskLruCache.Snapshot快取快照物件,根據key獲取對應的快取快照,如果獲取到的為null,則說明沒有找到快取,直接返回null;如果找到對應的快取快照,則根據快照生成Entry物件,再通過呼叫Entry的response()方法獲取到快取的Response物件。
response()方法:
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
}
複製程式碼
這裡實際上根據快取的資料建立了Request請求,並返回生成的Response物件。
獲取到快取的Response物件後,get()方法裡將其與傳入的request進行比對,如果二者不是成對的,則關閉流且返回null,如果是的話,則返回由快取資料生成的Response物件。
關於OkHttpClient的快取機制,這裡已經初步介紹完了,本篇主要是為介紹CacheInterceptor做鋪墊,下一篇我們將瞭解第三個攔截器CacheInterceptor快取攔截器,感興趣的朋友可以繼續閱讀: