手动缓存Retrofit+OkHttp响应体,不再局限于Get请求缓存
Posted iGoach
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手动缓存Retrofit+OkHttp响应体,不再局限于Get请求缓存相关的知识,希望对你有一定的参考价值。
转载请标明出处:
http://blog.csdn.net/iamzgx/article/details/51764848
本文出自:【iGoach的博客】
概括
这篇博客是接着上一篇博客学会Retrofit+OkHttp+RxAndroid三剑客的使用,让自己紧跟Android潮流的步伐,没看过的,建议看完上一篇再来看这篇。在上一篇博客中仅仅是简单的讲解了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结合提交参数是唯一标示,而Post请求的参数是在http的body体里面,是可变的,无法成为唯一的标示。但是,我们在项目中基本上每一个接口都要提交基本参数,一般用的都是Post请求。Get请求还不太安全,请求的路径大小还有限制。既然OkHttp有限制。那么我们可以自己手动缓存。
android的缓存处理
既然要手动缓存,那么我们就要来看看android里面手动缓存有哪些。主要有两种方式,一种是sqlite缓存,一种是文件缓存。
sqlite缓存
目前有很多第三方sqlite框架,比如可以结合GreenDao来做缓存,一个缓存对应一个表。把url路经,下载时间,过期时间等信息都存放到数据库。然后把url做为请求的唯一标示,在有网的情况下,判断当前请求url缓存是否存在,存在就要移除数据库里面的缓存,然后缓存新的缓存,在没有网络的情况下,判断缓存是否过期,然后进行数据库操作。从这里我们可以看出,数据库操作还是比较频繁的,一不留神,就会出现应用性能问题,ANR问题,指针问题。而且android数据库是放在/data/data/<包名>/databases/目录下,它会占用应用内存的,一但缓存很多的话,就要及时去清理缓存,很麻烦。文件缓存
为什么说文件缓存更好呢?如果SD存在的话,我们可以把缓存放在SD的/data/data/<包名>/cache目录下,不存在SD的话,再放在/data/data/<包名>下面。即使内存再多,也不会影响应用的内置应用空间。文件缓存一般都会通过DiskLruCache实现,DiskLruCache是硬盘缓存,即使应用进程结束了,缓存还是存在的。当应用卸载时,改目录的数据也会清除掉,不会留下残余数据。DiskLruCache缓存,没有什么过期时间之说,只要它存在文件里面,我们就可以随时去读取它。下面我们就用DiskLruCache对Retrofit+OkHttp的响应体进行缓存。这里我们只缓存json数据。
DiskLruCache的使用方法
获取DiskLruCache对象
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
不能直接通过new的方法创建,要通过调用DiskLruCache.open()这个方法获取,有四个参数,File指的是缓存的存储路径,一般优先存储于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指的就是一个key对应多少个文件,一般我们指定1个文件,一对一使得后面更好获取。maxSize指的是缓存的最大大小,一般传入5M或者10M就够了。
写入缓存
首先我们先获取一个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生成唯一的请求标示,这样就可以通过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框架一样,挺好用的。
使用方法如下(来源于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做为唯一的标示。
下面就使用这个DiskLruCache封装进行手动缓存,DiskLruCache的源码和封装代码可以去鸿洋的github上下载。
HRetrofitNetHelper代码的修改
基于上一篇博客的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-8
Charset charset = Charset.forName("UTF-8");
//去获取要保存的key
String key="";
//如果是Post请求,要通过Buffer去读取body体里面的参数
if(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();
}
key = buffer.readString(charset);
buffer.close();
}else{
//如果不是Post请求,比如Get请求,那么久通过url做为唯一标识
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,一定要先配置,要不然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;
}
}
//.....省略,和上篇博客代码一样
//这里我们改成通过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响应式编程也出来了。
以上是关于手动缓存Retrofit+OkHttp响应体,不再局限于Get请求缓存的主要内容,如果未能解决你的问题,请参考以下文章