Retrofit——Cache

要讲解cache部分内容,笔者想狂吼一句:网上好多都是错误的!
为什么?因为笔者跟着跌了好多坑,下面列举几个链接:
http://www.easyread.cc/p/3a8d910cce38(错的)
http://blog.qiji.tech/archives/1690(错的)
笔者说他错,是因为笔者真的在上面跌了好几个坑!!!!

好了,牢骚发完,我们言归正传,要、想实现缓存,我们还是先从解下http的cache基础知识吧,理解的人可直接往下拖。

我们为什么要使用cache?

往往我们在请求完一次后,再想请求同样的内容,我们不想再从服务端获取,而是从第一次访问的时候保存下来的数据请求,这样有利于减少访问时间、降低网络负荷。


ok,我们知道需求以后,就得看下Cache有哪些东西。

http缓存相关头

1.Expires (实体标头,HTTP 1.0+)

一个GMT时间,试图告知客户端,在此日期内,可以信任并使用对应缓存中的副本,缺点是,一但客户端日期不准确.则可能导致失效.


2.Pragma : no-cache(常规标头,http1.0+)
对Pragma定义的唯一的伪指令,同http1.1的Cache-Control : no-cache

  • Last-Modified(实体标头,HTTP1.0+)

一个GMT时间,告知,被请求实体的最后修改时间.用于客户端校验其缓存副本是否仍然可以信任.与其相关的两个条件请求标头:

  • If-Modified-Since:(此标头,仅对Get方法有意义)

如果实体在指定时间后,没有修改则返回一个304,否则返回一个常规的Get请求的响应(比如200).
另外,如果该标头的值是一个非法的值,那么也同样返回一个常规的Get请求的响应.PS:用户代理发起
If-Modified-Since尝试握手的条件,可能会有不同,比如IE系,如果该实体第一次响应头中包含Cache-Control:no-cache.则
IE不会使用If-Modified-Since请求资源.而其他浏览器则会.
但是如果使用Cache-Control:no-store.则所有用户代理的表现一致.都不使用If-Modified-Since(因为no-store的语义十分强烈.不允许任何缓存,这个在后续有专门介绍.)

  • If-Unmodified-Since:

如果实体在指定时间后,没有任何修改,那么就可以直接执行该请求使用方法的对应行为. 而如果有修改,则返回一个412 Precondition
Failed状态码,并且抛弃该方法对应的行为操作(GET方法除外).


3.Cache-Control : (常规标头,HTTP1.1)

  • public:(仅为响应标头)

响应:告知任何途径的缓存者,可以无条件的缓存该响应.

  • private(仅为响应标头)

响应:告知缓存者(据我所知,是指用户代理,常见浏览器的本地缓存.用户也是指,系统用户.但也许,不应排除,某些网关,可以识别每个终端用户的情况),只针对单个用户缓存响应.
且可以具体指定某个字段.如private –“username”,则响应头中,名为username的标头内容,不会被共享缓存.

  • no-cache:

请求:
告知缓存者,必须原原本本的转发原始请求,并告知任何缓存者,别直接拿你缓存的副本,糊弄人.你需要去转发我的请求,并验证你的缓存(如果有的话).对应名词:端对端重载.

响应: 允许缓存者缓存副本.那么其实际价值是,总是强制缓存者,校验缓存的新鲜度.一旦确认新鲜,则可以使用缓存副本作为响应.
no-cache,还可以指定某个包含字段,比如一个典型应用,no-cache=Set-Cookie.
这样做的结果,就是告知缓存者,对于Set-Cookie字段,你不要使用缓存内容.而是使用新滴.其他内容则可以使用缓存.

  • no-store:

请求:告知,请求和响应都禁止被缓存.(也许是出于隐私考虑) 响应:同上.

  • max-age:

请求:强制响应缓存者,根据该值,校验新鲜性.即与自身的Age值,与请求时间做比较.如果超出max-age值,则强制去服务器端验证.以确保返回一个新鲜的响应.其功能本质上与传统的Expires类似,但区别在于Expires是根据某个特定日期值做比较.一但缓存者自身的时间不准确.则结果可能就是错误的.而max-age,显然无此问题.
Max-age的优先级也是高于Expires的. 响应:同上类似,只不过发出方不一样.

  • max-stale:

请求:意思是,我允许缓存者,发送一个,过期不超过指定秒数的,陈旧的缓存. 响应:同上.

  • must-revalidate(仅为响应标头)

