缓存的必要性
作为一个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调试可以看到。
虽然通过ParameterizedType
获得了泛型的Type,数据得到的也没有问题,但是得到的数据类型是LinkedTreeMap
com.google.gson.internal.LinkedTreeMap cannot be cast to com.xylife.community.bean.Exercise
经过查询,得到的答案是:
因为泛型在编译期间被擦除的缘故。
问一下GSON解析JSON问题?
在经过gson解析之后,泛型被解析成LinkedTreeMap,也就是那个T所代表的数据类变成了LinkedTreeMap
当然热心的网友给出了解决方案,我全试了一遍。都他妈行不通,都没能从根本上解决泛型擦除的问题。
因为他们的方法无论是怎么做,都需要传入一个具体类的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
判断缓存类型。
代码过于简单,详情可见仓库地址。
总结
整个过程深入了rxjava的使用
网友评论