OkHttp緩存使用指南

HTTP緩存

在Http協議中,緩存的控制是經過首部的Cache-Control來控制,經過對Cache-Control進行設置,便可實現不一樣的緩存策略。html

Cache-Control和其餘的首部字段同樣,使用key:value結構,同時value可有多個值, 值之間以,分隔(具體參考HTTP詳解)。Cache-Control是一個通用首部字段,在Http請求報文中可以使用,也可在應答報文中使用。java

請求指令集(在請求報文中的取值):

  • no-cache: 不要緩存數據,直接從源服務器獲取數據;
  • no-store: 不緩存請求或響應的任何內容;
  • max-age: 表示可接受過時太久的緩存數據,同指定了參數的max-stale;
  • max-stale: 表示接收過時的緩存,如後面未指定參數,則表示永遠接收緩存數據。如max-stale: 3600, 表示可接受過時1小時內的數據;
  • min-fresh: 表示指定時間內的緩存數據仍有效,與緩存是否過時無關。如min-fresh: 60, 表示60s內的緩存數據都有效,60s以後的緩存數據將無效。
  • only-if-cache: 表示直接獲取緩存數據,若沒有數據返回,則返回504(Gateway Timeout)

應答指令集(在應答報文中的取值):

  • public: 可向任一方提供緩存數據;
  • private: 只向指定用戶提供緩存數據;
  • no-cache: 緩存前需確認其有效性;
  • no-store: 不緩存請求或響應的任何內容;
  • max-age: 表示緩存的最大時間,在此時間範圍內,訪問該資源時,直接返回緩存數據。不須要對資源的有效性進行確認;
  • must-revalidate: 訪問緩存數據時,須要先向源服務器確認緩存數據是否有效,如沒法驗證其有效性,則需返回504。須要注意的是:若是使用此值,則max-stale將無效。

更詳細內容可參考:Http首部字段定義segmentfault

瞭解了HTTP的理論知識,後面咱們對OkHttp中的緩存進行簡單的介紹。緩存

OkHttp攔截器

OkHttp默認對Http緩存進行了支持,只要服務端返回的Response中含有緩存策略,OkHttp就會經過CacheInterceptor攔截器對其進行緩存。可是OkHttp默認狀況下構造的HTTP請求中並無加Cache-Control,即使服務器支持了,咱們仍是不能正常使用緩存數據。因此須要對OkHttp的緩存過程進行干預,使其知足咱們的需求。服務器

OkHttp的優雅之處就在於使用了責任鏈模式,將請求-應答過程當中的每一步都經過一個攔截器來實現,並對此過程的頭部和尾部都提供了擴展,這也爲咱們干預緩存過程提供了可能。因此在實現緩存以前,咱們須要對OkHttp對攔截器的處理過程有個大概的瞭解。cookie

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。那麼流程可簡化爲:ide

Request <-> 自定義攔截器 <-> CacheInterceptor <-> 網絡攔截器 <-> Response

從這個流程能夠看出,若是服務端返回的Response中沒有Cache-Control, 那麼咱們可經過添加網絡攔截器來實現。一樣,在訪問緩存數據時,咱們可經過添加自定義攔截器來實現。ui

使用OkHttp緩存

在開始添加緩存策略以前,咱們先了解一個完整的緩存策略:
imagethis

總體來講,在有網絡的狀況下,使用緩存仍是比較複雜,這裏咱們經過簡化版的緩存策略(有網絡時訪問服務器,無網絡時返回緩存數據)來演示OkHttp使用緩存的過程。

image

首先,咱們經過定義一個網絡攔截器來爲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();

        return response;
    }
}

其次,經過自定義攔截器設置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的緩存

若是以爲OkHttp的緩存太複雜,想本身來緩存數據怎麼辦呢?有兩種方案來實現:

  • 自定義攔截器,
  • 監聽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: 使用這種方案你良心不會痛嗎?

這種方案能夠說摒棄了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請求過程這種方法。但也都有一個很大的缺點:須要實現一個緩存模塊。在開發中具體使用哪一種緩存策略,根據已有代碼模塊和需求衡量便可。

注意點

  1. 對Response的緩存策略進行修改的攔截器必定要應用於網絡攔截器,不然沒法緩存數據,由於在Response返回的過程當中,普通的攔截器在內置的CacheInterceptor以後執行;
  2. 修改Response的Cache-Control時,max-Age不能太大,不然你將在指定的max-Age時間內訪問的始終是緩存數據(即使是有網的狀況下);
  3. 實際的開發過程當中,咱們在網絡請求中會添加一些公共參數,對於一些可變的公共參數,在緩存數據和訪問緩存數據的過程當中須要刪除,好比網絡類型,有網絡時其值爲Wifi或4G等,無網絡時可能爲none, 這時訪問緩存時就會因url不一致致使訪問緩存失敗。
@Override // BaseInterceptor.java
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);
}


@Override // HttpCacheInterceptor.java
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();
}
相關文章
相關標籤/搜索