給Retrofit新增離線快取,支援Post請求
需要實現的需求:
有網路的時候使用網路獲取資料,網路不可用的情況下使用本地快取。
Retrofit本身並沒有可以設定快取的api,它的底層網路請求使用Okhttp,所以新增快取也得從Okhttp入手。
一.Okhttp自帶的快取支援:
首先設定快取目錄,Okhttp的快取用到了DiskLruCache這個類。
OkHttpClient.Builder builder = new OkHttpClient.Builder();
File cacheDir = new File(context.getCacheDir(), "response");
//快取的最大尺寸10m
Cache cache = new Cache(cacheDir, 1024 * 1024 * 10);
builder.cache(cache);
Okhttp快取攔截器:
public class CacheInterceptor implements Interceptor {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
boolean netAvailable = NetWorkUtil.isNetAvailable(AppIml.appContext);
if (netAvailable) {
request = request.newBuilder()
//網路可用 強制從網路獲取資料
.cacheControl(CacheControl.FORCE_NETWORK)
.build();
} else {
request = request.newBuilder()
//網路不可用 從快取獲取
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
if (netAvailable) {
response = response.newBuilder()
.removeHeader("Pragma")
// 有網路時 設定快取超時時間1個小時
.header("Cache-Control", "public, max-age=" + 60 * 60)
.build();
} else {
response = response.newBuilder()
.removeHeader("Pragma")
// 無網路時,設定超時為1周
.header("Cache-Control", "public, only-if-cached, max-stale=" + 7 * 24 * 60 * 60)
.build();
}
return response;
}
}
給OkHttpClient 設定攔截器,並用我們建立的OkHttpClient 替代Retrofit 預設的OkHttpClient:
builder.addInterceptor(new CacheInterceptor());
OkHttpClient client = builder.build();
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
到這裡Okhttp的快取就配置完成了,實現了開頭所提出的需求。
但是這裡有個問題,Okhttp是隻支援Get請求的,如果我們使用其他方式請求比如Post,請求的能夠回撥onResponse方法,但是通過 response.body()來獲取請求的資料會得到null, response.code()得到的是504。
我還目前沒有找到能夠讓Okhttp的快取支援Post方式的方法,所以我只能自己去實現快取機制。
二.自己手動新增快取支援:
首先將 DiskLruCache.java 新增進來,我們和Okhttp一樣使用它來實現磁碟快取策略。
關於DiskLruCache的原始碼分析:Android DiskLruCache完全解析,硬碟快取的最佳方案。
寫一個工具類 來設定和獲取快取:
public final class CacheManager {
public static final String TAG = "CacheManager";
//max cache size 10mb
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10;
private static final int DISK_CACHE_INDEX = 0;
private static final String CACHE_DIR = "responses";
private DiskLruCache mDiskLruCache;
private volatile static CacheManager mCacheManager;
public static CacheManager getInstance() {
if (mCacheManager == null) {
synchronized (CacheManager.class) {
if (mCacheManager == null) {
mCacheManager = new CacheManager();
}
}
}
return mCacheManager;
}
private CacheManager() {
File diskCacheDir = getDiskCacheDir(AppIml.appContext, CACHE_DIR);
if (!diskCacheDir.exists()) {
boolean b = diskCacheDir.mkdirs();
Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdirs()=" + b);
}
if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,
getAppVersion(AppIml.appContext), 1/*一個key對應多少個檔案*/, DISK_CACHE_SIZE);
Log.d(TAG, "mDiskLruCache created");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 同步設定快取
*/
public void putCache(String key, String value) {
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) {
e.printStackTrace();
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 非同步設定快取
*/
public void setCache(final String key, final String value) {
new Thread() {
@Override
public void run() {
putCache(key, value);
}
}.start();
}
/**
* 同步獲取快取
*/
public String getCache(String key) {
if (mDiskLruCache == null) {
return null;
}
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];
int len;
while ((len = fis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
byte[] data = bos.toByteArray();
return new String(data);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
/**
* 非同步獲取快取
*/
public void getCache(final String key, final CacheCallback callback) {
new Thread() {
@Override
public void run() {
String cache = getCache(key);
callback.onGetCache(cache);
}
}.start();
}
/**
* 移除快取
*/
public boolean removeCache(String key) {
if (mDiskLruCache != null) {
try {
return mDiskLruCache.remove(encryptMD5(key));
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
/**
* 獲取快取目錄
*/
private File getDiskCacheDir(Context context, String uniqueName) {
String cachePath = context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* 對字串進行MD5編碼
*/
public static String 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));
}
return hex.toString();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
return string;
}
/**
* 獲取APP版本號
*/
private int getAppVersion(Context context) {
PackageManager pm = context.getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
return pi == null ? 0 : pi.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 0;
}
}
然後我們還是給Okhttp新增攔截器,將請求的Request和請求結果Response以Key Value的形式快取的磁碟。這裡的重點是判斷請求的方式,如果是Post請求這將請求的body轉成String然後新增到url的後面作為磁碟快取的key。
public class EnhancedCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String url = request.url().toString();
RequestBody requestBody = request.body();
Charset charset = Charset.forName("UTF-8");
StringBuilder sb = new StringBuilder();
sb.append(url);
if (request.method().equals("POST")) {
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(Charset.forName("UTF-8"));
}
Buffer buffer = new Buffer();
try {
requestBody.writeTo(buffer);
} catch (IOException e) {
e.printStackTrace();
}
sb.append(buffer.readString(charset));
buffer.close();
}
Log.d(CacheManager.TAG, "EnhancedCacheInterceptor -> key:" + sb.toString());
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.d(CacheManager.TAG, "put cache-> key:" + key + "-> json:" + json);
return chain.proceed(request);
}
}
建立OkHttpClient並新增快取攔截器,初始化Retrofit;
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(new EnhancedCacheInterceptor());
OkHttpClient client = builder.build();
Retrofit retrofit = new Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
到這裡我們已經實現了請求網路新增快取,下一步是在網路不可用的時候獲取磁碟上的快取,這裡我通過改造Call和Callback來實現:
EnhancedCall使用裝飾模式裝飾Retrofit的Call,EnhancedCallback比Retrofit的Callback介面多一個onGetCache方法。
網路不可用的時候會回撥onFailure方法,我們攔截onFailure,並根據請求Request去獲取快取,獲取到快取走EnhancedCallback的onGetCache方法,如果沒有獲取到快取或者請求不需要使用快取再呼叫onFailure方法,
public class EnhancedCall<T> {
private Call<T> mCall;
private Class dataClassName;
// 是否使用快取 預設開啟
private boolean mUseCache = true;
public EnhancedCall(Call<T> call) {
this.mCall = call;
}
/**
* Gson反序列化快取時 需要獲取到泛型的class型別
*/
public EnhancedCall<T> dataClassName(Class className) {
dataClassName = className;
return this;
}
/**
* 是否使用快取 預設使用
*/
public EnhancedCall<T> useCache(boolean useCache) {
mUseCache = useCache;
return this;
}
public void enqueue(final EnhancedCallback<T> enhancedCallback) {
mCall.enqueue(new Callback<T>() {
@Override
public void onResponse(Call<T> call, Response<T> response) {
enhancedCallback.onResponse(call, response);
}
@Override
public void onFailure(Call<T> call, Throwable t) {
if (!mUseCache || NetWorkUtil.isNetAvailable(AppIml.appContext)) {
//不使用快取 或者網路可用 的情況下直接回撥onFailure
enhancedCallback.onFailure(call, t);
return;
}
Request request = call.request();
String url = request.url().toString();
RequestBody requestBody = request.body();
Charset charset = Charset.forName("UTF-8");
StringBuilder sb = new StringBuilder();
sb.append(url);
if (request.method().equals("POST")) {
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(Charset.forName("UTF-8"));
}
Buffer buffer = new Buffer();
try {
requestBody.writeTo(buffer);
} catch (IOException e) {
e.printStackTrace();
}
sb.append(buffer.readString(charset));
buffer.close();
}
String cache = CacheManager.getInstance().getCache(sb.toString());
Log.d(CacheManager.TAG, "get cache->" + cache);
if (!TextUtils.isEmpty(cache) && dataClassName != null) {
Object obj = new Gson().fromJson(cache, dataClassName);
if (obj != null) {
enhancedCallback.onGetCache((T) obj);
return;
}
}
enhancedCallback.onFailure(call, t);
Log.d(CacheManager.TAG, "onFailure->" + t.getMessage());
}
});
}
}
public interface EnhancedCallback<T> {
void onResponse(Call<T> call, Response<T> response);
void onFailure(Call<T> call, Throwable t);
void onGetCache(T t);
}
到這裡已經實現了最開始的我的需求,也可以支援Post請求的快取。最後看看使用的方式:
public void getRequest(View view) {
ApiService service = getApiService();
Call<UserList> call = service.getUserList();
//使用我們自己的EnhancedCall 替換Retrofit的Call
EnhancedCall<UserList> enhancedCall = new EnhancedCall<>(call);
enhancedCall.useCache(true)/*預設支援快取 可不設定*/
.dataClassName(UserList.class)
.enqueue(new EnhancedCallback<UserList>() {
@Override
public void onResponse(Call<UserList> call, Response<UserList> response) {
UserList userlist = response.body();
if (userlist != null) {
Log.d(TAG, "onResponse->" + userlist.toString());
}
}
@Override
public void onFailure(Call<UserList> call, Throwable t) {
Log.d(TAG, "onFailure->" + t.getMessage());
}
@Override
public void onGetCache(UserList userlist) {
Log.d(TAG, "onGetCache" + userlist.toString());
}
});
}
全部程式碼地址Github:https://github.com/wangyiwy/CacheUtil4Retrofit)
相關文章
- WKWebView的快取策略不支援POST請求!!!WebView快取
- Retrofit統一新增post請求的預設引數
- shell妙用 —— 發post請求重新整理CDN快取快取
- 離線快取快取
- Android RxJava+Retrofit完美封裝(快取,請求,生命週期管理)AndroidRxJava封裝快取
- java post 請求Java
- 使用RxJava快取Rest請求RxJava快取REST
- ajax請求如何防止快取快取
- ServiceWorker 快取離線化快取
- iOS 同步請求 非同步請求 GET請求 POST請求iOS非同步
- 前端快取API請求資料前端快取API
- 實現AVPlayer離線快取快取
- 快取專題:HTML5離線快取與HTTP快取快取HTMLHTTP
- jQueryAjax:$.post請求示例jQuery
- requests 模組 - post 請求
- post 請求工具類
- HTTP請求的快取(Cache)機制HTTP快取
- iOS 網路請求資料快取iOS快取
- 原始碼分析Retrofit請求流程原始碼
- RxJava + Retrofit完成網路請求RxJava
- SpringMVC中如何傳送GET請求、POST請求、PUT請求、DELETE請求。SpringMVCdelete
- Postman傳送Post請求Postman
- Java傳送Post請求Java
- Java 監聽POST請求Java
- Android網路請求(終) 網路請求框架RetrofitAndroid框架
- Retrofit 動態引數(非固定引數、非必須引數)(Get、Post請求)
- 【轉】怎麼用PHP傳送HTTP請求(POST請求、GET請求)?PHPHTTP
- 離散請求
- Android Http請求框架一:Get 和 Post 請求AndroidHTTP框架
- Android okHttp網路請求之Get/Post請求AndroidHTTP
- get請求和post請求的區別
- vue 發起get請求和post請求Vue
- 如何優雅地取消Retrofit請求?
- Retrofit網路請求原始碼解析原始碼
- 使用retrofit進行網路請求
- Retrofit+Rxjava的資料請求RxJava
- HTML5 manifest離線快取HTML快取
- 平穩擴充套件:可支援RevenueCat每日12億次API請求的快取套件API快取