响应:意思是,如果缓存过了新鲜期,则必须重新验证.而不是试图返回一个不在新鲜期的缓存.与no-cache的区别在于,no-cache,完全无视新鲜期的概念.总是强制重新验证.理论上,must-revalidate更节省流量,但相比no-cache,可能并不总是那么精准.因为即使缓存者,认为是新鲜的,也不能保证服务器端没有做过更新.如果缓存者是一个缓存代理服务器,如果其试图重新验证时,无法连接上原始服务器,则也不允许返回一个不新鲜的,缓存中的副本.而是必须返回一个504
Gateway timeout.

  • proxy-revalidate(仅为响应标头)

响应:限制上与must-revalidate类似.区别在于受体的范围.proxy-revalidate,是要排除掉用户代理的缓存的.即,其规则并不应用于用户代理的本地缓存上.

  • min-fresh(仅为请求标头)

请求:告知缓存者,如果当前时间加上min-fresh的值,超了该缓存的过期时间.则要给我一个新的.其实个人觉得,其功能上有点和max-age类似.但是更大的是语义上的区别.

  • only-if-cached:(仅为请求标头)

请求:告知缓存者,我希望内容来自缓存,我并不关心被缓存响应,是否是新鲜的.

  • s-maxage(仅为响应标头)

响应:与max-age的唯一区别是,s-maxage仅仅应用于共享缓存.而不引用于用户代理的本地缓存,等针对单用户的缓存.
另外,s-maxage的优先级要高于max-age.

  • cache-extension

cache-extension是一个泛化的代称.它指所有自定义,或者说扩展的,指令,客户端和服务器端都可以自定义扩展Cache-Control相关的指令.
那么,实际上我们可以这样 Cache-Control:max-age=300, custom-directive = xxx,
public. 这样我们就定义了一个被统称为cache-extension的扩展指令.该指令如果对应的客户端或服务器端,不认识,就会忽略掉.


以上信息整理于这篇博客
更多相关http的cache可参考:
http://www.bkjia.com/headlines/491799.html

好了,了解完大概的cache头信息之后,我们就来正式搞一搞咱们的retrofit+cache吧!


要想客户端支持缓存,那么得开辟缓存空间,如下:

    @Provides
    @Singleton
    Cache provideOkHttpCache(Context context) {
        int cacheSize = 10 * 1024 * 1024; // 10 MiB
        Cache cache = new Cache(context.getCacheDir(), cacheSize);
        return cache;
    }

缓存空间有了,okhttp直接提供缓存支持,但是服务端一般都不会提供缓存支持(返回头一般不会有cache信息,有也估计不符合要求),故我们得重写拦截器,自己写返回头信息,先贴上okhttp部分代码(注:两种拦截器添加缓存,否则有可能失效,关于两者区别,笔者暂时也分不清,如果有大神知道的,请留言):

    @Provides
    @Singleton
    public OkHttpClient provideOkHttpClient(Context context, HttpLoggingInterceptor httpLoggingInterceptor, HttpInterceptor httpInterceptor,Cache cache,CacheInterceptor cacheInterceptor) {
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .cache(cache)
                .addInterceptor(httpLoggingInterceptor)
               .addInterceptor(httpInterceptor)
                .addInterceptor(cacheInterceptor)
                .addNetworkInterceptor(cacheInterceptor)
                .build();

        return client;
    }

我们想实现的效果是:
有网情况下,一分钟内访问的请求不会去真正http请求,而是从cache中获取;
没网情况下,一律从缓存获取,6小时过期时间。


下面贴上缓存拦截器部分:

public class CacheInterceptor implements Interceptor {
    Context context;

