缓存的必要性
作为一个APP,必须为客户的流量做到最优化。同时能在无网的情况下不显示一个光秃秃的列表。所以缓存非常有必要
Retrofit2+Rxjava2组合的网络访问框架
这个不用多说,基本上现在最流行的网络访问模式了。同时Retrofit2基于okhttp3,所以可以基于okhttp做更多的定制。
缓存的最佳实践(1)---基于okhttp header的网络缓存
首先是支持缓存的 开启okhttp的缓存只需要给OkHttpClient
设置两个Interceptor
即可(拦截器),最后cache(cache)
即可。
有
addInterceptor
和addNetworkInterceptor
这两种。他们的区别简单的说下,不知道也没关系,addNetworkInterceptor
添加的是网络拦截器,他会在在request和resposne是分别被调用一次,addinterceptor
添加的是aplication拦截器,他只会在response被调用一次。
首先是第一个拦截器,用于设置header 数据,开启缓存。
其中这里有个重要
显然这是个addNetworkInterceptor
因为既需要在request添加header头,又需要resposne里获取缓存数据。
maxAge 参数用于设置 一个多少秒内访问不重复请求接口的参数 单位为秒
public class CacheNetworkInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response originalResponse = chain.proceed(request); int maxAge = 20; // 在线缓存,单位:秒 return originalResponse.newBuilder() .removeHeader("Pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效 .removeHeader("Cache-Control") .header("Cache-Control", "public, max-age=" + maxAge) .build(); } }
第二个是`addinterceptor·,用于response时候讲数据加入缓存,并设置一个最大缓存时间 maxStale
public class CacheInterceptor implements Interceptor { private int maxStale; public CacheInterceptor(int maxStale) { this.maxStale = maxStale; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); if (!NetworkUtils.isConnected(AppApplication.applicationContext)) { //如果断网 这里返回 缓存数据 直接结束这次访问 CacheControl tempCacheControl = new CacheControl.Builder() .onlyIfCached() .maxStale(maxStale, TimeUnit.SECONDS) .build(); request = request.newBuilder() .cacheControl(tempCacheControl) .build(); } return chain.proceed(request); } }
使用:
public static Cache cache = new Cache(new File(Environment.getExternalStorageDirectory() + "/cache"), 10 * 1024 * 1024); OkHttpClient.Builder builder = new OkHttpClient().newBuilder(); builder.retryOnConnectionFailure(true)//默认重试一次,若需要重试N次,则要实现拦截器 .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(20, TimeUnit.SECONDS) .writeTimeout(20, TimeUnit.SECONDS); builder.addInterceptor(new CacheInterceptor(cacheConfig.maxAge)).addNetworkInterceptor(new CacheNetworkInterceptor()).cache(cache); OkHttpClient okHttpClientInstance = builder.build();
ok,缓存设置到此为止。
但是总是感觉少了什么,没错就是缓存访问策略。
不是所有的页面都是这种断网情况下才访问缓存的策略。
另外一个就是这种缓存策略只能用户get请求,post请求无效。(为什么post请求需要缓存数据啊,瞎搞)
总结起来大概有以下几种策略:
优先网络
优先缓存
优先缓存,并设置超时时间
仅加载网络,但数据依然会被缓存
先加载缓存,后加载网络
仅加载网络,不缓存
缓存的最佳实践(2)---定制自己的缓存策略
所谓的缓存不就是写入文件系统,然后再取出来吗?使用DiskLruCache
即可实现磁盘缓存。
实际上实体类只需要继承Serializable
接口 就可以缓存了。
当然每个类继承Serializable
也太麻烦了吧,为什么不能直接使用Gson把类变成String,只储存String不行吗?
ok,使用Gson储存实体类的Sting没有问题,非常nice。但是会有一点小问题需要解决。
开始搞代码
写一个简单的网络访问,首先访问缓存,如果缓存不存在就访问网络。思路很清晰,要怎么做呢。
好在我们使用了Rxjava,按顺序订阅两个事件是可以的。使用concat
操作符即可。concat可以接受多个Observable
对象依次处理。
搞两个Observable
对象,一个是缓存处理,一个是网络处理。(如果你的项目用到了背压 那就用 Flowable )
缓存,使用 ACache 作为缓存工具,项目地址 https://github.com/yangfuhai/ASimpleCache
Observable<ResponeBean> cacheObservable = new Observable<ResponeBean>() { @Override protected void subscribeActual(Observer<? super ResponeBean> observer) { String d = ACache.get(getBaseContext()).getAsString(cachekey); if (d != null) { observer.onNext(new ResponeBean(2, d)); } observer.onComplete(); //去下一个 } };
网络,这里就不详细解答了 这里有个关键的东西,就是给Retrofit
添加新的ConverterFactory
通常我们使用rxjava和retrofit 只会添加 GsonConverterFactory
和RxJava2CallAdapterFactory
但是为了得到统一的数据缓存,我们在前面添加ScalarsConverterFactory
用于获取String 数据
Observable<ResponeBean> netObservable = AppDataRepository.getIndex();
合并访问:
Observable.concat(cacheObservable, netObservable).firstElement().concatMap(new io.reactivex.functions.Function<ResponeBean, MaybeSource<BannerBean>>() { @Override public MaybeSource<BannerBean> apply(final ResponeBean responeBean) throws Exception { return new MaybeSource<BannerBean>() { @Override public void subscribe(MaybeObserver<? super BannerBean> observer) { if (responeBean.state == 1) { //来自网络 缓存数据 Log.e("TAG", "访问网络数据,加入缓存"); ACache.get(getBaseContext()).put(SecretUtil.getMD5Result("banner/json"), responeBean.data); } else { Log.e("TAG", "访问缓存数据"); } Gson gson = new Gson(); Type type = new TypeToken<BannerBean>() { }.getType(); BannerBean bannerBean = gson.fromJson(responeBean.data, type); observer.onSuccess(bannerBean); } }; } }).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io()).subscribe(new MaybeObserver<BannerBean>() { @Override public void onSubscribe(Disposable d) { } @Override public void onSuccess(BannerBean bannerBean) { baseQuickAdapter.setNewData(bannerBean.getData()); } @Override public void onError(Throwable e) { Toast.makeText(getBaseContext(), e.getMessage(), Toast.LENGTH_SHORT).show(); } @Override public void onComplete() { } });
ok,我们可以看到,使用concat 访问两个数据源,同时concatMap 操作符转换数据类型。其中firstElement()
的意思是只射第一个成功的数据。
如果cacheObservable
成功拿到数据发射了observer.onNext
则netObservable
不发射数据。
测试:
优先取缓存,如果没有缓存或者缓存过时使用网络获取数据。
08-07 11:49:46.047 11897-11918/? E/TAG: 访问网络数据,加入缓存 08-07 11:56:36.060 14038-14061/? E/TAG: 访问缓存数据
封装一下,简化代码
首先建立一个类SubscriberManager
用于简化订阅过程。
那要怎么确定访问类型呢,这里使用泛型代替。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
这样就能直接处理访问类型了。
具体代码如下:
public class SubscriberManager<T> {public ResponeFunc responeFunc = new ResponeFunc(); //内部类public void toCacheSubscribe(Observable<ResponeBean> o, final MaybeObserver<T> s) { final Observable<ResponeBean> cacheObservable = new Observable<ResponeBean>() { @Override protected void subscribeActual(Observer<? super ResponeBean> observer) { String d = ACache.get(AppApplication.applicationContext).getAsString(SecretUtil.getMD5Result("banner/json")); if (d != null && d.length() > 0) { observer.onNext(new ResponeBean(2, d)); } observer.onComplete(); //去下一个 } }; Observable.concat(cacheObservable, o).firstElement().concatMap(new Function<ResponeBean, MaybeSource<T>>() { @Override public MaybeSource<T> apply(final ResponeBean responeBean) throws Exception { return new MaybeSource<T>() { @Override public void subscribe(MaybeObserver<? super T> observer) { if (responeBean.state == 1) { //来自网络 缓存数据 Log.e("TAG", "访问网络数据,加入缓存"); ACache.get(AppApplication.applicationContext).put(SecretUtil.getMD5Result("banner/json"), responeBean.data); } else { Log.e("TAG", "访问缓存数据"); } T t = (new Gson()).fromJson(responeBean.data, genericityType); observer.onSuccess(t); } }; } }).observeOn(AndroidSchedulers.mainThread()).subscribeOn(Schedulers.io()).subscribe(s); } public class ResponeFunc implements Function<String, ResponeBean> { @Override public ResponeBean apply(String s) throws Exception { return new ResponeBean(1, s); } } }
可以看到toCacheSubscribe
方法简单的处理了缓存访问和网络访问,同时通过ParameterizedType
方法获取到了泛型的Type
给gson做转换。
网络调用:
public static Observable<ResponeBean> getBanner(final String url, Map<String, String> map, MaybeObserver s) { SubscriberManager<BannerBean> subscriberManager = new SubscriberManager<BannerBean>(); Observable<ResponeBean> o = ApiClient.create(AppApiService.class).get(Constants.URL + url, map).map(subscriberManager.responeFunc); subscriberManager.toCacheSubscribe(o, s); return o; }
网络访问
AppDataRepository.getBanner("banner/json", new ArrayMap<String, String>(), new MaybeObserver<BannerBean>() { @Override public void onSubscribe(Disposable d) { } @Override public void onSuccess(BannerBean o) { baseQuickAdapter.setNewData(o.getData()); } @Override public void onError(Throwable e) { Toast.makeText(getBaseContext(), e.getMessage(), Toast.LENGTH_SHORT).show(); } @Override public void onComplete() { } });
一切完美,调用。
崩溃,再次调用 崩溃 。而且连崩溃日志都看不到。
经过仔细排查,关键点在于observer.onSuccess(t);
这一行,debug调试可以看到。
TIM截图20180808175726.png
虽然通过ParameterizedType
获得了泛型的Type,数据得到的也没有问题,但是得到的数据类型是LinkedTreeMap
com.google.gson.internal.LinkedTreeMap cannot be cast to com.xylife.community.bean.Exercise
经过查询,得到的答案是:
因为泛型在编译期间被擦除的缘故。
问一下GSON解析JSON问题?
在经过gson解析之后,泛型被解析成LinkedTreeMap,也就是那个T所代表的数据类变成了LinkedTreeMap
image
当然热心的网友给出了解决方案,我全试了一遍。都他妈行不通,都没能从根本上解决泛型擦除的问题。
因为他们的方法无论是怎么做,都需要传入一个具体类的Type才能正确转换。而弱在带泛型的类内部,无法通过泛型获取到正确的Type提供给Gson做转换。
最终我想到的是,既然gson不能使用泛型,而在SubscriberManager
内部只能使用T泛型来转换。不如通过接口将泛型解决掉。
代码如下:
接口:
public interface IGsonTobean { void toBean(String json,MaybeObserver s); }
实现接口:
AppDataRepository.getBanner("banner/json", new ArrayMap<String, String>(), new MaybeObserver<BannerBean>() { @Override public void onSubscribe(Disposable d) { } @Override public void onSuccess(BannerBean o) { baseQuickAdapter.setNewData(o.getData()); } @Override public void onError(Throwable e) { Toast.makeText(getBaseContext(), e.getMessage(), Toast.LENGTH_SHORT).show(); } @Override public void onComplete() { } }, new IGsonTobean() { @Override public void toBean(String json, MaybeObserver s) { s.onSuccess(new Gson().fromJson(json, BannerBean.class)); } });
结果正确取得。
更多的思考,在更多的搜索过程中。我发现至少有两个库或者方法解决了这个泛型擦除的问题。
1.https://github.com/z-chu/RxCache
作者提到了使用Kotlin使用内联函数避免泛型擦除问题,注意是避免而不是解决。
2.retrofit 的适配器GsonConverterFactory
同样做到了任意类型得数据转换。
也许可以参考这两个东西来简化整个过程。
按照文章的内容可知,通过内联函数。可以去掉获取Type的过程,直接传入数据类即可。同样的分析了RxCache这个库,发现只是在RxCache的load方法里面得到了数据的Type,然后将Type传到后面的Gson解析文件中。
由此可见,所谓的Kotlin也没有解决泛型擦除问题,毕竟二者基于JVM。但是使用kotlin可以只传实体类,不传实体类型,好像代码简化了那么一丢丢(没有多大意义)。
缓存策略的添加
其实这一部分就很简单了,无非就是控制缓存访问和网络访问的流程。
首先设置一个CacheConfig
,配置缓存类型和缓存时间。
通过CacheConfig
判断缓存类型。
代码过于简单,详情可见仓库地址。
作者:没有杀手的感情
链接:https://www.jianshu.com/p/89f6bea0bc39
共同学习,写下你的评论
评论加载中...
作者其他优质文章