眾所周知,在Android開發中我們經常要和服務端進行交互,通過網絡請求獲取我們客戶端所需要的數據,進而將這些數據通過頁面呈現給用戶,存在一種情況就是我們在一段時間內重復發送同一個接口的網絡請求獲得的數據可能是內容一致的。隨著現在app的頁面效果越來越絢麗,內容越來越豐富,網絡請求的數量和數據越來越多,由此而導致的流量問題和頁面不流暢等問題隨之越來越被重視,因此如何解決上述問題呢?
解決上述問題就是利用http的緩存機制,把重復的網絡請求的數據緩存在本地,并設置超時時間,這樣在規定的時間內客戶端不再向遠程請求數據,而是直接從本地緩存中獲取數據,從而提高App的體驗和節省帶寬。
上面提到了http的緩存,那么什么是http的緩存呢?本文還將以OKHTTP為例講解如何配置緩存。
為了更好的講解OKHTTP怎么設置緩存,我們追根溯源先從瀏覽器的緩存說起,這樣后面的OKHTTP緩存內容自然更加好理解。
我這部分內容也是經網絡上查閱,這一篇寫得很詳細瀏覽器 HTTP 協議緩存機制詳解。以下內容基本出自于此文章。
http請求有服務端和客戶端之分。因此緩存也可以分為兩個類型服務端側和客戶端側。
常見的服務端有Ngix和Apache。服務端緩存又分為代理服務器緩存和反向代理服務器緩存。常見的CDN就是服務器緩存。這個好理解,當瀏覽器重復訪問一張圖片地址時,CDN會判斷這個請求有沒有緩存,如果有的話就直接返回這個緩存的請求回復,而不再需要讓請求到達真正的服務地址,這么做的目的是減輕服務端的運算壓力。
客戶端主要指瀏覽器(如IE、Chrome等),當然包括我們的OKHTTPClient.客戶端第一次請求網絡時,服務器返回回復信息。如果數據正常的話,客戶端緩存在本地的緩存目錄。當客戶端再次訪問同一個地址時,客戶端會檢測本地有沒有緩存,如果有緩存的話,數據是有沒有過期,如果沒有過期的話則直接運用緩存內容。
而我們講的就是客戶端的緩存。
Cache-Control是什么呢?先別急, 我們先用觀察一個結果,用Fiddler監聽瀏覽器訪問http://blog.csdn.net/briblue。如下圖:
然后,查看服務端返回來的信息如下:
可以看到頭信息中有這么一行:
Cache-Control: private
Cache-control是由服務器返回的Response中添加的頭信息,它的目的是告訴客戶端是要從本地讀取緩存還是直接從服務器摘取消息。它有不同的值,每一個值有不同的作用。
max-age:這個參數告訴瀏覽器將頁面緩存多長時間,超過這個時間后才再次向服務器發起請求檢查頁面是否有更新。對于靜態的頁面,比如圖片、CSS、javascript,一般都不大變更,因此通常我們將存儲這些內容的時間設置為較長的時間,這樣瀏覽器會不會向瀏覽器反復發起請求,也不會去檢查是否更新了。s-maxage:這個參數告訴緩存服務器(proxy,如Squid)的緩存頁面的時間。如果不單獨指定,緩存服務器將使用max-age。對于動態內容(比如文檔的查看頁面),我們可告訴瀏覽器很快就過時了(max-age=0),并告訴緩存服務器(Squid)保留內容一段時間(比如,s-maxage=7200)。一旦我們更新文檔,我們將告訴Squid清除老的緩存版本。must-revalidate:這告訴瀏覽器,一旦緩存的內容過期,一定要向服務器詢問是否有新版本。proxy-revalidate:proxy上的緩存一旦過期,一定要向服務器詢問是否有新版本。no-cache:不做緩存。no-store:數據不在硬盤中臨時保存,這對需要保密的內容比較重要。public:告訴緩存服務器, 即便是對于不該緩存的內容也緩存起來,比如當用戶已經認證的時候。所有的靜態內容(圖片、Javascript、CSS等)應該是public的。private:告訴proxy不要緩存,但是瀏覽器可使用private cache進行緩存。一般登錄后的個性化頁面是private的。no-transform: 告訴proxy不進行轉換,比如告訴手機瀏覽器不要下載某些圖片。max-stale指示客戶機可以接收超出超時期間的響應消息。如果指定max-stale消息的值,那么客戶機可以接收超出超時期指定值之內的響應消息。我們上面的例子是Cache-Control:private。說明服務器希望客戶端不要緩存消息,但是可以進行private cache方法進行緩存。這是因為與用戶系統相關,所以為了安全起見,建議用private cache的方式緩存。
在OKHttp開發中我們常見到的有下面幾個: * max-age * no-cache * max-stale
expires的效果等同于Cache-Control,不過它是Http 1.0的內容,它的作用是告訴瀏覽器緩存的過期時間,在此時間內瀏覽器不需要直接訪問服務器地址直接用緩存內容就好了。expires最大的問題在于如果服務器時間和本地瀏覽器相差過大的問題。那樣誤差就很大。所以基本上用Cache-Control:max-age=多少秒的形式代替。
這個需要配合Cache-Control使用
Last-Modified:標示這個響應資源的最后修改時間。web服務器在響應請求時,告訴瀏覽器資源的最后修改時間。
If-Modified-Since:當資源過期時(使用Cache-Control標識的max-age),發現資源具有Last-Modified聲明,則再次向web服務器請求時帶上頭 If-Modified-Since,表示請求時間。web服務器收到請求后發現有頭If-Modified-Since 則與被請求資源的最后修改時間進行比對。若最后修改時間較新,說明資源又被改動過,則響應整片資源內容(寫在響應消息包體內),HTTP 200;若最后修改時間較舊,說明資源無新修改,則響應HTTP 304 (無需包體,節省瀏覽),告知瀏覽器繼續使用所保存的cache。
這個也需要配合Cache-Control使用
Etag對應請求的資源在服務器中的唯一標識(具體規則由服務器決定),比如一張圖片,它在服務器中的標識為ETag: W/”ACXbWXd1n0CGMtAd65PcoA==”。
If-None-Match 如果瀏覽器在Cache-Control:max-age=60設置的時間超時后,發現消息頭中還設置了Etag值。然后,瀏覽器會再次向服務器請求數據并添加In-None-Match消息頭,它的值就是之前Etag值。服務器通過Etag來定位資源文件,根據它是否更新的情況給瀏覽器返回200或者是304。
Etag機制比Last-Modified精確度更高,如果兩者同時設置的話,Etag優先級更高。
Pragma頭域用來包含實現特定的指令,最常用的是Pragma:no-cache。
在HTTP/1.1協議中,它的含義和Cache- Control:no-cache相同。
以上是Http中關于緩存的相關信息。接下來我們進入主題,如何配置OkHttp的緩存。
OKHTTP如果要設置緩存,首要的條件就是設置一個緩存文件夾,在Android中為了安全起見,一般設置為私密數據空間。通過getExternalCacheDir()獲取。如然后通過調用OKHttpClient.Builder中的cache()方法。如下面代碼所示:
//緩存文件夾File cacheFile = new File(getExternalCacheDir().toString(),"cache");//緩存大小為10Mint cacheSize = 10 * 1024 * 1024;//創建緩存對象Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build();
設置好Cache我們就可以正常訪問了。我們可以通過獲取到的Response對象拿到它正常的消息和緩存的消息。
Response的消息有兩種類型,CacheResponse和NetworkResponse。CacheResponse代表從緩存取到的消息,NetworkResponse代表直接從服務端返回的消息。示例代碼如下:
private void testCache(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize); new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //官方的一個示例的url String url = "http://publicobject.com/helloworld.txt"; Request request = new Request.Builder() .url(url) .build(); Call call1 = client.newCall(request); Response response1 = null; try { //第一次網絡請求 response1 = call1.execute(); Log.i(TAG, "testCache: response1 :"+response1.body().string()); Log.i(TAG, "testCache: response1 cache :"+response1.cacheResponse()); Log.i(TAG, "testCache: response1 network :"+response1.networkResponse()); response1.body().close(); } catch (IOException e) { e.printStackTrace(); } Call call12 = client.newCall(request); try { //第二次網絡請求 Response response2 = call12.execute(); Log.i(TAG, "testCache: response2 :"+response2.body().string()); Log.i(TAG, "testCache: response2 cache :"+response2.cacheResponse()); Log.i(TAG, "testCache: response2 network :"+response2.networkResponse()); Log.i(TAG, "testCache: response1 equals response2:"+response2.equals(response1)); response2.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }我們在上面的代碼中,用同一個url地址分別進行了兩次網絡訪問,然后分別用Log打印它們的信息。
class CacheInterceptor implements Interceptor{ @Override public Response intercept(Chain chain) throws IOException { Response originResponse = chain.proceed(chain.request()); //設置緩存時間為60秒,并移除了pragma消息頭,移除它的原因是因為pragma也是控制緩存的一個消息頭屬性 return originResponse.newBuilder().removeHeader("pragma") .header("Cache-Control","max-age=60").build(); } }定義好攔截器中后,我們可以添加到OKHttpClient中了。
private void testCacheInterceptor(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize); OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new CacheInterceptor()) .cache(cache) .build(); .......}代碼后面部分有省略。主要通過在OkHttpClient.Builder()中addNetworkInterceptor()中添加。而這樣也挺簡單的,就幾步完成了緩存代碼。
網上有人說用攔截器進行緩存是野路子,是HOOK行為。這個我不大同意,前面我有分析過情況,如果客戶端能夠同服務端一起協商開發,當然以服務器控制的緩存消息頭為準,但問題在于你沒法這樣做。所以,能夠解決問題才是最實在的。
好了,回到正題。用攔截器控制緩存有什么不好的地方呢?我們先看看下面的情況。 1. 網絡訪問請求的資源是文本信息,如新聞列表,這類信息經常變動,一天更新好幾次,它們用的緩存時間應該就很短。 2. 網絡訪問請求的資源是圖片或者視頻,它們變動很少,或者是長期不變動,那么它們用的緩存時間就應該很長。
那么,問題來了。 因為OKHTTP開發建議是同一個APP,用同一個OKHTTPCLIENT對象這是為了只有一個緩存文件訪問入口。這個很容易理解,單例模式嘛。但是問題攔截器是在OKHttpClient.Builder當中添加的。如果在攔截器中定義緩存的方法會導致圖片的緩存和新聞列表的緩存時間是一樣的,這顯然是不合理的,這屬于一刀切,就像這兩天專家說的要把年收入12萬元的人群劃分為高收入人群而不區別北上廣深的房價物價情況。真實的情況不應該是圖片請求有它的緩存時間,新聞列表請求有它的緩存時間,應該是每一個Request有它的緩存時間。那么,有解決的方案嗎? 有的,okhttp官方有建議的方法。
okhttp中建議用CacheControl這個類來進行緩存策略的制定。 它內部有兩個很重要的靜態實例。
/**強制使用網絡請求*/public static final CacheControl FORCE_NETWORK = new Builder().noCache().build(); /** * 強制性使用本地緩存,如果本地緩存不滿足條件,則會返回code為504 */ public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build();我們看到FORCE_NETWORK常量用來強制使用網絡請求。FORCE_CACHE只取本地的緩存。它們本身都是CacheControl對象,由內部的Buidler對象構造。下面我們來看看CacheControl.Builder
它有如下方法:
- noCache();//不使用緩存,用網絡請求- noStore();//不使用緩存,也不存儲緩存- onlyIfCached();//只使用緩存- noTransform();//禁止轉碼- maxAge(10, TimeUnit.MILLISECONDS);//設置超時時間為10ms。- maxStale(10, TimeUnit.SECONDS);//超時之外的超時時間為10s- minFresh(10, TimeUnit.SECONDS);//超時時間為當前時間加上10秒鐘。知道了CacheControl的相關信息,那么它怎么使用呢?不同于攔截器設置緩存,CacheControl是針對Request的,所以它可以針對每個請求設置不同的緩存策略。比如圖片和新聞列表。下面代碼展示如何用CacheControl設置一個60秒的超時時間。
private void testCacheControl(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize); new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //設置緩存時間為60秒 CacheControl cacheControl = new CacheControl.Builder() .maxAge(60, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(cacheControl) .build(); try { Response response = client.newCall(request).execute(); response.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); }前面有講CacheControl.FORCE_CACHE這個常量。
public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build();
它內部其實就是調用onlyIfCached()和maxStale方法。 它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_CACHE) .build();但是如前面后提到的,如果緩存不符合條件會返回504.這個時候我們要根據情況再進行編碼,如緩存不行就再進行一次網絡請求。
Response forceCacheResponse = client.newCall(request).execute(); if (forceCacheResponse.code() != 504) { // 資源已經緩存了,可以直接使用 } else { // 資源沒有緩存,或者是緩存不符合條件了。 };前面也有講CacheControl.FORCE_NETWORK這個常量。
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();它的內部其實是調用noCache()方法,也就是不緩存的意思。 它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_NETWORK) .build();
還有一種情況將maxAge設置為0,也不會取緩存,直接走網絡。
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(new CacheControl.Builder() .maxAge(0, TimeUnit.SECONDS)) .build();
本文其實內容不多,前面講了很多http協議下的緩存機制,我認為是值得的,知道了Cache-Control這些定義,才能更好的懂得OKHTTP中的緩存設置。能夠明白為什么它要這樣做,為什么它可以這樣做。最后歸納下要點
http協議下Cache-Control等消息頭的作用okhttp如何用攔截器添加Cache-Control消息頭進行緩存定制okhttp如何用CacheControl進行緩存的控制。http://www.cnblogs.com/l1pe1/archive/2010/07/14/1777621.html
http://www.cnblogs.com/whoislcj/p/5537640.html
新聞熱點
疑難解答