您的位置:首页 > 其它

Retrofit——Cache

2016-05-04 16:38 197 查看
要讲解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网上大多数的写法都是有问题的,请读者仔细测试,不能因为有了缓存就觉得没问题,一定要查看缓存有效时间是否吻合!

最后,给大家提供一个开源项目,这个项目笔者之前看过,他的缓存刚开始写的是有问题的,后来经过笔者提醒,现在应该是正常了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: