轉載請標明出處:
http://blog.csdn.net/iamzgx/article/details/51764848
本文出自:【iGoach的博客】
這篇博客是接著上1篇博客學會Retrofit+OkHttp+RxAndroid3劍客的使用,讓自己緊跟Android潮流的步伐,沒看過的,建議看完上1篇再來看這篇。在上1篇博客中僅僅是簡單的講授了OkHttp的緩存問題,主要是通過http協議里面的control-cache控制緩存,而且是僅僅只能是Get要求才能緩存,如果Post要求OkHttp會讓response返回null,同時報504毛病,也就是沒緩存。okhttp為何要這樣做呢?通過查看緩存的文件,我們可以發現,OkHttp緩存的是全部http要求的信息,所以這就和http協議有關系了。在RESTful API里面,我們把Get要求理解為從服務端查詢數據,Post要求理解為更新服務端數據,而http協議里面緩存通常只適用于idempotent request,也就是Get要求,為何只適應Get要求?我們都知道Get要求url結合提交參數是唯1標示,而Post要求的參數是在http的body體里面,是可變的,沒法成為唯1的標示。但是,我們在項目中基本上每個接口都要提交基本參數,1般用的都是Post要求。Get要求還不太安全,要求的路徑大小還有限制。既然OkHttp有限制。那末我們可以自己手動緩存。
既然要手動緩存,那末我們就要來看看android里面手動緩存有哪些。主要有兩種方式,1種是sqlite緩存,1種是文件緩存。
sqlite緩存
目前有很多第3方sqlite框架,比如可以結合GreenDao來做緩存,1個緩存對應1個表。把url路經,下載時間,過期時間等信息都寄存到數據庫。然后把url做為要求的唯1標示,在有網的情況下,判斷當前要求url緩存是不是存在,存在就要移除數據庫里面的緩存,然后緩存新的緩存,在沒有網絡的情況下,判斷緩存是不是過期,然落后行數據庫操作。從這里我們可以看出,數據庫操作還是比較頻繁的,1不留心,就會出現利用性能問題,ANR問題,指針問題。而且android數據庫是放在/data/data/<包名>/databases/目錄下,它會占用利用內存的,1但緩存很多的話,就要及時去清算緩存,很麻煩。
文件緩存
為何說文件緩存更好呢?如果SD存在的話,我們可以把緩寄存在SD的/data/data/<包名>/cache目錄下,不存在SD的話,再放在/data/data/<包名>下面。即便內存再多,也不會影響利用的內置利用空間。文件緩存1般都會通過DiskLruCache實現,DiskLruCache是硬盤緩存,即便利用進程結束了,緩存還是存在的。當利用卸載時,改目錄的數據也會清除掉,不會留下殘余數據。DiskLruCache緩存,沒有甚么過期時間之說,只要它存在文件里面,我們就能夠隨時去讀取它。下面我們就用DiskLruCache對Retrofit+OkHttp的響應體進行緩存。這里我們只緩存json數據。
獲得DiskLruCache對象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
不能直接通過new的方法創建,要通過調用DiskLruCache.open()這個方法獲得,有4個參數,File指的是緩存的存儲路徑,1般優先存儲于SD卡的 /sdcard/Android/data/<包名>/cache 路徑下,如果SD卡不存在,再存在/data/data/<包名>/cache 這個路徑下,判斷代碼以下
private File getDiskCacheDir(Context context, String uniqueName)
{
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable())
{
//如果SD卡存在通過getExternalCacheDir()獲得路徑,
cachePath = context.getExternalCacheDir().getPath();
} else
{
//如果SD卡不存在通過getCacheDir()獲得路徑,
cachePath = context.getCacheDir().getPath();
}
//放在路徑 /.../data/<application package>/cache/uniqueName
return new File(cachePath + File.separator + uniqueName);
}
appVersion指的是版本號,可以指利用的版本號,valueCount指的就是1個key對應多少個文件,1般我們指定1個文件,1對1使得后面更好獲得。maxSize指的是緩存的最大大小,1般傳入5M或10M就夠了。
寫入緩存
首先我們先獲得1個DiskLruCache.Editor對象,代碼以下
public DiskLruCache.Editor editor(String key)
{
try
{
key = Utils.hashKeyForDisk(key);
//wirte DIRTY
DiskLruCache.Editor edit = mDiskLruCache.edit(key);
//edit maybe null :the entry is editing
if (edit == null)
{
Log.w(TAG, "the entry spcified key:" + key + " is editing by other . ");
}
return edit;
} catch (IOException e)
{
e.printStackTrace();
}
return null;
}
首先進行的是Utils.hashKeyForDisk(key),也就是通過MD5生成唯1的要求標示,這樣就能夠通過key來獲得DiskLruCache.Editor實例。獲得到實例后就能夠獲得到OutputStream,然后通過BufferedWriter寫入,以下代碼
public void put(String key, String value)
{
DiskLruCache.Editor edit = null;
BufferedWriter bw = null;
try
{
edit = editor(key);
if (edit == null) return;
OutputStream os = edit.newOutputStream(0);
bw = new BufferedWriter(new OutputStreamWriter(os));
bw.write(value);
edit.commit();//write CLEAN
} catch (IOException e)
{
e.printStackTrace();
try
{
//s
edit.abort();//write REMOVE
} catch (IOException e1)
{
e1.printStackTrace();
}
} finally
{
try
{
if (bw != null)
bw.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
讀取緩存
首先是通過key獲得DiskLruCache.Snapshot實例,然后得到InputStream,以下代碼
public InputStream get(String key)
{
try
{
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key));
if (snapshot == null) //not find entry , or entry.readable = false
{
Log.e(TAG, "not find entry , or entry.readable = false");
return null;
}
//write READ
return snapshot.getInputStream(0);
} catch (IOException e)
{
e.printStackTrace();
return null;
}
}
然后就是InputStreamReader讀取,以下代碼
public String getAsString(String key) {
InputStream inputStream = null;
inputStream = get(key);
if (inputStream == null) return null;
String str = null;
try {
str = Util.readFully(new InputStreamReader(inputStream, Util.UTF_8));
} catch (IOException e) {
e.printStackTrace();
try {
inputStream.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return str;
}
static String readFully(Reader reader) throws IOException
{
try
{
StringWriter writer = new StringWriter();
char[] buffer = new char[1024];
int count;
while ((count = reader.read(buffer)) != -1)
{
writer.write(buffer, 0, count);
}
return writer.toString();
} finally
{
reader.close();
}
}
然后就是刪除操作
public boolean remove(String key)
{
try
{
key = Utils.hashKeyForDisk(key);
return mDiskLruCache.remove(key);
} catch (IOException e)
{
e.printStackTrace();
}
return false;
}
直接remove掉就ok了。
從Github里面搜索DiskLruCache,可以看到鴻洋大神的base-diskcache框架,它主要是把diskcache封裝成和AsimpleCache框架1樣,挺好用的。
使用方法以下(來源于base-diskcache框架)
存
put(String key, Bitmap bitmap)
put(String key, byte[] value)
put(String key, String value)
put(String key, JSONObject jsonObject)
put(String key, JSONArray jsonArray)
put(String key, Serializable value)
put(String key, Drawable value)
editor(String key).newOutputStream(0);//原本的方式
取
String getAsString(String key);
JSONObject getAsJson(String key)
JSONArray getAsJSONArray(String key)
<T> T getAsSerializable(String key)
Bitmap getAsBitmap(String key)
byte[] getAsBytes(String key)
Drawable getAsDrawable(String key)
InputStream get(String key);//原本的用法
這里我只是保存響應的json,只用到
put(String key, String value)
和
String getAsString(String key);
兩個方法,至于key使用要求參數生成的MD5做為唯1的標示。
下面就使用這個DiskLruCache封裝進行手動緩存,DiskLruCache的源碼和封裝代碼可以去鴻洋的github上下載。
基于上1篇博客的HRetrofitNetHelper對象。進行代碼修改,修改點以下
然后再貼上全部的代碼,注意幾個修改點就行了。
public class HRetrofitNetHelper implements HttpLoggingInterceptor.Logger {
public static HRetrofitNetHelper mInstance;
public Retrofit mRetrofit;
public OkHttpClient mOkHttpClient;
public HttpLoggingInterceptor mHttpLogInterceptor;
private BasicParamsInterceptor mBaseParamsInterceptor;
private Context mContext;
public Gson mGson;
//DiskLruCache封裝的幫助類,
private DiskLruCacheHelper diskLruCacheHelper;
public static final String BASE_URL = "http://192.168.1.102:8080/GoachWeb/";
private Action1<String> onNextAction;
private HRetrofitNetHelper(Context context){
this.mContext = context ;
createSubscriberByAction();
mGson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
mHttpLogInterceptor = new HttpLoggingInterceptor(this);
mHttpLogInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
Map<String,String> tempParams = getBaseParams();
mBaseParamsInterceptor = new BasicParamsInterceptor.Builder()
.addParamsMap(tempParams)
.build();
try {
//創建DiskLruCacheHelper 對象
diskLruCacheHelper = new DiskLruCacheHelper(mContext);
} catch (IOException e) {
e.printStackTrace();
}
//這里去除緩存配置和mUrlInterceptor的配置
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(12, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(mHttpLogInterceptor)
.addInterceptor(mBaseParamsInterceptor)
.build();
mRetrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(mGson))
.client(mOkHttpClient)
.build();
}
public static HRetrofitNetHelper getInstance(Context context){
if(mInstance==null){
synchronized (HRetrofitNetHelper.class){
if(mInstance==null){
mInstance = new HRetrofitNetHelper(context);
}
}
}
return mInstance;
}
public <T> T getAPIService(Class<T> service) {
return mRetrofit.create(service);
}
/*這里改成鏈式編程,默許是不緩存。在不緩存的情況下,只需配置Call<BaseResp<D>>實例,也就是調用上面getAPIService方法獲得的實例。然后就是retrofitCallBack回調接口,如果需要緩存的情況,那末就要再配置isCache為true,然后配置Type(主要是Gson解析泛型會報錯Java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to,所以需再傳遞這個參數進行解析),最后調用start方法進行要求*/
public static final class enqueueCall{
boolean isCache;
Type clazz;
Call call;
RetrofitCallBack retrofitCallBack;
HRetrofitNetHelper mRetrofitNetHelper;
private Context mContext;
public Gson mGson;
private DiskLruCacheHelper diskLruCacheHelper;
public enqueueCall(HRetrofitNetHelper retrofitNetHelper){
isCache = false;
this.mRetrofitNetHelper = retrofitNetHelper;
this.mContext = retrofitNetHelper.mContext;
this.mGson = retrofitNetHelper.mGson;
this.diskLruCacheHelper = retrofitNetHelper.diskLruCacheHelper;
}
public <D> enqueueCall call(Call<BaseResp<D>> call){
this.call = call ;
return this;
}
public enqueueCall clazz(Type clazz){
this.clazz = clazz ;
return this;
}
public <D> enqueueCall retrofitCallBack(RetrofitCallBack<D> retrofitCallBack){
this.retrofitCallBack = retrofitCallBack ;
return this;
}
public enqueueCall isCache(boolean isCache){
this.isCache = isCache ;
return this;
}
public <D> enqueueCall start(){
call.enqueue(new Callback<BaseResp<D>>() {
@Override
public void onResponse(Call<BaseResp<D>> call, Response<BaseResp<D>> response) {
//獲得要求Request
Request request = call.request();
//獲得要求的url
String requestUrl = call.request().url().toString();
//去獲得返回數據
BaseResp<D> resp = response.body() ;
//去獲得RequestBody
RequestBody requestBody = request.body();
//緩存格式為utf⑻
Charset charset = Charset.forName("UTF⑻");
//去獲得要保存的key
String key="";
//如果是Post要求,要通過Buffer去讀取body體里面的參數
if(method.equals("POST")){
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(Charset.forName("UTF⑻"));
}
Buffer buffer = new Buffer();
try {
requestBody.writeTo(buffer);
} catch (IOException e) {
e.printStackTrace();
}
key = buffer.readString(charset);
buffer.close();
}else{
//如果不是Post要求,比如Get要求,那末久通過url做為唯1標識
key = requestUrl;
}
Log.d("zgx","response==========key"+key);
//處理特殊接口,如果是登錄接口進行彈框提示
if(!TextUtils.isEmpty(requestUrl)){
if(requestUrl.contains("LoginDataServlet")) {
if (Looper.myLooper() == null) {
Looper.prepare();
}
mRetrofitNetHelper.createObservable("現在要求的是登錄接口");
}
}
//分為有網和沒網的情況下
//如果有網
if(NetUtil.checkNetwork(mContext)!=NetUtil.NO_NETWORK){
//如果返回數據為null
if(resp==null){
//回調失敗接口
if(retrofitCallBack!=null)
retrofitCallBack.onFailure("暫無數據");
}else{
//如果是接口返回2000或2001或2002,進行彈框提示
if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) {
Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show();
}
//如果接口返回200,并且http要求code返回200,說明要求成功
if (resp.getResultCode() == 200&&response.code()==200) {
if(retrofitCallBack!=null){
//需要緩存數據
String cacheResponse = mGson.toJson(resp);
//判斷下當前是不是存在key緩存的數據,如果存在移除掉, if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(diskLruCacheHelper.getAsString(key)))
diskLruCacheHelper.remove(key);
//當需要緩存的數據不為空的時候,并且需要緩存的時候,通過diskLruCacheHelper進行緩存 if(!TextUtils.isEmpty(key)&&!TextUtils.isEmpty(cacheResponse)&&isCache){
Log.d("zgx","response========cacheResponse"+cacheResponse);
diskLruCacheHelper.put(key,cacheResponse);
}
//然后就是回調成功接口
retrofitCallBack.onSuccess(resp);
}
} else {
//這個是要求失敗,那末就回調失敗接口
// ToastMaker.makeToast(mContext, resp.errMsg, Toast.LENGTH_SHORT);
if(retrofitCallBack!=null)
retrofitCallBack.onFailure(resp.getErrMsg());
}
}
return;
}
//沒有網絡的情況下,去獲得key對應的緩存
String json = diskLruCacheHelper.getAsString(key);
//如果緩存不存在,那末久回調失敗接口
if(json==null){
Toast.makeText(mContext, "沒有緩存!", Toast.LENGTH_SHORT).show();
if(retrofitCallBack!=null){
retrofitCallBack.onFailure("沒有緩存!");
}
}else{
//判斷是不是配置clazz,1定要先配置,要不然Gson解析出錯
if(clazz==null){
throw new IllegalArgumentException("請先配置clazz");
}
//解析緩存數據,然落后行回調成功接口
resp = mGson.fromJson(json,clazz);
if(retrofitCallBack!=null){
retrofitCallBack.onSuccess(resp);
}
}
}
@Override
public void onFailure(Call<BaseResp<D>> call, Throwable t) {
// ToastMaker.makeToast(mContext, "網絡毛病,請重試!", Toast.LENGTH_SHORT);
if(retrofitCallBack!=null){
retrofitCallBack.onFailure(t.toString());
}
}
});
return this;
}
}
//.....省略,和上篇博客代碼1樣
//這里我們改成通過diskLruCacheHelper封裝的類進行刪除緩存
public void clearCache() throws IOException {
diskLruCacheHelper.delete();
}
}
主要修改的地方,上面基本上都注釋到了,這里沒有做緩存的過期時間,有網的情況下,還是保持數據的實時性,沒網的情況下才會去讀取緩存。
ILoginService.class
public interface ILoginService {
@FormUrlEncoded
@POST("LoginDataServlet")
Call<BaseResp<RegisterBean>> userLogin(@Field("username") String username, @Field("password") String password);
}
INewsService.class
public interface INewsService {
@FormUrlEncoded
@POST("NewsDataServlet")
Call<BaseResp<News<NewItem>>> userNews(@Field("userId") String userId);
}
這里主要是測試這兩個接口
登錄要求修改代碼以下
首先實現回調接口
//傳入成功回調的BaseResp<T>的泛型T為RegisterBean
implements HRetrofitNetHelper.RetrofitCallBack<RegisterBean>
然后是Call要求配置
final Call<BaseResp<RegisterBean>> repos = loginService.userLogin(username,password);
new HRetrofitNetHelper
.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)//repos指的是retrofitNetHelper.getAPIService返回的API
.retrofitCallBack(this)//配置回調接口
.isCache(true)//設置需要緩存
.clazz(new TypeToken<BaseResp<RegisterBean>>(){}.getType())//Gson解析緩存需要
.start();//真正開始發起要求
然后實現兩個回調方法
@Override
public void onSuccess(BaseResp<RegisterBean> baseResp) {
Date date = baseResp.getResponseTime();
if(baseResp.getData().getErrorCode()==1){
Toast.makeText(getBaseContext(),"登錄成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(getBaseContext(),"用戶不存在",Toast.LENGTH_SHORT).show();
}
mDialog.dismiss();
}
@Override
public void onFailure(String error) {
Log.d("zgx","onFailure======"+error);
mDialog.dismiss();
}
如果新聞頁也要緩存,那末代碼同理修改以下。
private void loadData(){
INewsService newService = retrofitNetHelper.getAPIService(INewsService.class);
Log.d("zgx","mUserId====="+mUserId);
final Call<BaseResp<News<NewItem>>> repos = newService.userNews(mUserId);
new HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)
.retrofitCallBack(this)
.isCache(true)
.clazz(new TypeToken<BaseResp<News<NewItem>>>(){}.getType())
.start();
}
這樣就緩存了登錄接口的數據和新聞頁面的數據。
下面就來測試下,只緩存登錄接口。測試結果為有網的情況下,根據上面代碼知道登錄成功會彈出登錄成功的Toast,并且會生成緩存文件,沒有網絡的情況下會去讀取緩存,并且還是會彈出Toast提示,登錄失敗不彈。效果以下
接下來我們再看下沒有緩存的效果,代碼只要修改不配置
HRetrofitNetHelper.enqueueCall(HRetrofitNetHelper.getInstance(this))
.call(repos)
.retrofitCallBack(this)
.start();
然后就來看效果,有網的情況下應當為登錄成功,沒網的情況下,提示沒有緩存,效果以下
Get要求效果同理。一樣可以得到這樣的效果,感興趣的可以去試下。
最后配置3個權限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
整體感覺Retrofit+OkHttp框架用起來還是很方便的。特別是響應式編程,用的特別爽。還有就是Retrofit的源碼設計的特別完善。不過在這里,用RxAndroid用的還是比較少,相信以后會用的愈來愈多,而且現在谷歌的agera響應式編程也出來了。