背景
資料持久化在現在移動app開發中已經越來越被大家認可,提高了使用者體驗和軟體的穩定性,但是由於retrofit持久化的侷限性,所以需要自己動手改造一個適合自己的資料持久化方案!
封裝效果
第一次請求是網路載入,之後只要在設定的保鮮時間以內都是通過快取拉取資料,提高載入速度!
下面我們分兩節講解,一節講述自帶的retrofit-cache用法和缺陷,一節講述自己定義的快取處理方案
Retrofit-cookie
由於retrofit是基於okhttp的,所以他的cache原理就是運用了okhttp的cookie處理;
注意:這裡自帶的cookie前提是伺服器提供了支援(返回頭有cache資訊),只有get請求才具備http的快取功能,post沒有!沒有!沒有
Retrofit-Cache的內容
1.http快取相關頭:Expires (實體標頭,HTTP1.0+):一個GMT時間,試圖告知客戶端,在此日期內,可以信任並使用對應快取中的副本,缺點是,一但客戶端日期不準確.則可能導致失效
2.Pragma : no-cache(常規標頭,http1.0+)
3.Cache-Control : (常規標頭,HTTP1.1)
3.1 public:(僅為響應標頭)響應:告知任何途徑的快取者,可以無條件的快取該響應
3.2 private(僅為響應標頭):響應:告知快取者(據我所知,是指使用者代理,常見瀏覽器的本地快取.使用者也是指,系統使用者.但也許,不應排除,某些閘道器,可以識別每個終端使用者的情況),只針對單個使用者快取響應. 且可以具體指定某個欄位.如private –“username”,則響應頭中,名為username的標頭內容,不會被共享快取.
3.3 no-cache:告知快取者,必須原原本本的轉發原始請求,並告知任何快取者,別直接拿你快取的副本,糊弄人.你需要去轉發我的請求,並驗證你的快取(如果有的話).對應名詞:端對端過載.
cache-retrofit使用
註解使用,具體方法具體設定(max-age設定的是保鮮時間)
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();複製程式碼
當然我們肯定想要動態設定,而且每一個get方法都需要快取保鮮處理,怎麼解決呢?
1.開闢一片本地空間,設定給OkHttpClient.Builder
OkHttpClient.Builder builder = new OkHttpClient.Builder();
/*快取位置和大小*/
builder.cache(new Cache(MyApplication.app.getCacheDir(),10*1024*1024));複製程式碼
2.設定攔截器,請求前判斷網路,攔截資料和返回本地資料
網上很多資源都是錯誤的,走了很多彎路,注意這裡一定要返回一個新的Response 不讓不會有結果顯示
/**
* get快取方式攔截器
* Created by WZG on 2016/10/26.
*/
public class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
if (!isNetworkAvailable(MyApplication.app)) {//沒網強制從快取讀取(必須得寫,不然斷網狀態下,退出應用,或者等待一分鐘後,就獲取不到快取)
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
}
Response response = chain.proceed(request);
Response responseLatest;
if (isNetworkAvailable(MyApplication.app)) {
int maxAge = 60; //有網失效一分鐘
responseLatest = response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, max-age=" + maxAge)
.build();
} else {
int maxStale = 60 * 60 * 6; // 沒網失效6小時
responseLatest= response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
.build();
}
return responseLatest;
}
}複製程式碼
有網情況下,一分鐘內訪問的請求不會去真正http請求,而是從cache中獲取;
沒網情況下,一律從快取獲取,6小時過期時間。
3.設定OkHttpClient.Builder設定攔截器
addNetworkInterceptor在請求發生前和發生後都處理一遍,addInterceptor在有結果返回後處理一遍
注意:這裡一定要兩個方法同時設定才能保證生效,暫時沒搞懂為什麼
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addNetworkInterceptor(new CacheInterceptor());
builder.addInterceptor(new CacheInterceptor());複製程式碼
現在你的retrofit就能自動給get新增cookie了!
總結
自帶資料持久化處理方便快捷簡單,但是侷限性太大,必須是get請求而且還需要伺服器配合標頭檔案返回處理,所以在實際開發中並不適用;所以才有了自定義cookie處理的方案
自定義本地資料持久化方案
思路
主要是通過greenDao資料庫存放資料,在網路請求成功後儲存資料,再次請求判斷url是否已經存在快取資料
有網路:onstart中判斷再判斷保鮮時間,如果有效返回快取資料,無效則再一次請求資料!
無網路(包含各種失敗):onError中判斷處理,有效時間內返回資料,無效自定義的網路錯誤丟擲異常!
1.建立快取物件資料
記錄返回資料,標識url,和快取時間
/**
* post請求快取資料
* Created by WZG on 2016/10/26.
*/
@Entity
public class CookieResulte {
@Id
private long id;
/*url*/
private String url;
/*返回結果*/
private String resulte;
/*時間*/
private long time;
}複製程式碼
2.BaseApi新增快取相關設定引數
保持和封裝1-4封裝的一致性,將快取的相關設定放入在BaseApi中,並且將baseUrl和超時connectionTime也包含進來,更加靈活
/**
* 請求資料統一封裝類
* Created by WZG on 2016/7/16.
*/
public abstract class BaseApi<T> implements Func1<BaseResultEntity<T>, T> {
//rx生命週期管理
private SoftReference<RxAppCompatActivity> rxAppCompatActivity;
/*回撥*/
private SoftReference<HttpOnNextListener> listener;
/*是否能取消載入框*/
private boolean cancel;
/*是否顯示載入框*/
private boolean showProgress;
/*是否需要快取處理*/
private boolean cache;
/*基礎url*/
private String baseUrl="http://www.izaodao.com/Api/";
/*方法-如果需要快取必須設定這個引數;不需要不用設定*/
private String mothed;
/*超時時間-預設6秒*/
private int connectionTime = 6;
/*有網情況下的本地快取時間預設60秒*/
private int cookieNetWorkTime=60;
/*無網路的情況下本地快取時間預設30天*/
private int cookieNoNetWorkTime=24*60*60*30;
}複製程式碼
注意:如果需要使用快取功能必須要設定mothed引數(和baseurl拼成一個url標識快取資料)
3.攔截Gson資料
由於使用GsonConverterFactory自動解析資料,所以需要在自動轉換前得到伺服器返回的資料,我們可以自定義Interceptor在addInterceptor(成功後呼叫)攔截資料,儲存到本地資料庫中!
/**
* gson持久化擷取儲存資料
* Created by WZG on 2016/10/20.
*/
public class CookieInterceptor implements Interceptor {
private CookieDbUtil dbUtil;
/*是否快取標識*/
private boolean cache;
public CookieInterceptor( boolean cache) {
dbUtil=CookieDbUtil.getInstance();
this.cache=cache;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if(cache){
ResponseBody body = response.body();
BufferedSource source = body.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.buffer();
Charset charset = Charset.defaultCharset();
MediaType contentType = body.contentType();
if (contentType != null) {
charset = contentType.charset(charset);
}
String bodyString = buffer.clone().readString(charset);
String url = request.url().toString();
CookieResulte resulte= dbUtil.queryCookieBy(url);
long time=System.currentTimeMillis();
/*儲存和更新本地資料*/
if(resulte==null){
resulte =new CookieResulte(url,bodyString,time);
dbUtil.saveCookie(resulte);
}else{
resulte.setResulte(bodyString);
resulte.setTime(time);
dbUtil.updateCookie(resulte);
}
}
return response;
}
}複製程式碼
4.新增回撥方法
因為快取回撥過程中無法手動傳遞Gson物件,也就是ResulteEntity中的T泛型,所以自由單獨新增一個方法,返回快取資料!考慮到可能不需要回到所以寫成了具體的方法,可主動覆蓋!
/**
* 成功回撥處理
* Created by WZG on 2016/7/16.
*/
public abstract class HttpOnNextListener<T> {
/**
* 成功後回撥方法
* @param t
*/
public abstract void onNext(T t);
/**
* 快取回撥結果
* @param string
*/
public void onCacheNext(String string){
}
*********
}複製程式碼
5.資料持久化呼叫,獲取快取
這裡分兩種情況,有網路-和無網路(包含各種失敗不單單只是無網路)
有網
判斷是否存在快取,如果有判斷保鮮時間,有效期內返回資料,失效在一起請求;
/**
* 訂閱開始時呼叫
* 顯示ProgressDialog
*/
@Override
public void onStart() {
showProgressDialog();
/*快取並且有網*/
if(api.isCache()&& AppUtil.isNetworkAvailable(MyApplication.app)){
/*獲取快取資料*/
CookieResulte cookieResulte= CookieDbUtil.getInstance().queryCookieBy(api.getUrl());
if(cookieResulte!=null){
long time= (System.currentTimeMillis()-cookieResulte.getTime())/1000;
if(time< api.getCookieNetWorkTime()){
if( mSubscriberOnNextListener.get()!=null){
mSubscriberOnNextListener.get().onCacheNext(cookieResulte.getResulte());
}
onCompleted();
unsubscribe();
}
}
}
}複製程式碼
無網路(失敗情況)
原理和有網路一樣,但是額外的加入了rx異常處理,防止使用者在處理工程中導致錯誤崩潰!並且無緩衝丟擲自定義異常
/**
* 對錯誤進行統一處理
* 隱藏ProgressDialog
*
* @param e
*/
@Override
public void onError(Throwable e) {
dismissProgressDialog();
/*需要快取並且本地有快取才返回*/
if(api.isCache()){
Observable.just(api.getUrl()).subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
errorDo(e);
}
@Override
public void onNext(String s) {
/*獲取快取資料*/
CookieResulte cookieResulte= CookieDbUtil.getInstance().queryCookieBy(s);
if(cookieResulte==null){
throw new HttpTimeException("網路錯誤");
}
long time= (System.currentTimeMillis()-cookieResulte.getTime())/1000;
if(time<api.getCookieNoNetWorkTime()){
if( mSubscriberOnNextListener.get()!=null){
mSubscriberOnNextListener.get().onCacheNext(cookieResulte.getResulte());
}
}else{
CookieDbUtil.getInstance().deleteCookie(cookieResulte);
throw new HttpTimeException("網路錯誤");
}
}
});
}else{
errorDo(e);
}
}複製程式碼
6.回撥解析資料
由於是返回的string資料,所以需要在回撥onCacheNext中手動解析Gson資料
// 回撥一一對應
HttpOnNextListener simpleOnNextListener = new HttpOnNextListener<List<SubjectResulte>>() {
@Override
public void onNext(List<SubjectResulte> subjects) {
tvMsg.setText("網路返回:\n" + subjects.toString());
}
@Override
public void onCacheNext(String cache) {
/*快取回撥*/
Gson gson=new Gson();
java.lang.reflect.Type type = new TypeToken<BaseResultEntity<List<SubjectResulte>>>() {}.getType();
BaseResultEntity resultEntity= gson.fromJson(cache, type);
tvMsg.setText("快取返回:\n"+resultEntity.getData().toString() );
}
};複製程式碼
好了,一套自定義的快取方案就解決了!
總結
優點:
1.有效的解決了post請求快取的問題
2.可以同時快取get資料
3.自定義更加靈活,可更換任意第三方庫
缺點:
1.快取資料無法和onext公用一個回到介面,導致需要手動解析資料(由於Gson自動轉換導致)
由於Gson在回撥的過程中和使用過程中給程式導致的一些列的限制,所以決定封裝一個變種框架,去掉Gson自動解析回撥功能,改用String回撥,讓回撥介面一對多處理,並且解決快取無法和成功統一回撥的問題!歡迎大家關注!
如有幫助換start和follow!