转载请标明出处:
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); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
appVersion指的是版本号,可以指应用的版本号,valueCount指的就是一个key对应多少个文件,一般我们指定1个文件,一对一使得后面更好获取。maxSize指的是缓存的最大大小,一般传入5M或者10M就够了。
写入缓存
首先我们先获取一个DiskLruCache.Editor对象,代码如下
public DiskLruCache.Editor editor(String key) { try { key = Utils.hashKeyForDisk(key); DiskLruCache.Editor edit = mDiskLruCache.edit(key); if (edit == null) { Log.w(TAG, "the entry spcified key:" + key + " is editing by other . "); } return edit; } catch (IOException e) { e.printStackTrace(); } return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
首先进行的是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(); } catch (IOException e) { e.printStackTrace(); try { edit.abort(); } catch (IOException e1) { e1.printStackTrace(); } } finally { try { if (bw != null) bw.close(); } catch (IOException e) { e.printStackTrace(); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
读取缓存
首先是通过key获取DiskLruCache.Snapshot实例,然后得到InputStream,如下代码
public InputStream get(String key) { try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(Utils.hashKeyForDisk(key)); if (snapshot == null) { Log.e(TAG, "not find entry , or entry.readable = false"); return null; } return snapshot.getInputStream(0); } catch (IOException e) { e.printStackTrace(); return null; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
然后就是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(); } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
然后就是删除操作
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);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
这里我只是保存响应的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; 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 = new DiskLruCacheHelper(mContext); } catch (IOException e) { e.printStackTrace(); } 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); } 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 = call.request(); String requestUrl = call.request().url().toString(); BaseResp<D> resp = response.body() ; RequestBody requestBody = request.body(); Charset charset = Charset.forName("UTF-8"); String key=""; 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{ 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){ if(resp==null){ if(retrofitCallBack!=null) retrofitCallBack.onFailure("暂无数据"); }else{ if (resp.getResultCode() == 2000 || resp.getResultCode() == 2001 || resp.getResultCode() == 2002) { Toast.makeText(mContext,"code====="+resp.getResultCode(),Toast.LENGTH_SHORT).show(); } if (resp.getResultCode() == 200&&response.code()==200) { if(retrofitCallBack!=null){ String cacheResponse = mGson.toJson(resp); diskLruCacheHelper.remove(key); Log.d("zgx","response========cacheResponse"+cacheResponse); diskLruCacheHelper.put(key,cacheResponse); } retrofitCallBack.onSuccess(resp); } } else { if(retrofitCallBack!=null) retrofitCallBack.onFailure(resp.getErrMsg()); } } return; } String json = diskLruCacheHelper.getAsString(key); if(json==null){ Toast.makeText(mContext, "没有缓存!", Toast.LENGTH_SHORT).show(); if(retrofitCallBack!=null){ retrofitCallBack.onFailure("没有缓存!"); } }else{ 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) { if(retrofitCallBack!=null){ retrofitCallBack.onFailure(t.toString()); } } }); return this; } } public void clearCache() throws IOException { diskLruCacheHelper.delete(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
主要修改的地方,上面基本上都注释到了,这里没有做缓存的过期时间,有网的情况下,还是保持数据的实时性,没网的情况下才会去读取缓存。
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为RegisterBeanimplements HRetrofitNetHelper.RetrofitCallBack<RegisterBean>
然后是Call请求配置
final Call<BaseResp<RegisterBean>> repos = loginService.userLogin(username,password); new HRetrofitNetHelper .enqueueCall(HRetrofitNetHelper.getInstance(this)) .call(repos) .retrofitCallBack(this) .isCache(true) .clazz(new TypeToken<BaseResp<RegisterBean>>(){}.getType()) .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响应式编程也出来了。