美文网首页Android知识库学习区Rxjava相关
使用Retrofit2.0+OkHttp3.0实现缓存处理

使用Retrofit2.0+OkHttp3.0实现缓存处理

作者: wanbo_ | 来源:发表于2016-07-29 21:06 被阅读4309次

    最近在写一个信息流的项目,整个架构是基于 MVP + Retrofit + Rxjava 实现的,由于刚刚使用 RxJava + Retrofit,对它理解不深,所以在一开始做数据缓存的时候还是用常规思维来设计的。

    想到的缓存处理方式:

    • 使用 sharedpreferences
    • 使用 SqLite 数据库

    但是有一个问题:

    • Retrofit + RxJava 之所以强大,有一点,是可以直接将返回的JSON数据转化为我们的 JavaBean 对象,直接操作。
    • 如果使用常规方式处理,我们只是缓存JSON数据,在操作的时候还是要通过GSON转化为对象。
    • 那这样,我们就没有体现出 Retrofit 的强大之处,所以我想如果Retrofit能够做缓存处理就好了。

    这里要吐槽一句,网上关于 Retrofit 和 RxJava 的相关资料真的很少,而且大部分都是重复或只写了一个片段,但功夫不负有心人,还是找到了一些解决方法。

    先说一下为什么要做缓存处理?

    有一篇文章是这样说的:

    减少服务器负荷,降低延迟提升用户体验。复杂的缓存策略会根据用户当前的网络情况采取不同的缓存策略,比如在2g网络很差的情况下,提高缓存使用的时间;不用的应用、业务需求、接口所需要的缓存策略也会不一样,有的要保证数据的实时性,所以不能有缓存,有的你可以缓存5分钟,等等。你要根据具体情况所需数据的时效性情况给出不同的方案。当然你也可以全部都一样的缓存策略,看你自己。

    Retrofit+OkHttp的缓存机制

    • 在响应请求之后在 data/data/<包名>/cache 下建立一个response 文件夹,保持缓存数据。
    • 这样我们就可以在请求的时候,如果判断到没有网络,自动读取缓存的数据。
    • 同样这也可以实现,在我们没有网络的情况下,重新打开App可以浏览的之前显示过的内容。
    • 也就是:判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取。

    缓存实现方式

    1. 先开启OkHttp缓存

      在Retrofit2.0版本之后,Retrofit底层自动依赖了OkHttp,所以我们不用重复依赖Okhttp了

      File httpCacheDirectory = new File(MyApp.mContext.getCacheDir(), "responses");
      int cacheSize = 10 * 1024 * 1024; // 10 MiB
      Cache cache = new Cache(httpCacheDirectory, cacheSize);
      
      OkHttpClient client = new OkHttpClient.Builder()
              .addInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
              .cache(cache).build();
      

      这一步是设置缓存路径,以及缓存大小,其中addInterceptor是我们第二步的内容。

    2. 设置 OkHttp 拦截器

      主要是拦截操作,包括控制缓存的最大生命值,控制缓存的过期时间

      两个操作都是在 Interceptor 中进行的

      • 通过 CacheControl 控制缓存数据
        CacheControl.Builder cacheBuilder = new CacheControl.Builder();
        cacheBuilder.maxAge(0, TimeUnit.SECONDS);//这个是控制缓存的最大生命时间
        cacheBuilder.maxStale(365,TimeUnit.DAYS);//这个是控制缓存的过时时间
        CacheControl cacheControl = cacheBuilder.build();
        
    • 设置拦截器
    Request request = chain.request();
    if(!StateUtils.isNetworkAvailable(MyApp.mContext)){
        request = request.newBuilder()
                .cacheControl(cacheControl)
                .build();
    }
    Response originalResponse = chain.proceed(request);
    if (StateUtils.isNetworkAvailable(MyApp.mContext)) {
        int maxAge = 60; // read from cache
        return originalResponse.newBuilder()
                .removeHeader("Pragma")
                .header("Cache-Control", "public ,max-age=" + maxAge)
                .build();
    } else {
        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
        return originalResponse.newBuilder()
                .removeHeader("Pragma")
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .build();
    }
    

    可以看到上面两个有设置了相同的内容,有什么区别呢?

    有篇文章是这样解释的:

    如果.maxAge(0,TimeUnit.SECONDS)设置的时间比拦截器长是不起效果,如果设置比拦截器设置的时间短就会以这个时间为主,我觉得是为了方便控制。.maxStale(365, TimeUnit.DAYS)设置的是过时时间,我觉得okthhp缓存分成了两个来考虑,一个是为了请求时直接拿缓存省流量,一个是为了下次进入应用时可以直接拿缓存。

    全部代码

    通过这样,我们就可以直接使用同一个Retrofit请求方法,无论是最新数据还是缓存数据,都可以转化为我们需要的对象,直接来使用。

    weiBoApiRetrofit() {
    
        //cache url
        File httpCacheDirectory = new File(MyApp.mContext.getCacheDir(), "responses");
        int cacheSize = 10 * 1024 * 1024; // 10 MiB
        Cache cache = new Cache(httpCacheDirectory, cacheSize);
    
        OkHttpClient client = new OkHttpClient.Builder()
                .addInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
                .cache(cache).build();
    
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
    
        WeiBoApiService = retrofit.create(WeiBoApi.class);
    }
    
      //cache
      Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
    
          CacheControl.Builder cacheBuilder = new CacheControl.Builder();
          cacheBuilder.maxAge(0, TimeUnit.SECONDS);
          cacheBuilder.maxStale(365,TimeUnit.DAYS);
          CacheControl cacheControl = cacheBuilder.build();
    
          Request request = chain.request();
          if(!StateUtils.isNetworkAvailable(MyApp.mContext)){
              request = request.newBuilder()
                      .cacheControl(cacheControl)
                      .build();
          }
          Response originalResponse = chain.proceed(request);
          if (StateUtils.isNetworkAvailable(MyApp.mContext)) {
              int maxAge = 0; // read from cache
              return originalResponse.newBuilder()
                      .removeHeader("Pragma")
                      .header("Cache-Control", "public ,max-age=" + maxAge)
                      .build();
          } else {
              int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
              return originalResponse.newBuilder()
                      .removeHeader("Pragma")
                      .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                      .build();
          }
        };
    }
    

    注意的问题

    • 缓存是在每一次网络请求之后,重新保存的,所以在超过缓存过期时间后,Retrofit会在检查到没缓存之后自动请求网络服务器数据,这里要自己处理好后续的操作,比如弹个吐司什么的告诉用户没有网络了。
    • 缓存数据也是需要网络下载的,所以在网络不好的情况下,可能不能立即缓存,这也是我之前犯晕的地方:明明已经设置好缓存了,为什么有时候有缓存,有时候没有呢?- -真是对自己的智商捉急。

    相关文章

    Contact Me

    因为我也是刚刚接触 Retrofit + RxJava ,所以有写的不好或不对,以及表达不清楚的地方,请及时指出,我及时修改,PS:不过上面的代码在我测试后是完全可以进行缓存的,希望可以帮到你们。

    相关文章

      网友评论

      • 追梦_10c1:楼主,设置了缓存,断网请求报这个错:Unable to resolve host "...": No address associated with hostname,是不是域名域名的问题,他报我BaseURl,http://xx.xx.cn,就是报http://后面的那串错,求解!谢谢
        广成de微博:okhttp 3.8之后, dns通过域名找不到ip就直接报错了...
        就是说离线必报错
      • 63d7328b70bf:请问楼主为什么用了拦截器但是在没网状况下控制台还是提示说连不上服务器啊,不是应该被拦截器拦截不会访问服务器了吗?

        求解答,用了retrofit2的post方法,这个代码能拦截吗?
        蟹蟹!
        6c64cae1cae0:@听任蔓草堙路 后台所有的接口都是post,怎么破
        63d7328b70bf:@听任蔓草堙路 虽然用了post方法,但实际上没有给服务器传数据,而是从服务器端获取数据,是为了安全考虑才用了post,所以就想把得到的数据缓存到本地。
        但是现在,没网的时候拦截了对服务器的请求,在测试机里也看到了缓存目录里的缓存文件,打开了也开不太懂,可是弹出了“HTTP 504 Unsatisfiable Request (only-if-cached)”错误,网上有人说是因为没网时要求缓存但是缓存里没东西,楼楼,你知道怎么解决吗?
        如果能帮助我就太好了!谢谢!
        wanbo_:@邓富贵儿 一般get方法会做缓存处理,post干嘛要做缓存?要缓存什么呢?
      • boboyu:为啥我这样写了之后缓存文件夹不出来呢
      • de89be2fcf9a:目前有能缓存post请求的缓存方式吗
        de89be2fcf9a:@听任蔓草堙路 可能和大神的见解不一样 不过还是谢谢了
        wanbo_:@刘婧琳 post 一般不缓存数据呀...
      • risemoon:问问没有网络时设置:request设置的cachecontrol的maxage设置0是干嘛的?maxstale是表示没网络时打开应用缓存能显示的时间吗,与没有网络时response设置的maxstale有没有关系?response的maxstale设置为4周,假如cachecontrol的maxstale设置1天的话,是不是1天之后打开应用就不能显示缓存了,那这个4周有什么用?
        有网络时设置:response设置maxage为60或是0有什么区别?设置0代表每次加载网页都重新获取数据而不拿缓存吗?
        解答下,困扰好一会了。。
      • Jafir:你好 问题解决了 是没有使用subscriber或者observer,用的action没有处理onError出现的问题。
      • Jafir:你好,我遇到一个问题就是 如果没有网络 并且又没有缓存的话,就会报错 java.lang.IllegalStateException: Exception thrown on Scheduler.Worker thread. Add `onError` handling.
        rx.exceptions.OnErrorNotImplementedException: HTTP 504 Unsatisfiable Request
        意思就是rxjava没有捕获到这个异常,并且,这个异常在scheduler worker线程里面
        请问怎么解决呢?
        wanbo_:@Jafir 5xx 是服务器错误,有网的情况会清空缓存,同时请求服务器,把新的响应内容设置为缓存,但如果服务器出现错误,就会造成你这样的情况...不一定是大错,可能只是超时等,同时这里报错的内容“onError”,是说明你没有对请求出错时做处理,如果你是用的普通retrofit异步请求,在onFailure里面对异常做一些处理,如果是配合RxJava,在subscribe里面做处理。
      • 最最最最醉人:为什么在文件管理系统里看到了缓存的文件,但是在没有网的状态下还是没有数据呢?
        wanbo_:@最最最最醉人 先看正常状态下能不能取到数据,还要注意一点,缓存的机制是在无网条件下,但是有一个情况,只要是有网,但网络情况特别差的时候,还是会请求服务器,出现加载不出的情况。另外5xx错误是服务器错误,结合服务器查看一下。
        最最最最醉人:@一只奇思妙想会做白日梦的喵 这个怎么查看呢? 好像是返回了504的错误
        wanbo_:@最最最最醉人 需要知道控制台okhttp的trace提示是什么?

      本文标题:使用Retrofit2.0+OkHttp3.0实现缓存处理

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