美文网首页android开发Android开发精选Android
使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网

使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网

作者: never615 | 来源:发表于2016-01-15 21:13 被阅读42721次

    使用Retrofit和Okhttp实现网络缓存,更新于2016.02.02


    本文使用 Retrofit2.0.0-beta2、Okhttp 2.6.0(Okhttp3.0之后api写法有变化)

    • 配置Okhttp的Cache
    • 配置请求头中的cache-control或者统一处理所有请求的请求头
    • 云端配合设置响应头或者自己写拦截器修改响应头中cache-control

    最后实现的效果是:有网的时候根据你每个接口设置的需要缓存的时间(1分钟、5分钟等)进行缓存,过了时间重新请求;没网的时候读缓存。

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

    1.配置okhttp中的Cache

    OkHttpClient okHttpClient = new OkHttpClient();
    File cacheFile = new File(context.getCacheDir(), "[缓存目录]");
    Cache cache = new Cache(cacheFile, 1024 * 1024 * 100); //100Mb
    okHttpClient.setCache(cache);
    

    2.配置请求头中的cache-control

    缓存的相关知识和参数的说明,我是个链接1
    缓存的相关知识和参数的说明,我是个链接2

    在Retrofit中,我们可以通过@Headers来配置,如:

    @Headers("Cache-Control: public, max-age=3600)
    @GET("merchants/{shopId}/icon")
    Observable<ShopIconEntity> getShopIcon(@Path("shopId") long shopId);
    

    没有设置的可以即为有网的时候不进行缓存。

    或者你所有接口在有网的时候都不需要缓存或者都需要缓存且时间一样,那么也不用配置每个接口的@Headers的Cache-Control了。

    3.云端配合设置响应头或者自己写拦截器修改响应头response中cache-control

    到这一步缓存就已经待在你的缓存目录了。
    如果云端有处里cache的话,就已经可以了。
    但是很可能云端没有处理,所以返回的响应头中cache-control是no-cache,这时候你还是无法做缓存,大家可以用okhttp的写日志拦截器查看响应头的内容。

    [ Okhttp Interceptors 使用说明,我是个链接](https://github.com/square/okhttp/wiki/Interceptors" target="_blank)

    如果云端现在不方便处理的话,你也可以自己搞定缓存的,那就是写拦截器修改响应头中的cache-control。我把请求头中的cache-control读出来然后设置到了响应头中。

    设置拦截器:
    REWRITE_CACHE_CONTROL_INTERCEPTOR拦截器需要同时设置networkInterceptors和interceptors(OKHTTP3.0配置是否有效待我测试)

    okHttpClient.interceptors().add(LoggingInterceptor);
    okHttpClient.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);
    okHttpClient.interceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);
    

    拦截器如下:云端响应头拦截器,用来配置缓存策略

    /**
     * 云端响应头拦截器,用来配置缓存策略
     * Dangerous interceptor that rewrites the server's cache-control header.
     */
    private final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
        Request request = chain.request();
        if(!NetUtils.hasNetwork(context)){
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
            Logger.t(TAG).w("no network");
        }
        Response originalResponse = chain.proceed(request);
        if(NetUtils.hasNetwork(context)){
            //有网的时候读接口上的@Headers里的配置,你可以在这里进行统一的设置
            String cacheControl = request.cacheControl().toString();
            return originalResponse.newBuilder()
                    .header("Cache-Control", cacheControl)
                    .removeHeader("Pragma")
                    .build();
        }else{
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=2419200")
                    .removeHeader("Pragma")
                    .build();
        }
    };
    
    

    最后日志拦截器也贴上来吧

    private final Interceptor LoggingInterceptor = chain -> { 
        Request request = chain.request(); 
        long t1 = System.nanoTime();
        Logger.t(TAG).i(String.format("Sending request %s on %s%n%s", request.url(),  chain.connection(), request.headers()));
        Response response = chain.proceed(request); 
        long t2 = System.nanoTime(); 
        Logger.t(TAG).i(String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); 
        return response; 
    };
    
    

    以下测试Cache-Control的配置在请求头和响应头中都有且一样。

    max-stale在请求头设置有效,在响应头设置无效。(因为max-stale是请求头设置参数,参考上面的缓存相关的知识第二个链接)
    max-stale和max-age同时设置的时候,缓存失效的时间按最长的算。
    关于max-age和max-stale我这里做了一个测试:
    测试结果:
    我在请求头中设置了:Cache-Control: public, max-age=60,max-stale=120,响应头的Cache-Control和请求头一样。

    • 在第一次请求数据到一分钟之内,响应头有:Cache-Control: public, max-age=60,max-stale=120
    • 在1分钟到3分钟在之间,响应头有:Cache-Control: public, max-age=60,max-stale=120
      Warning: 110 HttpURLConnection "Response is stale"
      可以发现多了一个Warning。
    • 三分钟的时候:重新请求了数据,如此循环,如果到了重新请求的节点此时没有网,则请求失败。

    另外关于缓存有一个rxcache也可以试试。

    感谢@Picasso_L一起讨论研究

    相关文章

      网友评论

      • 暴走的Jacky:Interceptor的拦截顺序是:
        1.首先执行用户自定义的,这里是你自己的CacheInterceptor
        也就是说,你这里的response是没有写入到缓存目录中去的,那你后续取出来的response的header的数据里面的Cache-Controll还是noCahce吧
      • lvTravler:请问作者亲自试验过吗?
      • lvTravler:你的数据是怎样缓存的?
      • 不说话的唐僧:你这个还是错的啊
      • 飞天舞乐:谢谢楼主分享,我封装了一下,让使用起来更加方便了,欢迎大家提意见:
        https://github.com/yale8848/RetrofitCache
      • 崔小妖:不希望所有的接口都实现缓存 只想要 某个url 有缓存 的话应该怎样实现呢
      • AndroidHarry:你好作者,我这边有个问题,就是加入同意配置了缓存,每个接口都会缓存,那么假如我有写接不需要缓存我怎么办呢?还有下拉加载我只需缓存第一页的数据我该怎么办呢?我的qq1106919334 希望讨论下 非常感谢
      • 5a0c727074e1:你好,我关闭网路了,但是拉取数据还是跑504没读到缓存,能加qq交流吗。或者私信。?
      • distancelin:博主你好,有个问题想请教下,max-stale在响应头里设置无效,那么为什么还要在拦截器里面设置max-stale呢?
        return originalResponse.newBuilder()
        .header("Cache-Control", "public, only-if-cached, max-stale=2419200")
        .removeHeader("Pragma")
        .build();
        不说话的唐僧:@黑土地_a80f 对的
        黑土地_a80f:我觉得也没有意义了,对于okhttp缓存的保存是在用户端自定义拦截器之前完成保存的,所以此时的response中header中设置的自定义的值不会对缓存的存储起什么作用。
      • ae12:我和你代码一样,可是响应头Cache-Contro =no cache,怎么回事?
      • Jlanglang:我照着文中,设置了,为什么关了网之后get请求还是读不了缓存?

        我的需求是:有网走网络不走缓存,无网走缓存.

        楼主能解答下嘛:smile:
        Jlanglang: Interceptor cacheInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        //有网的时候,读接口上的@Headers里的注解配置
        String cacheControl = request.cacheControl().toString();
        //没有网络并且添加了注解,才使用缓存.
        if (!Utils.isOpenInternet()&&!TextUtils.isEmpty(cacheControl)){
        //重置请求体;
        request = request.newBuilder()
        .cacheControl(CacheControl.FORCE_CACHE)
        .build();
        }
        //如果没有添加注解,则不缓存
        if (TextUtils.isEmpty(cacheControl)) {
        //响应头设置成无缓存
        cacheControl = "no-cache";
        }
        //返回设置好的响应体
        Response response = chain.proceed(request);
        HLog.i("httpInterceptor", cacheControl);
        return response.newBuilder()
        .header("Cache-Control", cacheControl)
        .removeHeader("Pragma")
        .build();

        }
        };
        我现在是这样设置的
        Jlanglang:@never615 解决了.设置缓存的Interceptor,我没有 addInterceptor(),只设置了addNetworkInterceptor();
        never615:@Jlanglang 把你代码贴上来
      • iamzk:max-stale在请求头设置有效,在响应头设置无效。这句什么意思?如果响应头设置无效,那么设置响应头的max-stale还有什么用?
        return originalResponse.newBuilder()
        .header("Cache-Control", "public, only-if-cached, max-stale=2419200")
        .removeHeader("Pragma")
        .build();
        never615:@iamzk max-stale是请求头设置参数,响应头只是把请求头的请求返回回去.
        这个断网情况下对应的请求头的max-stale在代码中传的实际是Integer.MAX_VALUE,所以按理说也应该返回这个值.
        你可以删掉试一下,看看还可不可以.做一下测试

        正常使用应该是,请求者设置max-stale请求服务器,服务器根据这个参数做出相应的响应.返回的响应头还带着这个参数返回.咱们这个做法是本地自己拦截改了响应,正常响应是不会主动设置这个参数的.你可以自己改改做做测试.
      • 1c2222791dc8:如果我又的请求不想被缓存 怎么办 ,感觉所有的请求都被缓存了
        never615:@倚剑天涯 你设置了缓存请求头的才会缓存啊
      • 冰点k:你好,有网的那个统一设置缓存那里怎么设置啊,为什么我设置public, max-age=60后,有网的时候还是不读缓存啊
        冰点k:已经解决,忽略了 需要同时设置networkInterceptors和interceptors 这个,谢谢楼主的分享
      • Awanwan:刷新如何控制
        never615:@Launching_ 和浏览器一样,刷新请求和你想要缓存的请求头不一样,比如刷新的时候请求头cache-control 设置max-age 0,做缓存最好能和后端协商一下一起做,实在没办法你可以用这种比较hacker的方式
      • Henry2Sunshine:博主,请教个问题,这个缓存只针对 GET请求有效吧,Post请求我试过没效果,不知道是不是这样的还是说我有什么配置错误了,求解答
        Henry2Sunshine:谢咯 前一段时间做的时候,发现post怎么都不生效,所以确实困扰了好久,还以为,我自己配置有什么问题。现在就明了 :+1:
        never615:@HenrySunshine 应该是只对get有用,之前很多人都反应了,说实话我没试过,我之前需要缓存的也都是get请求,而且我不做android很久了。。
      • 0e472e000fe6:缓存写入成功了,还是读不到缓存
        恨自己不能小清新:@跳跳王 666 单用okhttp在request添加request.cacheControl(CacheControl.FORCE_CACHE)可以强制读取缓存。
      • wan7451:post 请求 怎么处理?
      • 1f2c39825533:POST缓存不行吗?
      • 8e2c8072e73c:感谢,我正准备做缓存来着,
      • ElyarAnwar:请问过期的缓存是如何处理的?需要手动处理吗
      • sunbinqiang:用这个方法addNetworkInterceptor, 缓存才会生效, addInterceptor 缓存不生效
      • _醉生梦死:博主我现在有网络的缓存60S已经跑通了。我有2个疑问,希望能帮我解决下。麻烦了。1。就是没有网络的时候那个设置的60S超时时间是什么意思啊。没有太理解。2就是为啥第二种缓存要判断2次网络情况啊。求大腿教教啊。
      • InnerNight:请问这样的缓存机制是无差别缓存吗?对所有的网络请求?还是所有的get请求?那像登录、post操作这种,怎么告诉底层不缓存呢?
        InnerNight:@never615 我问的是添加interceptor那种 :smiley:
        InnerNight:@never615 请教下怎么根据request设置?
        never615:@InnerNight post不缓存,每一个请求是否缓存,缓存时间都可以单独设置
      • z彭:http://mushuichuan.com/2016/03/01/okhttpcache/ 这篇文章和楼主的有点类似。我3.0的用他的方法也可以实现。
      • cc8367cdc9fa:楼主请问下,数据缓存下来之后,在未到过期时间请求那应该是走缓存的,这个代码是在哪一块实现的?
        never615:@放慢心跳111 拦截里判断有网做的操作
      • efc88684a24d:楼主不妥呀
        Cache cache = provideCache();
        OkHttpClient client = new OkHttpClient().newBuilder()
        .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
        .cache(cache)
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build();
      • 68768b474bfc:.removeHeader("Pragma")的作用是什么
        never615:@TellH 这个头会禁用缓存
      • z_ym:出现了以下这个报错
        Http 504 Unsatisfiable Request(only-if-cache)
        z_ym:@never615 可以是可以,只是想知道为什么会发生这样的错误。大神如果研究出来发给我哦,谢谢
        never615:@z_ym 可以把异常捕获处理一下吗
        z_ym:@z_ym 这个是因为该url没有访问过,没有缓存存在,但是怎么解决还不知道,大神,快来帮帮我。
      • 墨鬁:我想问问,这个缓存策略是根据 请求的 url 和参数来决定是取本地缓存,还是取服务器数据的么? 如果url和参数和缓存中的一样,才根据Cache-Control 来决定取缓存,还是刷新数据么??
      • 57855e86d40d:十分之强大!
      • 763cba3b6161:上面的评论问问题的人不要乱猜了,直接看英文文档你就明白为什么!

        英文文档: https://msdn.microsoft.com/en-us/library/27w3sx5e(v=vs.110).aspx
      • 763cba3b6161:之前贪图方便, 看了这篇"中文"的文档, 结果........

        到头还是去看了英文的文档才能没有歧义地去理解

        英文文档: https://msdn.microsoft.com/en-us/library/27w3sx5e(v=vs.110).aspx
        never615:@lihansey 而且我也没想在文章中讲这些关系,我想的就是直接明了的说明,不讲原理,直接看了按这个配置就可以实现功能,你不需要知道为什么就可以用了.
        因为我看文章也有点讨厌那种就是要讲一个使用的东西,开始就讲各种原理.我的想法是我需要知道这些原理我就去搜这些原理了,所以我也看过你发的那个文档.我想知道一个东西怎么用,那就直说怎么用.
        never615:@lihansey 微软的文档我在写文章之前就看过了.....
      • 71caf019970b:好吧。。懂了,你用了那个第三方的 lambda。。
      • 71caf019970b:chain是什么啊。没见到声明。
      • 2a36d9bceae4:请教一下,数据缓存下来之后,在未到过期时间请求那应该是走缓存的;但比如这个场景,我下拉刷新需要重新拉取新的数据,而不是走缓存该怎么做呢?
        never615:@赛飞 登录接口吗?登录接口是post不会缓存
        never615:@2a36d9bceae4 在写一个接口给刷新用吧,或者配置一个自定义header做标识,在拦截器中自己处理。
        71caf019970b:@2a36d9bceae4 同问。而且如果是登录,这个肯定不想让他缓存,怎么设置。
      • 0afb23927996:调试过程中出现了这样的错误,不知如何解决,请指教
        com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
        Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $
        class java.lang.String
        dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.yoloho.lv.httpproject-1/base.apk"],nativeLibraryDirectories=[/data/app/com.yoloho.lv.httpproject-1/lib/arm64, /vendor/lib64, /system/lib64]]]
        never615:@0afb23927996 json有问题,先打印看看,是一个接口有问题还是都有?
      • 0afb23927996:追加一下,那个url,可以通过log日志打印,这个调通了
      • 0afb23927996:楼主,刚刚学习这个,一直有个疑问,首先 如何打印出请求的整个url呢? 其次如何设计翻页查询,目前是拿到请求的句柄,调用异步或同步接口执行,接下来再执行,必须克隆,才可以调用,如果是翻页,那么参数就已经需要改变了,该怎么处理?请大神赐教...
        never615:@0afb23927996 就是要重新传参数
        0afb23927996:@never615 后台接口的设计已经存在,现在说的是客户端针对网络访问这里的处理,比如我拿到了Call请求一次,接下来处理翻页,我需要重新生成call,重新传入参数,那这样做是不是不太好? 拿到的call句柄,再次使用的时候必须clone()才可以继续调用,如果此时参数不更新那起到的不是翻页作用啊?
        never615:@0afb23927996 分批加载和分页加载,可以可以通过参数页数来控制,后台写接口的应该知道这些,如果是分批加载,你拿到新数据追加到集合,刷新你的recyclerview
      • passerbywhu:但是很可能云端没有处理,所以返回的响应头中cache-control是no-cache,这时候你还是无法做缓存,大家可以用okhttp的写日志拦截器查看响应头的内容。

        这里有问题吧。response里的no-cache的意思仅仅是说,在你从缓存中取数据之前要先去服务端做验证,而不是说不能缓存。其实客户端还是做了缓存的。
        这个在OKHttp的注释里写的很清楚。
        * In a response, this field's name "no-cache" is misleading. It doesn't
        * prevent us from caching the response; it only means we have to validate the
        * response with the origin server before returning it. We can do this with a
        * conditional GET.
        passerbywhu:@ae12 客户端有缓存的情况。进行网络请求读不读取缓存看的是你request请求的header中cache-control的内容,不是看response头信息。你要在request头里面设置only-if-cached, max-stale
        ae12:客户端是做了缓存,但是在没有网络情况下,也不读取本地缓存的数据,请问是不是响应头response里的Cache-Control: no-cache的问题?还是什么其它问题?
        never615:@passerbywhu 是的,我说的不能做缓存,不是本地没有缓存,表达的不准确
      • 9711922c6b29:我用楼主的方法试了下,发现OkHttp没有给我写缓存的文件。
        请求 @Headers("Cache-Control public,max-age=640000") 也加了 interceptor 打印的日志 显示 请求 和 响应 的 Cache-Control 都是我改之后的了。
        可是就是没有写缓存文件-。-,缓存路径:getExternalCacheDir(),大小 20*1024*1024
        还有可能有别的什么因素会导致不写缓存么?
        我看_set_cookie 里一长串 还跟着个 only-http 会不会和这个有关?
        9711922c6b29:@never615 就是普通get请求,我换成addNetworkInterceptor 就可以缓存了。话说楼主 使用addInterceptor 读取缓存的数据 也会走这个 然后你对已缓存的数据又设置了 max-age,会不会导致缓存时间延长?
        never615:@水手辛巴 你的请求类型?
        9711922c6b29:@水手辛巴 retrofit 2.0.1
      • colin210:Version 2.0.0 (2016-03-11),这是最新的版本,希望哥们再看一下,有没有改动,要给读者最好的体验不是嘛 :blush: 。迭代更新也是程序员的良好习惯,感谢分享!
      • 小池laucherish:写得很不错,赞一个。学习了。
      • 04f629ba3db8:lz,关于 max-age与max-stale优先级的问题

        我在@head 里面添加Cache-Control,有网络时也会一直使用缓存到max-stale时间结束,在Interceptor 的request中设置Cache-Control 却 只作用于max-age
        never615:@04f629ba3db8 文章又更新了。。现在实现了有网的时候根据接口设置的过期时间进行重新请求,没网的时候读缓存。
        04f629ba3db8:@never615 谢谢答复了~~缓存这块困惑好久,以前都是写文件缓存
        never615:@04f629ba3db8 我理解错了,不应该同时设置这两个参数,文章已经更新了
      • 04f629ba3db8:楼主,[假设你设置了100s的max-age,1000s的max-stale,在没网的时候,过100s,你还能读缓存;有网的时候过了100s,就重新请求。]
        int maxStale = 60 * 60 * 24 * 2;
        return response.newBuilder().removeHeader("Cache-Control") .header("Cache-Control", "max-age=30,max-stale=" + maxStale).build();

        我开飞行模式,超过max-age时间后,maxStale属性没有起作用,并没有读取缓存?


        3e76bf9c5a4f:@04f629ba3db8 层主,你这个问题解决了?我现在也遇到这问题。maxStale属性没起作用,求问怎么解决
        04f629ba3db8:@never615 在@header 配置cache control是可以的,或者在Interceptor 的request上设置head是 生效【之前搜索出来的都是 在Response 中设置cache control,没想到 request 与response要同时设置】
        never615:删掉removeHeader("Cache-Control") ,要是get请求,你的云端借口有@header配置cache control吗
      • Zane96:请问将retrofit和okhttp一起使用的目的就是方便添加请求头吗?
        never615:@飞起来的小糖豆 retrofit配合rxjava很好用
        飞起来的大雨:@never615 我感觉OKHTTP 用就可以 现在出个retrofit感觉好别扭
        never615:@Zane96 不是的,retrofit用起来爽的不行,很方便啊,还可以配合rxjava,如果你的云端是标准的restful api的话,推荐使用。至于okhttp,retrofit开始就可以设置不同的底层网络库,okhttp好用就用了他罢了。android6.0开始底层网络库踢掉了apache的换了okhttp了都。。像常用的glide也可以配合okhttp
      • KennethYo:楼主,我们最近才切换到 retrofit 2.0,你碰到过响应 body 里面的 Json,中文显示为 utf-8码吗?
        ab917d60b572:@KennethYo 请问解决了吗,我们传的json也遇到了乱码的entire:
        @FormUrlEncoded
        @POST("forward/default")
        Observable<CommonDataResponse> commonRequest(@Header("cookie") String cookie, @field("content") String context);
        never615:@KennethYo 我想我们中文就用的utf-8吧,所以没有遇到乱码
      • Picasso_L:楼主方便留下qq吗,有个问题和你讨论下

      本文标题:使用Retrofit和Okhttp实现网络缓存。无网读缓存,有网

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