OkHttp快取優化應用
OkHttp快取優化你的應用
Okhttp快取原理
我們先從HTTP協議開始入手,關於快取的HTTP請求/返回頭由以下幾個,我列了張表格一一解釋
請求頭/返回頭 | 含義 |
---|---|
Cache-Control | 這個欄位用於指定所有快取機制在整個請求/響應鏈中必須服從的指令。 |
Pragma | 與Cache-Control一樣,是相容HTTP1.0的頭部 |
Expires | 資源過期時間 |
Last-Modified | 資源最後修改的時間 |
If-Modified-Since | 在請求頭中指定一個日期,若資源最後更新時間超過該日期, 則伺服器接受請求,相反的頭為If-Unmodified-Since |
ETag | 識別內容版本的 唯一 字串,與資源關聯的記號 |
與快取最相關的Cache-Control有多條指令,並且在請求或返回頭中的效果不一樣
在請求頭中Cache-Control的指令
指令 | 引數 | 說明 |
---|---|---|
no-cache | 無 | 快取必須向伺服器確認是否過期候才能使用,即不接受過期快取, 並非不快取 |
no-store | 無 | 真正意義上的不快取 |
max-age=[秒] | 必須 | 響應的最大age值 |
max-stale=[秒] | 可忽略 | 可接受的最大過期時間 |
min-fresh=[秒] | 必須 | 詢問再過[秒]時間後資源是否過期,若過期則不返回 |
only-if-cached | 無 | 只獲取快取的資源而不聯網獲取 |
在返回頭中Cache-Control的指令
指令 | 引數 | 說明 |
---|---|---|
public | 無 | 可向任意方提供響應的快取 |
private | 無 | 向特定使用者提供響應快取 |
no-cache | 可省略 | 不快取 |
no-store | 無 | 不快取 |
max-age=[秒] | 必須 | 響應的最大age值 |
max-stale=[秒] | 可忽略 | 可接受的最大過期時間 |
min-fresh=[秒] | 必須 | 詢問再過[秒]時間後資源是否過期,若過期則不返回 |
only-if-cached | 無 | 只獲取快取的資源而不聯網獲取 |
假設Okhttp完全遵守HTTP協議(實際上應該也是),利用Cache-Control我們可以快取某些必要的資源.
1.有網路的時候:短時間內頻繁的請求,後面的請求使用快取中的資源.
2.無網路的時候:獲取之前快取的資料進行暫時的頁面顯示,當網路更新時對當前activity的資料進行重新整理,重新整理介面,避免介面空白的場景.
class CacheNetworkInterceptor implements Interceptor { public Response intercept(Interceptor.Chain chain) throws IOException { //無快取,進行快取 return chain.proceed(chain.request()).newBuilder() .removeHeader("Pragma") //對請求進行最大60秒的快取 .addHeader("Cache-Control", "max-age=60") .build(); } } static class CacheInterceptor implements Interceptor { public Response intercept(Interceptor.Chain chain) throws IOException { Response resp; Request req; if (ok) { //有網路,檢查10秒內的快取 req = chain.request() .newBuilder() .cacheControl(new CacheControl .Builder() .maxAge(10, TimeUnit.SECONDS) .build()) .build(); } else { //無網路,檢查30天內的快取,即使是過期的快取 req = chain.request().newBuilder() .cacheControl(new CacheControl.Builder() .onlyIfCached() .maxStale(30, TimeUnit.SECONDS) .build()) .build(); } resp = chain.proceed(req); return resp.newBuilder().build(); } } int cacheSize = 10 * 1024 * 1024; // 10 MiB Cache cache = new Cache(httpCacheDirectory, cacheSize); OkHttpClient client = new OkHttpClient.Builder() .cache(cache) //加入攔截器,注意Network與非Network的區別 .addInterceptor(new CacheInterceptor()) .addNetworkInterceptor(new CacheNetworkInterceptor()) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); //最後通過使用該HTTP Client進行網路請求, 就實現上述需求
OKHTTP關於Cache的原始碼分析如下
Response getResponseWithInterceptorChain() throws IOException { // Okhttp獲取Response的入口 // 採用責任鏈模式,一層層按順序轉交Request並處理Response List<Interceptor> interceptors = new ArrayList<>(); // 使用者定義的攔截器 interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor(client.cookieJar())); //CacheInterceptor主要用於做快取控制 interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); if (!forSocket/">WebSocket) { //使用者定義的Network攔截器 interceptors.addAll(client.networkInterceptors()); } // 發起實際請求的攔截器 interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); return chain.proceed(originalRequest); }
這裡我們主要看CacheInterceptor的實現
CacheInterceptor程式碼比較長,我們分段來解釋
@Override public Response intercept(Chain chain) throws IOException { Response cacheCandidate = cache != null ? cache.get(chain.request()) : null; // 實際上是類似map,將返回內容的URL的MD5的值當key,返回內容當response // 然後從cache檔案裡面查詢是否存在該快取 long now = System.currentTimeMillis(); //根據當前的時間,以及快取策略,來獲取response CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); Request networkRequest = strategy.networkRequest; Response cacheResponse = strategy.cacheResponse; // 根據策略得到cacheReposne 與 NetworkRequest // 之後的程式碼就是根據這兩個東西設定返回頭 // 不進行網路請求,且快取以及過期了,返回504錯誤 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 Response networkResponse = null; try { networkResponse = chain.proceed(networkRequest); } finally { // 請求異常,關閉快取避免洩漏 if (networkResponse == null && cacheCandidate != null) { closeQuietly(cacheCandidate.body()); } } // 請求了網路的同時,快取其實也找到的情況 // (比如 需要向伺服器確認快取是否可用的情況) if (cacheResponse != null) { // 返回了304, 我們都知道304的返回時不帶body的,此時必須向獲取cache的body 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(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). cache.trackConditionalCacheHit(); cache.update(cacheResponse, response); return response; } else { closeQuietly(cacheResponse.body()); } } //省略--------- }
// 快取策略CacheStrategy主要的策略寫在該方法下 private CacheStrategy getCandidate() { // 沒有快取! if (cacheResponse == null) { return new CacheStrategy(request, null); } // 當請求的協議是https的時候,如果cache沒有hansake就丟棄快取 if (request.isHttps() && cacheResponse.handshake() == null) { return new CacheStrategy(request, null); } /// -- 省略一些程式碼 // 根據快取的快取時間,快取可接受最大過期時間等等HTTP協議上的規範 // 來判斷快取是否可用, 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()); } }
借用一張圖來說明http的整個工作流程

image
最後附上當網路可用的時候,自動重新請求的一個基於MVP模式的實現方案
NetStatusMonitor是一個單例,用於監聽整個應用程式的網路狀態
ActivityManager也是一個單例,用來管理應用程式的活動棧
基於MVP模式,給presenter的抽象基類定義一個refresh的方法
如有不足請各位大佬指正
NetStatusMonitor.setNetStatusListener(object: NetStatusMonitor.Listener { var lostTime = 0L override fun onLost() { lostTime = System.currentTimeMillis() } override fun onAvailable() { with(ActivityManager.peek() as BaseView<*>){ //當棧頂活動位於前臺 if(this.lifecycle.currentState == Lifecycle.State.RESUMED){ // 獲取ForegroundActivity進行重新整理 // 斷線時間超過30秒重連再重新整理一次 if(System.currentTimeMillis() - lostTime > 1000 * 30){ // 通知presenter重新整理資料 this.presenter.refresh() } } } } override fun onNetStateChange(oldState: Int, newState: Int) { if(newState == NetStatusMonitor.MOBILE){ showToast("正在使用行動網路") } } })