美文网首页Android-Rxjava&retrofit&dagger我爱编程android
Retrofit2+Rxjava2添加缓存功能最佳实践(RxJa

Retrofit2+Rxjava2添加缓存功能最佳实践(RxJa

作者: 善笃有余劫 | 来源:发表于2018-08-09 17:41 被阅读124次

    缓存的必要性

    作为一个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调试可以看到。

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

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

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

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

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

    缓存策略的添加

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

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

    通过CacheConfig判断缓存类型。

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

    总结

    整个过程深入了rxjava的使用

    文章代码仓库地址

    相关文章

      网友评论

      本文标题:Retrofit2+Rxjava2添加缓存功能最佳实践(RxJa

      本文链接:https://www.haomeiwen.com/subject/qffuvftx.html