Cache-Control與retrofit快取
Cache-Control
HTTP中這個欄位用於指定所有快取機制在整個請求/響應鏈中必須服從的指令。快取指令是單向的,即請求中存在一個指令並不意味著響應中將存在同一個指令。常見的取值有private、no-cache、max-age、must-revalidate等,預設為private。
常用 cache-directive 值
Cache-directive | 說明 |
public | 所有內容都將被快取(客戶端和代理伺服器都可快取) |
private | 內容只快取到私有快取中(僅客戶端可以快取,代理伺服器不可快取) |
no-cache | 必須先與伺服器確認返回的響應是否被更改,然後才能使用該響應來滿足後續對同一個網址的請求。因此,如果存在合適的驗證令牌 (ETag),no-cache 會發起往返通訊來驗證快取的響應,如果資源未被更改,可以避免下載。 |
no-store | 所有內容都不會被快取到快取或 Internet 臨時檔案中 |
must-revalidation/proxy-revalidation | 如果快取的內容失效,請求必須傳送到伺服器/代理以進行重新驗證 |
max-age=xxx (xxx is numeric) | 快取的內容將在 xxx 秒後失效, 這個選項只在HTTP 1.1可用, 並如果和Last-Modified一起使用時, 優先順序較高 |
Retrofit實現網路快取
首先,配置OkHttp中Cache
OkHttpClient okHttpClient = new OkHttpClient();
File cacheFile = new File(context.getCacheDir(), "[快取目錄]");
Cache cache = new Cache(cacheFile, 1024 * 1024 * 100);//100Mb
okHttpClient.setCache(cache);
配置請求頭中的Cache-Control
@FormUrlEncoded
@Headers("Cache-Control: public,max-age:3600")
@POST("?method=app.system.init")
Observable<HttpResult<AppInit>>getAppInit(@Field("ucode") String ucode);
雲端配合設定響應頭或者自己寫攔截器修改響應頭
到這一步快取就已經待在你的快取目錄了。如果雲端有處裡cache的話,就已經可以了。但是很可能雲端沒有處理,所以返回的響應頭中cache-control是no-cache,這時候你還是無法做快取,大家可以用okhttp的寫日誌攔截器檢視響應頭的內容。
如果雲端現在不方便處理的話,你也可以自己搞定快取的,那就是寫攔截器修改響應頭中的cache-control。
設定攔截器:
CacheControlInterceptor cacheControlInterceptor = newCacheControlInterceptor();
return new OkHttpClient
.Builder()
.addInterceptor(loggingInterceptor)
.addInterceptor(paramsInterceptor)
.addInterceptor(cacheInterceptor)
.cache(cache)
.build();
攔截器的程式碼如下:
public class CacheControlInterceptor implementsInterceptor {
@Override
public Responseintercept(Chain chain) throws IOException {
CacheControl.Builder cacheBuilder = new CacheControl.Builder();
cacheBuilder.maxAge(0, TimeUnit.SECONDS);
cacheBuilder.maxStale(365, TimeUnit.DAYS);
CacheControl cacheControl = cacheBuilder.build();
Requestrequest = chain.request();
if(!NetworkStateUtils.isNetworkAvailable()) {
request= request.newBuilder()
.cacheControl(cacheControl)
.build();
}
ResponseoriginalResponse = chain.proceed(request);
if(NetworkStateUtils.isNetworkAvailable()) {
intmaxAge = 0; // read from cache
returnoriginalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public ,max-age=" +maxAge)
.build();
} else {
intmaxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
returnoriginalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, only-if-cached,max-stale=" + maxStale)
.build();
}
}
Retrofit支援快取post請求
不過,因為Retrofit和OkHttp是以支援RestfulAPI為前提的,所以,只對get請求快取。如果伺服器不是標準的RestfulAPI,比如全部採用post請求,那麼如何實現快取呢?
Retrofit本身的快取是通過DiskLRUCache實現的,我們可以仿照它來實現自己的快取來支援post請求。
寫一個工具類來設定和獲取快取:
public final class CacheManager {
public staticfinal String TAG = "CacheManager";
//max cachesize 100mb
private static final long DISK_CACHE_SIZE =1024 * 1024 * 100;
private staticfinal int DISK_CACHE_INDEX = 0;
private staticfinal String CACHE_DIR = "responses";
privatevolatile static CacheManager mCacheManager;
privateDiskLruCache mDiskLruCache;
privateCacheManager() {
FilediskCacheDir = getDiskCacheDir(App.getContext(), CACHE_DIR);
if(!diskCacheDir.exists()) {
booleanb = diskCacheDir.mkdirs();
Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdirs()="+ b);
}
if(diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,
getAppVersion(App.getContext()), 1/*一個key對應多少個檔案*/, DISK_CACHE_SIZE);
Log.d(TAG, "mDiskLruCache created");
} catch(IOException e) {
e.printStackTrace();
}
}
}
public staticCacheManager getInstance() {
if (mCacheManager == null) {
synchronized (CacheManager.class) {
if(mCacheManager == null) {
mCacheManager = new CacheManager();
}
}
}
returnmCacheManager;
}
/**
* 對字串進行MD5編碼
*/
private staticString encryptMD5(String string) {
try {
byte[]hash = MessageDigest.getInstance("MD5").digest(
string.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder(hash.length * 2);
for(byte b : hash) {
if((b & 0xFF) < 0x10) {
hex.append("0");
}
hex.append(Integer.toHexString(b & 0xFF));
}
returnhex.toString();
} catch(NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
returnstring;
}
/**
* 同步設定快取
*/
public voidputCache(String key, String value) throws IOException {
if(mDiskLruCache == null) return;
OutputStream os = null;
try {
DiskLruCache.Editor editor = mDiskLruCache.edit(encryptMD5(key));
os = editor.newOutputStream(DISK_CACHE_INDEX);
os.write(value.getBytes());
os.flush();
editor.commit();
mDiskLruCache.flush();
} catch(IOException e) {
throwe;
} finally {
if (os != null) {
try{
os.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 同步獲取快取
*/
public StringgetCache(String key) throws IOException {
if(mDiskLruCache == null) {
returnnull;
}
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(encryptMD5(key));
if(snapshot != null) {
fis= (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
bos= new ByteArrayOutputStream();
byte[] buf = new byte[1024];
intlen;
while ((len = fis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
byte[] data = bos.toByteArray();
return new String(data);
}
} catch(IOException e) {
throwe;
} finally {
if (fis!= null) {
try{
fis.close();
}catch (IOException e) {
e.printStackTrace();
}
}
if (bos!= null) {
try{
bos.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
return"";
}
/**
* 移除快取
*/
public booleanremoveCache(String key) {
if(mDiskLruCache != null) {
try {
return mDiskLruCache.remove(encryptMD5(key));
} catch(IOException e) {
e.printStackTrace();
}
}
returnfalse;
}
/**
* 獲取快取目錄
*/
private FilegetDiskCacheDir(Context context, String uniqueName) {
StringcachePath = context.getCacheDir().getPath();
return new File(cachePath + File.separator +uniqueName);
}
/**
* 獲取APP版本號
*/
private intgetAppVersion(Context context) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
returnpi == null ? 0 : pi.versionCode;
} catch(PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}
}
實現一個EnhancedCacheInterceptor,攔截post並快取post請求。實現無網路時讀取快取。
public class EnhancedCacheInterceptor implementsInterceptor {
@Override
public Responseintercept(Chain chain) throws IOException {
LogUtils.e("EnhancedCacheInterceptor");
Requestrequest = chain.request();
HttpUrlhttpUrl = request.url();
String url= httpUrl.toString();
RequestBodyrequestBody = request.body();
Charsetcharset = Charset.forName("UTF-8");
StringBuilder sb = new StringBuilder();
sb.append(httpUrl.queryParameter("method"));
/*sb.append(url);
if(request.method().equals("POST")) {
MediaType contentType = requestBody.contentType();
if(contentType != null) {
charset = contentType.charset(Charset.forName("UTF-8"));
}
Bufferbuffer = new Buffer();
try {
requestBody.writeTo(buffer);
} catch(IOException e) {
e.printStackTrace();
}
sb.append(buffer.readString(charset));
buffer.close();
}*/
Log.e(CacheManager.TAG, "EnhancedCacheInterceptor -> key:"+ sb.toString());
if(NetworkStateUtils.isNetworkAvailable()) {
Response response = chain.proceed(request);
CacheControl cacheControl = request.cacheControl();
LogUtils.e(cacheControl.toString());
LogUtils.e("response: " + response.toString());
LogUtils.e("responseBody : " + response.body().toString());
if(!cacheControl.noStore()) {
ResponseBody responseBody = response.body();
MediaType contentType = responseBody.contentType();
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.buffer();
if(contentType != null) {
charset = contentType.charset(Charset.forName("UTF-8"));
}
String key = sb.toString();
//伺服器返回的json原始資料
String json = buffer.clone().readString(charset);
CacheManager.getInstance().putCache(key, json);
Log.e(CacheManager.TAG, "putcache-> key:" + key + "-> json:" + json);
}
returnresponse;
} else {
CacheControl cacheControl = request.cacheControl();
if(!cacheControl.noStore()) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
String key = sb.toString();
String cache = CacheManager.getInstance().getCache(key);
Response originalResponse =chain.proceed(request);
return originalResponse
.newBuilder()
.code(200)
.message("OK")
.body(ResponseBody.create(originalResponse.body().contentType(), cache))
.build();
} else{
return chain.proceed(request);
}
}
}
}
注意,預設在無網路情況下,會返回504 error。這裡hack response,強制改回200,並返回快取資料。