为了账号安全,请及时绑定邮箱和手机立即绑定

Retrofit2+Rxjava2添加缓存功能最佳实践(RxJava的进阶使用)

标签:
MySQL

缓存的必要性

作为一个APP,必须为客户的流量做到最优化。同时能在无网的情况下不显示一个光秃秃的列表。所以缓存非常有必要

Retrofit2+Rxjava2组合的网络访问框架

这个不用多说,基本上现在最流行的网络访问模式了。同时Retrofit2基于okhttp3,所以可以基于okhttp做更多的定制。

缓存的最佳实践(1)---基于okhttp header的网络缓存

首先是支持缓存的 开启okhttp的缓存只需要给OkHttpClient设置两个Interceptor即可(拦截器),最后cache(cache)即可。

addInterceptoraddNetworkInterceptor这两种。他们的区别简单的说下,不知道也没关系,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 只会添加 GsonConverterFactoryRxJava2CallAdapterFactory
但是为了得到统一的数据缓存,我们在前面添加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.onNextnetObservable不发射数据。

测试:
优先取缓存,如果没有缓存或者缓存过时使用网络获取数据。

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调试可以看到。

webp

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

webp

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同样做到了任意类型得数据转换。

也许可以参考这两个东西来简化整个过程。

关于使用Kotlin内联函数简化Gson解析

按照文章的内容可知,通过内联函数。可以去掉获取Type的过程,直接传入数据类即可。同样的分析了RxCache这个库,发现只是在RxCache的load方法里面得到了数据的Type,然后将Type传到后面的Gson解析文件中。

由此可见,所谓的Kotlin也没有解决泛型擦除问题,毕竟二者基于JVM。但是使用kotlin可以只传实体类,不传实体类型,好像代码简化了那么一丢丢(没有多大意义)。

缓存策略的添加

其实这一部分就很简单了,无非就是控制缓存访问和网络访问的流程。

首先设置一个CacheConfig,配置缓存类型和缓存时间。

通过CacheConfig判断缓存类型。

代码过于简单,详情可见仓库地址。



作者:没有杀手的感情
链接:https://www.jianshu.com/p/89f6bea0bc39


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消