okhttp 快取實踐

SheHuan發表於2018-05-16

以下內容基於 okhttp:3.10.0 版本

在開發中,由於不同業務場景解,我們需要將介面返回的資料快取到本地,以實現複用。例如,介面資料每間隔一定時間才會更新,在時間間隔內就沒必要重複的向伺服器請求資料,直接使用快取即可;當 app 無法訪問網路時,也可以使用快取的介面資料,避免預設頁等等。所以使用快取也是好處多多:節省流量、提高響應速度、增強使用者體驗......

okhttp 的快取功能使用起來也比較簡單,我們一步步來看:

1、配置快取

配置快取首先要指定快取目錄和快取大小,這兩個可以根據專案的需求來確定,然後使用 OkHttpClient..Builder()cache()方法來配置快取物件。這裡的OkHttpClient是一個單例,保證了只有一個快取快取目錄的入口。配置程式碼如下:

public class OkHttpManager {
    private OkHttpClient client;

    private OkHttpManager() {
        // 快取目錄
        File file = new File(Environment.getExternalStorageDirectory(), "a_cache");
        // 快取大小
        int cacheSize = 10 * 1024 * 1024;
        client = new OkHttpClient.Builder()
                .cache(new Cache(file, cacheSize)) // 配置快取
                .build();
    }

    public static OkHttpManager getInstance() {
        return OkHttpHolder.instance;
    }

    private static class OkHttpHolder {
        private static final OkHttpManager instance = new OkHttpManager();
    }
    ......
}
複製程式碼

到這裡就完成了基本配置工作,不要忘了處理許可權問題,因為快取功能需要儲存空間的讀寫許可權

如果客戶端和服務端已經協商好了,在介面的響應包含合適的Cache-Control響應頭,表示快取的策略,例如Cache-Control:max-age=60,表示快取的有效期為60秒。

這個響應頭是實現快取的一個重點,如果包含合適的Cache-Control響應頭,在無論網路連線是否正常的情況下請求介面資料,如果在快取有效期內則直接從快取讀取資料,超過有效期會重新請求介面資料。

列舉幾個常用的Cache-control響應頭的可選值:

  • must-revalidate,一旦快取過期,必須向伺服器重新請求,不得使用過期內容
  • no-cache,不使用快取
  • no-store,不快取請求的響應
  • no-transform,不得對響應進行轉換或轉變
  • public,任何響應都可以被快取,即使響應預設是不可快取或僅私有快取可存的
  • private,表明響應只能被單個使用者快取,不能作為共享快取(即代理伺服器不能快取它)
  • proxy-revalidate,與must-revalidate類似,但它僅適用於共享快取(例如代理),並被私有快取忽略。
  • max-age,快取的有效時間
  • s-maxage,指定響應在公共快取中的最大存活時間,它覆蓋max-age和expires欄位。

所以目前的問題是,如果響應不包含合適的Cache-Control響應頭,該如何處理,這也是接下來主要討論的問題。

2、攔截器

由於客戶端和服務端不是同一團隊,或者客戶端使用了第三方介面等原因,無法進行協商,導致介面的響應沒有合適的Cache-Control響應頭,或者快取已被禁用。這種情況下要讓快取功能正常工作,就需要使用自定義攔截器了,通過攔截器給請求的響應(Response)新增合適的Cache-Control響應頭即可,這樣問題就得到了解決!

不瞭解 okhttp 攔截器的可以先看官網的文件,很詳細:github.com/square/okht…

看一下攔截器如何實現:

public class NetCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response originResponse = chain.proceed(request);

        //設定響應的快取時間為60秒,即設定Cache-Control頭,並移除pragma訊息頭,因為pragma也是控制快取的一個訊息頭屬性
        originResponse = originResponse.newBuilder()
                .removeHeader("pragma")
                .header("Cache-Control", "max-age=60")
                .build();

        return originResponse;
    }

複製程式碼

攔截請求的響應,先移除pragma,然後手動設定Cache-Control響應頭。

把定義好的攔截器新增到OkHttpClient中:

client = new OkHttpClient.Builder()
                .cache(new Cache(file, cacheSize))
                .addNetworkInterceptor(new NetCacheInterceptor())
                .build();
複製程式碼

3、測試

封裝一個asyncGet()方法來實現非同步get請求,publicobject.com/helloworld.…是官方的一個例項地址:

public class OkHttpManager {
    private OkHttpClient client;
    ......
    public void asyncGet(Callback callback) {
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        client.newCall(request).enqueue(callback);
    }
}
複製程式碼

