以下內容基於 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秒會重新請求資料,這也驗證了我們的快取功能可以正常工作了:
快取功能的具體實現是通過 DiskLruCache 完成的,在之前配置的快取目錄可以找到對應的快取檔案: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秒,測試下效果:
maxAge時間
+maxStale時間
。
我們修改maxAge
為100秒再測試下效果:
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…