在Http協議中,緩存的控制是經過首部的Cache-Control來控制,經過對Cache-Control進行設置,便可實現不一樣的緩存策略。html
Cache-Control和其餘的首部字段同樣,使用key:value結構,同時value可有多個值, 值之間以,分隔(具體參考HTTP詳解)。Cache-Control是一個通用首部字段,在Http請求報文中可以使用,也可在應答報文中使用。java
public: 可向任一方提供緩存數據;緩存
private: 只向指定用戶提供緩存數據;服務器
no-cache: 緩存前需確認其有效性;markdown
no-store: 不緩存請求或響應的任何內容;cookie
max-age: 表示緩存的最大時間,在此時間範圍內,訪問該資源時,直接返回緩存數據。不須要對資源的有效性進行確認;網絡
must-revalidate: 訪問緩存數據時,須要先向源服務器確認緩存數據是否有效,如沒法驗證其有效性,則需返回504。須要注意的是:若是使用此值,則max-stale將無效。ide
更詳細內容可參考:Http首部字段定義oop
瞭解了HTTP的理論知識,後面咱們對OkHttp中的緩存進行簡單的介紹。post
OkHttp默認對Http緩存進行了支持,只要服務端返回的Response中含有緩存策略,OkHttp就會經過CacheInterceptor攔截器對其進行緩存。可是OkHttp默認狀況下構造的HTTP請求中並無加Cache-Control,即使服務器支持了,咱們仍是不能正常使用緩存數據。因此須要對OkHttp的緩存過程進行干預,使其知足咱們的需求。
OkHttp的優雅之處就在於使用了責任鏈模式,將請求-應答過程當中的每一步都經過一個攔截器來實現,並對此過程的頭部和尾部都提供了擴展,這也爲咱們干預緩存過程提供了可能。因此在實現緩存以前,咱們須要對OkHttp對攔截器的處理過程有個大概的瞭解。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest, this, eventListener);
return chain.proceed(originalRequest);
}
複製代碼
以上代碼就是整個攔截器的處理過程,具體的流程可參考源碼,這裏咱們只說一下基本的流程:發起請求時,會按interceptors中加入的順序依次執行,返回Response時按照逆序執行:
自定義攔截器 <-> 內置攔截器(retryAndFollowUpInterceptor...ConnectInterceptor)
<-> 網絡攔截器 <-> CallServerInterceptor
複製代碼
其中CallServerInterceptor就是負責發送請求與接收應答的攔截器。因爲咱們關注的只是緩存,因此只考慮內置攔截器中的CacheInterceptor。那麼流程可簡化爲:
Request <-> 自定義攔截器 <-> CacheInterceptor <-> 網絡攔截器 <-> Response
複製代碼
從這個流程能夠看出,若是服務端返回的Response中沒有Cache-Control, 那麼咱們可經過添加網絡攔截器來實現。一樣,在訪問緩存數據時,咱們可經過添加自定義攔截器來實現。
在開始添加緩存策略以前,咱們先了解一個完整的緩存策略:
總體來講,在有網絡的狀況下,使用緩存仍是比較複雜,這裏咱們經過簡化版的緩存策略(有網絡時訪問服務器,無網絡時返回緩存數據)來演示OkHttp使用緩存的過程。
首先,咱們經過定義一個網絡攔截器來爲Response添加緩存策略:
public class HttpCacheInterceptor implements Interceptor {
private Context context;
public HttpCacheInterceptor(Context context) {
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
return chain.proceed(chain.request()).newBuilder()
.request(newRequest)
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + 1)
.build();
}
}
複製代碼
其次,經過自定義攔截器設置Request使用緩存的策略:
public class BaseInterceptor implements Interceptor {
private Context mContext;
public BaseInterceptor(Context context) {
this.mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
if (NetworkUtil.isConnected(mContext)) {
return chain.proceed(chain.request());
} else { // 若是沒有網絡,則返回緩存未過時一個月的數據
Request newRequest = chain.request().newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
return chain.proceed(newRequest);
}
}
}
複製代碼
Pragma是Http/1.1以前版本遺留的字段,用於作版本兼容,但不一樣的平臺對此有不一樣的實現,因此在使用緩存策略時須要將其屏蔽,避免對緩存策略形成影響。
將對修改Request和Response緩存策略的攔截器應用於OkHttp:
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(new BaseInterceptor(context))
.addNetworkInterceptor(new HttpCacheInterceptor(context))
.cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 設置緩存路徑和緩存容量
.build();
複製代碼
接下來就能夠在無網絡的狀況下愉快地使用緩存數據了。
若是以爲OkHttp的緩存太複雜,想本身來緩存數據怎麼辦呢?有兩種方案來實現:
這種方案首先須要考慮應使用普通的攔截器仍是網絡攔截器,上面咱們已經瞭解了整個請求過程當中攔截器的執行順序,須要注意的是:在無網絡的狀況下,請求在執行到CacheIntercepter,若是沒有緩存數據,將會直接返回,並不會執行到自定義的網絡攔截器中,因此不適合在網絡攔截器中緩存數據。那麼咱們可經過自定義普通攔截器來實現,基本的過程以下:
@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {
Response response = null;
if (NetworkUtil.isConnected(mContext)) {
response = chain.proceed(newRequest);
saveCacheData(response); // 保存緩存數據
} else { // 不執行chain.proceed會打斷責任鏈,即後面的攔截器不會被執行
response = getCacheData(chain.request().url()); // 獲取緩存數據
}
return response;
}
複製代碼
OkHttp: 使用這種方案你良心不會痛嗎?
這種方案能夠說摒棄了OkHttp擴展攔截器這一強大的功能,直接與請求和應答進行交互,基本的過程以下:
Request request = new Request.Builder()
.url(realUrl)
.build();
if (NetworkUtil.isConnected()) {
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
// 返回緩存數據
}
@Override
public void onResponse(Response response) throws IOException {
// 1. 緩存數據
// 2. 返回請求結果
}
});
} else {
// 返回緩存數據
}
複製代碼
這兩種方案都拋棄了OkHttp本身實現的緩存策略,因此更加靈活,尤爲是監聽OkHttp請求過程這種方法。但也都有一個很大的缺點:須要實現一個緩存模塊。在開發中具體使用哪一種緩存策略,根據已有代碼模塊和需求衡量便可。
對Response的緩存策略進行修改的攔截器必定要應用於網絡攔截器,不然沒法緩存數據,由於在Response返回的過程當中,普通的攔截器在內置的CacheInterceptor以後執行;
修改Response的Cache-Control時,max-Age不能太大,不然你將在指定的max-Age時間內訪問的始終是緩存數據(即使是有網的狀況下);
實際的開發過程當中,咱們在網絡請求中會添加一些公共參數,對於一些可變的公共參數,在緩存數據和訪問緩存數據的過程當中須要刪除,好比網絡類型,有網絡時其值爲Wifi或4G等,無網絡時可能爲none, 這時訪問緩存時就會因url不一致致使訪問緩存失敗。
// BaseInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
// 添加公共參數
HttpUrl.Builder urlBuilder = chain.request().url().newBuilder()
.addQueryParameter("a", "a")
.addQueryParameter("b", "b");
Request.Builder requestBuilder = chain.request().newBuilder();
if (NetworkUtil.isConnected(mContext)) {
urlBuilder.addQueryParameter("network", NetworkUtil.getNetwokType(mContext));
} else { // 無網絡時不添加可變的公共參數
requestBuilder.removeHeader("Pragma")
.header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
}
Request newRequest = requestBuilder
.url(urlBuilder.build())
.build();
return chain.proceed(newRequest);
}
// HttpCacheInterceptor.java
@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
HttpUrl newUrl = chain.request().url().newBuilder()
.removeAllQueryParameters("network")
.build(); // 緩存數據前刪除可變的公共參數
Request newRequest = chain.request().newBuilder()
.url(newUrl)
.build();
return response.newBuilder()
.request(newRequest)
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + 1)
.build();
}
複製代碼