美文网首页android开发RxJAva OKHttp RetrofitRxJava和Retrofit
Retrofit 源码解读之离线缓存策略的实现

Retrofit 源码解读之离线缓存策略的实现

作者: mrwangyong | 来源:发表于2016-01-15 18:02 被阅读3087次

    Retrofit 源码解读之离线缓存策略的实现

    相关代码已上传至 GitHub,已开源库,请移步:
    | 优雅的给 Retrofit 加上缓存 RetrofitCache

    Retrofit 是square公司开发的一款网络框架,也是至今Android网络请求中最火的一个,配合OkHttp+RxJava+Retrofit三剑客更是如鱼得水,公司项目重构时,我也在第一时间使用了RxJava+Retrofit,使用过程中遇到的一些问题,也会在后续的博客中,一点点分享出来,供大家参考!

    在项目的过程中,项目需求需要在离线的情况下能够继续浏览app内容,第一时间想到缓存,于是经过各种google搜索,得出以下结论(使用Retrofit 2.0)

    -参考stackoverflow地址 ,Retrofit 2.0开始,底层的网络连接全都依赖于OkHttp,故要设置缓存,必须从OkHttp下手

    -具体的使用过程为:1.先开启OkHttp缓存

    File httpCacheDirectory = new File(UIUtils.getContext().getExternalCacheDir(), "responses");
    client.setCache(new Cache(httpCacheDirectory,10 * 1024 * 1024));
    

    我们可以看到 先获取系统外部存储的缓存路径,命名为response,此文件夹可以在android/data/<包名>/cache/resposes看到里面的内容,具体OkHttp是如何做到离线缓存的呢?

    我们进入Cache类,有重大发现,首先是它的注释,极其详细

    Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and bandwidth.
    Cache Optimization
    To measure cache effectiveness, this class tracks three statistics:
    Request Count: the number of HTTP requests issued since this cache was created.
    Network Count: the number of those requests that required network use.
    Hit Count: the number of those requests whose responses were served by the cache.
    Sometimes a request will result in a conditional cache hit. If the cache contains a stale copy of the response, the client will issue a conditional GET. The server will then send either the updated response if it has changed, or a short 'not modified' response if the client's copy is still valid. Such responses increment both the network count and hit count.
    The best way to improve the cache hit rate is by configuring the web server to return cacheable responses. Although this client honors all HTTP/1.1 (RFC 7234) cache headers, it doesn't cache partial responses.
    Force a Network Response
    In some situations, such as after a user clicks a 'refresh' button, it may be necessary to skip the cache, and fetch data directly from the server. To force a full refresh, add the no-cache directive:
        
    Request request = new Request.Builder()
        .cacheControl(new CacheControl.Builder().noCache().build())
        .url("http://publicobject.com/helloworld.txt")
        .build();
    
    If it is only necessary to force a cached response to be validated by the server, use the more efficient max-age=0 directive instead:
        
        Request request = new Request.Builder()
            .cacheControl(new CacheControl.Builder()
                .maxAge(0, TimeUnit.SECONDS)
                .build())
            .url("http://publicobject.com/helloworld.txt")
            .build();
      
    Force a Cache Response
    Sometimes you'll want to show resources if they are available immediately, but not otherwise. This can be used so your application can show something while waiting for the latest data to be downloaded. To restrict a request to locally-cached resources, add the only-if-cached directive:
        
          Request request = new Request.Builder()
              .cacheControl(new CacheControl.Builder()
                  .onlyIfCached()
                  .build())
              .url("http://publicobject.com/helloworld.txt")
              .build();
          Response forceCacheResponse = client.newCall(request).execute();
          if (forceCacheResponse.code() != 504) {
            // The resource was cached! Show it.
          } else {
            // The resource was not cached.
          }
      
    This technique works even better in situations where a stale response is better than no response. To permit stale cached responses, use the max-stale directive with the maximum staleness in seconds:
        
        Request request = new Request.Builder()
            .cacheControl(new CacheControl.Builder()
                .maxStale(365, TimeUnit.DAYS)
                .build())
            .url("http://publicobject.com/helloworld.txt")
            .build();
      
    The CacheControl class can configure request caching directives and parse response caching directives. It even offers convenient constants CacheControl.FORCE_NETWORK and CacheControl.FORCE_CACHE that address the use cases above.
    

    文档详细说明了此类的作用,支持OkHttp直接使用缓存,然后罗列出了各种具体的用法,可惜的是我们这里使用的是Retrofit,无法直接用OkHttp;但是如果有直接用OkHttp的童鞋们,可以根据上面的提示,完成具体的缓存操作,so easy !。

    回到Retrofit,通过阅读上面的文档,我们知道还有一个类,CacheControl类,主要负责缓存策略的管理,其中,支持一下策略策略如下:

    1.  noCache  不使用缓存,全部走网络
    2.  noStore   不使用缓存,也不存储缓存
    3.  onlyIfCached 只使用缓存
    4.  maxAge  设置最大失效时间,失效则不使用 需要服务器配合
    5.  maxStale 设置最大失效时间,失效则不使用 需要服务器配合 感觉这两个类似 还没怎么弄清楚,清楚的同学欢迎留言
    6.  minFresh 设置有效时间,依旧如上
    7.  FORCE_NETWORK 只走网络
    8.  FORCE_CACHE 只走缓存
    

    通过上面的CacheControl类,我们很快就能指定详细的策略

    首先,判断网络,有网络,则从网络获取,并保存到缓存中,无网络,则从缓存中获取

    所以,最终的代码如下

    -首先,给OkHttp设置拦截器

    client.interceptors().add(interceptor);

    -然后,在拦截器内做Request拦截操作

    Request request = chain.request();//拦截reqeust
                    if (!AppUtil.isNetworkReachable(UIUtils.getContext())) {//判断网络连接状况
                        request = request.newBuilder()
                                .cacheControl(CacheControl.FORCE_CACHE)//无网络时只从缓存中读取
                                .build();
                        UIUtils.showToastSafe("暂无网络");
                    }
    

    其中,AppUtil.isNetworkReachable(UIUtils.getContext())是判断网络是否连接的方法,具体逻辑如下

    /**
     * 判断网络是否可用
     *
     * @param context Context对象
     */
    public static Boolean isNetworkReachable(Context context) {
        ConnectivityManager cm = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo current = cm.getActiveNetworkInfo();
        if (current == null) {
            return false;
        }
        return (current.isAvailable());
    }
    

    在每个请求发出前,判断一下网络状况,如果没问题继续访问,如果有问题,则设置从本地缓存中读取

    -接下来是设置Response

     Response response = chain.proceed(request);
                    if (AppUtil.isNetworkReachable(UIUtils.getContext())) {
                        int maxAge = 60*60; // 有网络时 设置缓存超时时间1个小时
                        response.newBuilder()
                                .removeHeader("Pragma")
                                //清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                                .header("Cache-Control", "public, max-age=" + maxAge)//设置缓存超时时间
                                .build();
                    } else {
                        int maxStale = 60 * 60 * 24 * 28; // 无网络时,设置超时为4周
                        response.newBuilder()
                                .removeHeader("Pragma")
                                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                                //设置缓存策略,及超时策略
                                .build();
                    }
    

    先判断网络,网络好的时候,移除header后添加cache失效时间为1小时,网络未连接的情况下设置缓存时间为4周

    -最后,拦截器全部代码

    Interceptor interceptor = new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request request = chain.request();
                    if (!AppUtil.isNetworkReachable(UIUtils.getContext())) {
                        request = request.newBuilder()
                                .cacheControl(CacheControl.FORCE_CACHE)
                                .url(path).build();
                        UIUtils.showToastSafe("暂无网络");//子线程安全显示Toast
                    }
    
                    Response response = chain.proceed(request);
                    if (AppUtil.isNetworkReachable(UIUtils.getContext())) {
                        int maxAge = 60 * 60; // read from cache for 1 minute
                        response.newBuilder()
                                .removeHeader("Pragma")
                                .header("Cache-Control", "public, max-age=" + maxAge)
                                .build();
                    } else {
                        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                        response.newBuilder()
                                .removeHeader("Pragma")
                                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                                .build();
                    }
                    return response;
                }
            };
    

    快过年了,祝所有的童鞋们,身体健康,事事如意!!,咳咳,还有最重要的,程序无Bug!!!

    相关文章

      网友评论

      • 老郭种树:你好,你这篇博客很有用。我还有点一问题,设置了头部进行缓存后,这缓存的操作不用部门做的吗,只要添加了缓存头部就可以了吗,我看了下缓存地址的文件,打不开,我想知道缓存了什么数据,这里你了解吗。还有一点就是在有网络的情况下,怎样知道这数据是通过缓存取到的,如果看到留言 有时间可以解答下 谢谢!
        mrwangyong:@探花so OKHTTP 支持支持标准的 Http 缓存协议,只需要在当前的请求地址加上 Cache 头部即可, OKHttp 内部实现了对此请求头的处理,相关代码可以看一下源码 cache 类,里面有详细的注释描述,每次请求都会分析 Cache 头以便进行处理的,是 OKHttp 内部自带的功能,只是需要特殊的方式才能调用的到
      • 安新小子:楼主,在没有网络的时候Response response = chain.proceed(request);报异常之后,后面的代码就走不到了,所以无网络的情况下也不能读缓存了,有什么方法解决吗
        安新小子:@mrwangyong 已经解决了,谢谢回复
        mrwangyong:@staticzh 什么异常 能把log贴一下吗
      • 安新小子:楼主在没有网络的时候这行代码会报异常Response response = chain.proceed(request);
      • 刻舟求剑KJ:我的项目也是遇到了这个问题,您有什么好的解决方案吗
      • 205蚁:博主,请教一个问题,当每个请求url 都带一个时间戳的时候,做缓存要怎样一个思路,因为带了时间戳每个请求url都是新的,缓存的MD5 key值都不一样就做不了缓存吧
      • LaoLee: Interceptor interceptor = new Interceptor() {
        @Override
        public okhttp3.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        request = request.newBuilder()
        .cacheControl(CacheControl.FORCE_CACHE)
        .build();

        okhttp3.Response originalResponse = chain.proceed(request);

        return originalResponse.newBuilder()
        .header("Cache-Control", "public, only-if-cached, max-stale=2419200")
        .removeHeader("Pragma")
        .build();

        }
        };


        Cache cache = new Cache(contentAndroidFragment.getActivity().getCacheDir(), 10 * 1024 * 1024);

        //创建OkHttpClient,并添加拦截器和缓存代码
        OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(interceptor)
        .cache(cache).build();



        Retrofit chcheRetrofit = new Retrofit.Builder()
        .baseUrl("http://api/data/&quot;).client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

        你好,我想实现有网络没网络都走缓存,没网的时候确实是走缓存的,为什么有网的时候还是会走网络?
        mrwangyong:@LaoLee 你试过更改模式为onlyIfCached了吗??
      • 活腿肠:还没用过Retrofit,刚好项目有缓存的需求,尝试一下
      • 2da632c69e0c:我把项目改成3.0后,我的缓存失效了。。楼主知道怎么解决吗
        2da632c69e0c:恩 是的
        mrwangyong:@那份眷恋55 是按照博客的方法做的吗?我这个是基于2.5的!
      • ppjuns:OkHttp's interceptors require OkHttp 2.2 or better. Unfortunately, interceptors do not work with OkUrlFactory, or the libraries that build on it, including Retrofit ≤ 1.8 and Picasso ≤ 2.4.
      • 91b9ccc21580:client.interceptors().add(interceptor);改成 client.networkInterceptors().add(interceptor);是不是好一点?
        91b9ccc21580:@Mr_AndroidCode 我的锅,应该用interceptors的= = 我把okhttp上的图看反了,interceptors会调用缓存而networkInterceptors不会,具体参考https://github.com/square/okhttp/wiki/Interceptors
        mrwangyong:@91b9ccc21580 刚特地尝试了一下,一旦改client.networkInterceptors().add(interceptor)马上会导致缓存失效,暂时还没弄清楚interceptors和networkInterceptors的区别,请多多赐教!!
      • vihuela:俩方面:首先,如果是判断网络设置缓存拦截器类别,那retrofit就不能单例
        再者:max-age 这个东西不是很理解,是设置了,我重新请求同一个url就不会是最新数据??
        vihuela:@Mr_AndroidCode https://github.com/vihuela/EasyNet/blob/Ok3_Version/easynet/build.gradle
        vihuela:@Mr_AndroidCode 新版Okhttp缓存完全失效
        mrwangyong:@vihuela Retrofit是否是单例的这个没关系,只需要OkHttpClient这个对象是一个就好,建议公司内部项目,完全可以把Retrofit设计成单例的,如果不行的话,只需要保证每个OkHttpClient对象的拦截器Interceptor一样就好,max-age我这里设置为一个小时,如果更新频率比较急的,可以设置一分钟!

      本文标题:Retrofit 源码解读之离线缓存策略的实现

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