    public CacheInterceptor(Context context) {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        if (!NetUtils.isConnected(context)) {//没网强制从缓存读取
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
        }

        Response response = chain.proceed(request);
        if (NetUtils.isConnected(context)) {
            int maxAge = 60; //有网失效一分钟
            response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 6; // 没网失效6小时
            response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
         return  response;
    }

请注意,以上代码是错误的,稍不留神就错了,笔者在这跌了好大一坑,细心的读者估计发现了问题所在了,没错,if-esle里面东西虽然执行了,但是最后你偏偏给我返回了之前的response,搞毛啊?下面请看正确代码:

  Request request = chain.request();
        if (!NetUtils.isConnected(context)) {//没网强制从缓存读取(必须得写,不然断网状态下,退出应用,或者等待一分钟后,就获取不到缓存)
            request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build();
        }
        Response response = chain.proceed(request);
        Response responseLatest;
        if (NetUtils.isConnected(context)) {
            int maxAge = 60; //有网失效一分钟
             responseLatest = response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 6; // 没网失效6小时
             responseLatest= response.newBuilder()
                    .removeHeader("Pragma")
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
         return  responseLatest;

这里强调一下,只有get请求才具备http的缓存功能,post没有!没有!没有!故当请求是get的时候,判断当前是否有网,有网情况下,将“Cache-Control”头设置为”public, max-age=21600”,public上面有讲过,无条件缓存!max-age与请求时间作比较,这里设为6小时(依个人需求来定)。最后注意最好要remove掉其他的缓存头,避免服务端进行一些限制,导致客户端不能进行缓存。有网情况下,请查看okhttp的打印日志

有人说我也是这样写的啊,为什么离线缓存的时间没用?请查看是否因为拦截器添加有问题:

  .addInterceptor(cacheInterceptor)
  .addNetworkInterceptor(cacheInterceptor)

经笔者测试,上面两种拦截器必须添加,如果只添加第一个,会发现有网情况下,一分钟内每次请求都会重新请求,不会走缓存;如果只添加第二个,会发现如果超过1分钟,离线请求不成功

下面是有网情况下请求demo打印日志:

05-05 17:14:25.960 11949-15260/com.example.yanjiang.txshare D/OkHttp: <-- 200 OK http://*****/FundDataService/Phone/Information?user=admin&product=3&infoID=2016050511100086 (40ms)
05-05 17:14:25.960 11949-15260/com.example.yanjiang.txshare D/OkHttp: Content-Length: 1942
05-05 17:14:25.960 11949-11949/com.example.yanjiang.txshare D/mali_winsys: new_window_surface returns 0x3000
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: Content-Type: application/json; charset=utf-8
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: Server: Microsoft-IIS/7.5
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: X-AspNet-Version: 4.0.30319
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: X-Powered-By: ASP.NET
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: Date: Thu, 05 May 2016 09:14:22 GMT
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: OkHttp-Sent-Millis: 1462439665947
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: OkHttp-Received-Millis: 1462439665960
05-05 17:14:25.965 11949-15260/com.example.yanjiang.txshare D/OkHttp: Cache-Control: public, max-age=60

可清楚看到返回头60s,如果想看更多的头信息,请抓包,还能查看到请求头的上次修改缓存时间,会上传到服务器。

If-Modified-Since: Thu, 05 May 2016 09:04:40 GMT

假如上次获取网络数据后缓存到本地时间是如上时间,也是更新缓存时间,那么当它下次进行访问的时候(无论有网还是没网)都会带上这个头信息,假如在09:05:50的时候进行网络请求,因为超过一分钟,所以会带上如上的头进行访问,并且会更新本地缓存,时间也会修改为09:05:50,再下次进行网络请求的时候,又会带上上次09:05:50的时间(注:此处说的网络请求是有网情况下,离线不算,因为离线无法进行抓包,测试不了,不能妄下结论)

断网情况下获取的数据打印log:

05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: <-- 200 OK http://*****/FundDataService/Phone/Information?user=admin&product=3&infoID=2016050511100086 (6ms)
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: Content-Length: 1942
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: Content-Type: application/json; charset=utf-8
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: Server: Microsoft-IIS/7.5
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: X-AspNet-Version: 4.0.30319
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: X-Powered-By: ASP.NET
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: Date: Thu, 05 May 2016 09:04:40 GMT
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: OkHttp-Sent-Millis: 1462439083848
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: OkHttp-Received-Millis: 1462439083866
05-05 17:12:04.110 11949-12036/com.example.yanjiang.txshare D/OkHttp: Warning: 110 HttpURLConnection "Response is stale"
05-05 17:12:04.115 11949-12036/com.example.yanjiang.txshare D/OkHttp: Cache-Control: public, only-if-cached, max-stale=21600

在有网情况下,如果日志看的不明显,不能确定在一分钟之内是否从缓存获取数据,请用fiddler抓包,如果测试第一次无缓存情况下,访问接口抓到包,而第二次同样能获取数据,却抓不到包,说明成功,一分钟后再行尝试,就会再次进行抓包(因为网络情况下只设置了一分钟的缓存)。

关于返回头部分的缓存处理,依照个人需求来定,文章开始部分已介绍很多种头信息,retrofit+cache网上大多数的写法都是有问题的,请读者仔细测试,不能因为有了缓存就觉得没问题,一定要查看缓存有效时间是否吻合!

最后,给大家提供一个开源项目,这个项目笔者之前看过,他的缓存刚开始写的是有问题的,后来经过笔者提醒,现在应该是正常了。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值