Android小知識-剖析OkHttp中的五個攔截器(中篇)
本平臺的文章更新會有延遲,大家可以關注微信公眾號-顧林海,包括年底前會更新kotlin由淺入深系列教程,目前計劃在微信公眾號進行首發,如果大家想獲取最新教程,請關注微信公眾號,謝謝
在上一小節介紹了重試重定向攔截器RetryAndFollowUpInterceptor和橋接適配攔截器BridgeInterceptor,這節分析快取攔截器CacheInterceptor。
快取策略
mHttpClient = new OkHttpClient.Builder() .cache(new Cache(new File("cache"),30*1024*1024))//使用快取策略 .build();
在OkHttp中使用快取可以通過OkHttpClient的靜態內部類Builder的cache方法進行配置,cache方法傳入一個Cache物件。
public Cache(File directory, long maxSize) { this(directory, maxSize, FileSystem.SYSTEM); }
建立Cache物件時需要傳入兩個引數,第一個引數directory代表的是快取目錄,第二個引數maxSize代表的是快取大小。
在Chache有一個很重要的介面:
public final class Cache implements Closeable, Flushable { final InternalCache internalCache = new InternalCache() { @Override public Response get(Request request) throws IOException { return Cache.this.get(request); } @Override public 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); } }; ... }
通過InternalCache這個介面實現了快取的一系列操作,接著我們一步步看它是如何實現的,接下來分析快取的get和put操作。
先看InternalCache的put方法,也就是儲存快取:
public final class Cache implements Closeable, Flushable { final InternalCache internalCache = new InternalCache() { ... @Override public CacheRequest put(Response response) throws IOException { return Cache.this.put(response); } ... }; ... }
InternalCache的put方法呼叫的是Cache的put方法,往下看:
@Nullable CacheRequest put(Response response) { //標記1:獲取請求方法 String requestMethod = response.request().method(); //標記2:判斷快取是否有效 if (HttpMethod.invalidatesCache(response.request().method())) { try { remove(response.request()); } catch (IOException ignored) { } return null; } //標記3:非GET請求不使用快取 if (!requestMethod.equals("GET")) { return null; } if (HttpHeaders.hasVaryAll(response)) { return null; } //標記4:建立快取體類 Cache.Entry entry = new Cache.Entry(response); //標記5:使用DiskLruCache快取策略 DiskLruCache.Editor editor = null; try { editor = cache.edit(key(response.request().url())); if (editor == null) { return null; } entry.writeTo(editor); return new Cache.CacheRequestImpl(editor); } catch (IOException e) { abortQuietly(editor); return null; } }
首先在標記1處獲取我們的請求方式,接著在標記2處根據請求方式判斷快取是否有效,通過HttpMethod的靜態方法invalidatesCache。
public static boolean invalidatesCache(String method) { return method.equals("POST") || method.equals("PATCH") || method.equals("PUT") || method.equals("DELETE") || method.equals("MOVE");// WebDAV }
通過invalidatesCache方法,如果請求方式是POST、PATCH、PUT、DELETE以及MOVE中一個,就會將當前請求的快取移除。
在標記3處會判斷如果當前請求不是GET請求,就不會進行快取。
在標記4處建立Entry物件,Entry的構造器如下:
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、頭部、請求方式、協議、響應碼等一系列引數。
在標記5處可以看到原來OkHttp的快取策略使用的是DiskLruCache,DiskLruCache是用於磁碟快取的一套解決框架,OkHttp對DiskLruCache稍微做了點修改,並且OkHttp內部維護著清理記憶體的執行緒池,通過這個執行緒池完成快取的自動清理和管理工作,這裡不做過多介紹。
拿到DiskLruCache的Editor物件後,通過它的edit方法建立快取檔案,edit方法傳入的是快取的檔名,通過key方法將請求url進行MD5加密並獲取它的十六進位制表示形式。
接著執行Entry物件的writeTo方法並傳入Editor物件,writeTo方法的目的是將我們的快取資訊儲存在本地。
點進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'); 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'); //判斷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(); }
writeTo方法內部對Response的相關資訊進行快取,並判斷是否是https請求並快取Https相關資訊,從上面的writeTo方法中發現,返回的響應主體body並沒有在這裡進行快取,最後返回一個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(); } }; } }
在CacheRequestImpl類中有一個body物件,這個就是我們的響應主體。CacheRequestImpl實現了CacheRequest介面,用於暴露給快取攔截器,這樣的話快取攔截器就可以直接通過這個類來更新或寫入快取資料。
看完了put方法,繼續看get方法:
public final class Cache implements Closeable, Flushable { final InternalCache internalCache = new InternalCache() { ... @Override public Response get(Request request) throws IOException { return Cache.this.get(request); } ... }; ... }
檢視Cache的get方法:
@Nullable Response get(Request request) { //獲取快取的key String key = key(request.url()); //建立快照 DiskLruCache.Snapshot snapshot; Cache.Entry entry; try { //更加key從快取獲取 snapshot = cache.get(key); if (snapshot == null) { return null; } } catch (IOException e) { return null; } try { //從快照中獲取快取 entry = new Cache.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; }
get方法比較簡單,先是根據請求的url獲取快取key,建立snapshot目標快取中的快照,根據key獲取快照,當目標快取中沒有這個key對應的快照,說明沒有快取返回null;如果目標快取中有這個key對應的快照,那麼根據快照建立快取Entry物件,再從Entry中取出Response。
Entry的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(); //建立Response return new Response.Builder() .request(cacheRequest) .protocol(protocol) .code(code) .message(message) .headers(responseHeaders) .body(new Cache.CacheResponseBody(snapshot, contentType, contentLength)) .handshake(handshake) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(receivedResponseMillis) .build(); }
Entry的response方法中會根據頭部資訊建立快取請求,然後建立Response物件並返回。
回到get方法,接著判斷響應和請求是否成對出現,如果不是成對出現,關閉流並返回null,否則返回Response。
到這裡快取的get和put方法的整體流程已經介紹完畢,接下來介紹快取攔截器。
CacheInterceptor
進入CacheInterceptor的intercept方法,下面貼出部分重要的程式碼。
@Override public Response intercept(Interceptor.Chain chain) throws IOException { Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; ... }
第一步先嚐試從快取中獲取Response,這裡分兩種情況,要麼獲取快取Response,要麼cacheCandidate為null。
@Override public Response intercept(Interceptor.Chain chain) throws IOException { ... CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; ... }
第二步,獲取快取策略CacheStrategy物件,CacheStrategy內部維護了一個Request和一個Response,也就是說CacheStrategy能指定到底是通過網路還是快取,亦或是兩者同時使用獲取Response。
CacheStrategy內部工廠類Factory的get方法如下: public CacheStrategy get() { CacheStrategy candidate = getCandidate(); if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) { // We're forbidden from using the network and the cache is insufficient. return new CacheStrategy(null, null); } return candidate; }
方法中通過getCandidate方法獲取CacheStrategy物件,繼續點進去:
private CacheStrategy getCandidate() { //標記1:沒有快取Response if (cacheResponse == null) { return new CacheStrategy(request, null); } //標記2 if (request.isHttps() && cacheResponse.handshake() == null) { return new CacheStrategy(request, null); } ... CacheControl requestCaching = request.cacheControl(); //標記3 if (requestCaching.noCache() || hasConditions(request)) { return new CacheStrategy(request, null); } CacheControl responseCaching = cacheResponse.cacheControl(); //標記4 if (responseCaching.immutable()) { return new CacheStrategy(null, cacheResponse); } ... //標記5 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()); } ... }
標記1處,可以看到先對cacheResponse進行判斷,如果為空,說明沒有快取物件,這時建立CacheStrategy物件並且第二個引數Response傳入null。
標記2處,判斷請求是否是https請求,如果是https請求但沒有經過握手操作 ,建立CacheStrategy物件並且第二個引數Response傳入null。
標記3處,判斷如果不使用快取或者服務端資源改變,亦或者驗證服務端發過來的最後修改的時間戳,同樣建立CacheStrategy物件並且第二個引數Response傳入null。
標記4處,判斷快取是否受影響,如果不受影響,建立CacheStrategy物件時,第一個引數Request為null,第二個引數Response直接使用cacheResponse。
標記5處,根據一些資訊新增頭部資訊 ,最後建立CacheStrategy物件。
回到CacheInterceptor的intercept方法:
@Override public Response intercept(Chain chain) throws IOException { ... 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(); } ... }
第三步,判斷當前如果不能使用網路同時又沒有找到快取,這時會建立一個Response物件,code為504的錯誤。
@Override public Response intercept(Chain chain) throws IOException { ... if (networkRequest == null) { return cacheResponse.newBuilder() .cacheResponse(stripBody(cacheResponse)) .build(); } ... }
第四步,如果當前不能使用網路,就直接返回快取結果。
@Override public Response intercept(Chain chain) throws IOException { ... Response networkResponse = null; try { networkResponse = chain.proceed(networkRequest); } finally { ... } ... }
第五步,呼叫下一個攔截器進行網路請求。
@Override public Response intercept(Chain chain) throws IOException { ... 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,並且網路返回的Response的響應碼為304,代表從快取中獲取。
@Override public Response intercept(Chain chain) throws IOException { ... if (cache != null) { //標記1 if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) { CacheRequest cacheRequest = cache.put(response); return cacheWritingResponse(cacheRequest, response); } //標記2 if (HttpMethod.invalidatesCache(networkRequest.method())) { try { cache.remove(networkRequest); } catch (IOException ignored) { } } } return response; }
第七步,標記1判斷http頭部有沒有響應體,並且快取策略可以被快取的,滿足這兩個條件後,網路獲取的Response通過cache的put方法寫入到快取中,這樣下次取的時候就可以從快取中獲取;標記2處判斷請求方法是否是無效的請求方法,如果是的話,從快取池中刪除這個Request。最後返回Response給上一個攔截器。

838794-506ddad529df4cd4.webp.jpg
搜尋微信“顧林海”公眾號,定期推送優質文章。