以下是發起請求、以及回撥的程式碼:

OkHttpManager.getInstance().asyncGet(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    Log.e("failure", e.toString());
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    if (response.isSuccessful()) {
                        if (response.networkResponse() != null) {
                            Log.e("network", response.body().string().length() + "");
                        } else if (response.cacheResponse() != null) {
                            if (Utils.isNetworkAvailable(context)) {
                                Log.e("cache", response.body().string().length() + "");
                            } else {
                                Log.e("cache(no network)", response.body().string().length() + "");
                            }
                        }
                    }
                }
            });
複製程式碼

如果響應是從網路請求得到的,那麼response.networkResponse()不為空,如果是從快取中得到的response.cacheResponse()不為空,以此來列印不同 log 觀察快取功能是否能正常工作,這裡列印了響應的 boay 長度。

下邊是在60秒內發起了若干次請求,即便斷開網路連線也能正常的從快取讀取資料,超過60秒會重新請求資料,這也驗證了我們的快取功能可以正常工作了:

okhttp 快取實踐
快取功能的具體實現是通過 DiskLruCache 完成的,在之前配置的快取目錄可以找到對應的快取檔案:

okhttp 快取實踐

4、okhttp 快取策略

上邊在攔截器中統一設定了響應的快取時間,導致所有的介面資料都會快取,且時間相同。這樣問題就來了,可能不同介面對資料的快取時間要求不同,或者有些介面並不需要快取資料。要解決這個問題可以在攔截器中根據請求的地址(request.url().toString())來決定如何設定響應的快取時間,但不夠優雅!除此之外可以使用 okhttp 的快取策略類CacheControl來處理。

CacheControl類提供瞭如下兩個預設的快取策略:

  • CacheControl.FORCE_NETWORK,即強制使用網路請求
  • CacheControl.FORCE_CACHE,即強制使用本地快取,如果無可用快取則返回一個code為504的響應

根據預設快取策略的實現方式,我們可以通過CacheControl.Builder()定製自己的快取策略,可選的設定方法如下:

  • noCache(),不使用快取,使用網路請求
  • noStore(),不使用快取也不儲存快取資料
  • maxAge(),快取的有效時間,超過該時間會重新請求資料
  • maxStale(),超過快取有效時間後,可繼續使用舊快取的時間,之後需要重新請求資料
  • minFresh(),增加額外的快取的有效時間,之後需要重新請求資料
  • onlyIfCached(),使用快取,不使用網路請求
  • noTransform(),不接受經過轉碼的響應
  • immutable(),快取有效時間內,響應不會變化,避免伺服器處理304響應

瞭解了這些配置方法後,修改之前的asyncGet()方法,建立一個CacheControl,並新增到Request

public void asyncGet(Callback callback) {
        CacheControl cacheControl = new CacheControl.Builder()
                .maxStale(10, TimeUnit.SECONDS)
                .maxAge(10, TimeUnit.SECONDS)
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();

        client.newCall(request).enqueue(callback);
    }
複製程式碼

Request新增CacheControl配置,就相當於給給Request新增了對應的Cache-Control請求頭!!!

我們設定maxAge為10秒、maxStale為10秒,此時攔截器中設定的Cache-Control響應頭還是60秒,測試下效果:

okhttp 快取實踐
可以看出,當時間間隔大於20秒會重新請求資料,即超過maxAge時間+maxStale時間

我們修改maxAge為100秒再測試下效果:

okhttp 快取實踐
可以看出,當時間間隔大於70秒會重新請求資料,即Cache-Control響應頭時間+maxStale時間

所以當通過CacheControl類設定的快取時間大於Cache-Control響應頭時間,快取有效時間為Cache-Control響應頭時間,否則為CacheControl類設定的快取時間。

所以我們可以給有需要的介面請求通過CacheControl類設定快取策略,然後在攔截器中判斷請求是否包含Cache-Control請求頭,如果有就把Cache-Control請求頭新增到響應中去,這樣問題就解決了,修改後的攔截器如下:

public class NetCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response originResponse = chain.proceed(request);

        if (!TextUtils.isEmpty(request.header("Cache-Control"))){
            originResponse = originResponse.newBuilder()
                    .removeHeader("pragma")
                    .header("Cache-Control", request.header("Cache-Control"))
                    .build();
        }

        return originResponse;
    }
}
複製程式碼

內容就這些了,不合理的地方還望指出。

測試程式碼地址:github.com/Othershe/So…

相關文章