程序员人生 网站导航

手动缓存Retrofit+OkHttp响应体,不再局限于Get请求缓存

栏目:综合技术时间:2016-06-30 12:45:42

转载请标明出处:
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的缓存处理

既然要手动缓存,那末我们就要来看看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的使用方法

获得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了。

DiskLruCache的封装

从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上下载。

HRetrofitNetHelper代码的修改

基于上1篇博客的HRetrofitNetHelper对象。进行代码修改,修改点以下

  • 去除OkHttp的cache缓存配置
  • 去除mUrlInterceptor的拦截器
  • 改在call的onresponse里面进行操作
  • enqueueCall方法配置成链式编程配置

然后再贴上全部的代码,注意几个修改点就行了。

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(); } }

主要修改的地方,上面基本上都注释到了,这里没有做缓存的过期时间,有网的情况下,还是保持数据的实时性,没网的情况下才会去读取缓存。

API修改成Post要求

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响应式编程也出来了。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