不同的HTTP缓存体验

前言

继上篇《来一份Android动画全家桶》发布后,我相信你对Android的动画有必定的认识。此次咱们讲解的内容是关于HTTP缓存,经过本篇咱们不仅仅只是了解HTTP缓存机制,更重要的是学以至用,至于怎么用,嘿嘿。git

[舒适提示]对HTTP缓存已经有必定了解且对OkHttp缓存源码实现感兴趣,能够看看我写的玩一玩OkHttp缓存源码github

HTTP缓存

咱们试着本身实现一套HTTP缓存机制。首先咱们必须了解HTTP是客户端请求服务器响应的标准。算法

客户端缓存

OK,假设我如今是服务器,有一个客户端请求我,我把他想要的内容响应给他,很愉快的一次交流。缓存

好景不长,暴露出各类问题,例若有客户端反应我响应太慢,你本身网速差,距离我远,怪我喽?这都是还好的,可气的是天天不停地请求,甚至有时候同时多人请求,请求的内容仍是重复的,我又不能不给他,我只想说住院费报销吗?bash

本身太累不要一我的扛着,说出来,因而我向老大反馈这件事情,老大不愧是老大,次日就想出了一套方案。老大的方案我一遍就懂了(手动滑稽),我这里给你们说说。服务器

当客户端第一次访问服务器的时候,服务器如实把内容响应,其次在响应首部添加Expires,其值是一个GMT格式时间,告知客户端把内容在本地存一份,只要不超过这个时间,客户端请求时都直接读取本地文件。dom

我内心想着,若是客户端请求的内容具备时效性,那要是缓存,咱们的名声岂不是一败涂地?做为一名优秀的搬砖工得提醒老大啊,老大听后露出欣慰的笑容。分布式

若是想告知客户端不要缓存,那么服务器会在响应首部添加Pragma,其值是no-cache。ide

这是咱们最初的版本(HTTP1.0)。但随着版本发布,出现了一个问题,咱们没法保证客户端和服务器的时间一致,由于Expires的值是一个绝对时间,依赖于计算机时钟的正确设置。因而老大想出了用相对时间,哇,老大的形象在我内心又高大了。post

服务器原先返回Expires的时候,另外添加Cache-Control,其值为max-age=相对时间值,单位是秒。Expires仍然可用(主要用于兼容),优先级是Pragma -> Cache-Control -> Expires。

这是咱们第二个版本(HTTP1.1)。XXX年后,客户端这帮家伙组团来到咱们总部,声讨:你要咱们缓存就缓存,不缓存就不缓存,咱们不要的面子的啊?

行行行,大家来讲。

客户端能够在请求首部添加Cache-Control,若其值为no-cache,那么不使用缓存而直接向服务器发出请求,但返回的不必定不是缓存,这是客户端指望的缓存策略。

服务器缓存

这群家伙自从有了这个规定,又让咱们回到了过去,全给我no-cache,搞得我老大暴跳如雷。我知道,这时候是个人showtime,晋升指日可待。我告知老大,您当初的规范真的是一个伟大的决定,但能够用在客户端为什么不能用在服务器呢?我老大深思一下,又欣慰对我一笑。

若是客户端缓存过时或者请求首部Cache-Control值为no-cache,会略过客户端缓存而直接向服务器请求,此时服务器采用条件方法再验证。

条件方法再验证通常使用两种条件首部:If-Modified-Since和If-None-Match。前者须要配合Last-Modified[其值是GMT时间,其意是文件的最后修改时间],过程是客户端请求到服务器最新资源时,服务器会返回Last-Modified,当客户端再次请求服务器时,便会带上If-Modified-Since[其值是上次服务器返回的Last-Modified],服务器会根据文件的最终修改时间与此比较,若一致,则返回304 Not Modified响应报文,反之,正常返回200。

大体过程以下:

老大听完,先是对个人方案大赞一番,而后说能够改进,好比这两个场景:一个文件任你千万次修改,但内容不变;一个文件内容虽然改变了,但并不重要。哇!我对老大的敬佩之情犹如滔滔江水连绵不绝。

服务器返回ETag[其值通常是文件的hash],时机与Last-Modified相似。客户端下次请求服务器时便带上If-None-Match[其值是Etag],服务器会与之匹配,若匹配上,则返回304 Not Modified响应报文,反之,正常返回200。针对内容微改不影响主体,HTTP1.1支持"弱验证器",即原先ETag添加前缀"W/"。

大体过程以下:

高,实在是高!一股饭香扑鼻而来,低语,若是二者同时存在咋整?

老大说容我三思,而后出去了一趟,回来跟我说,这个简单。

RFC2616提到除非全部请求首部一致,否则不可返回304。后来RFC2616拆分红6份,其中RFC7232提到若是二者同时存在,那么服务器能够自由发挥,能够二者都判断,也能够有优先级等等。

优秀啊!其实我还有一个问题,由于ETag会用一种算法去计算值,若是服务器采用了分布式(例如CDN),会致使ETag不一致。其实算法保持一致就行啦。

缓存分类

真是一刻都不得悠闲,客户端这帮家伙又来闹事,哭诉说,用户表示有些内容极其私密,只能偷偷看;用户表示常常访问的须要快速打开。

通常来讲,缓存能够分为私有缓存和公有缓存。

私有缓存

服务器返回Cache-Control,其值带有private,客户端将文件保存在本地并容许用户配置缓存信息,而服务器并不会缓存。

公有缓存

服务器返回Cache-Control,其值带有public,默认为public,代理服务器就会把文件保存下来,客户端再次请求,若保存的文件可用,那么直接返回给客户端。代理服务器又被称为代理缓存。

XXX往后发布,今后世界和平!

第一次以故事形式讲解知识点,首先我来讲句公道话,我以为写得很是好,情节环环相扣又错综复杂(手动滑稽)!但你觉得到这里就已经结束了吗?

缓存层次结构

咱们先前讲的客户端和服务器缓存明显的层次结构。首先客户端发出请求,先验证缓存是否过时,若未过时则使用(缓存命中),此为一级缓存;若过时(缓存未命中),那么继续向服务器请求,服务器验证(新鲜度检测)该缓存仍然可用则使用(再验证命中),此为二级缓存;若不可用(再验证未命中),那么服务器返回原始文件(200),此为三级缓存;若是源服务器的文件已经被删除,那么返回404。

但理想很丰满,现实很骨感。例如分布式(CDN),即便是较为易懂的单中心节点结构和多中心节点结构都比咱们所说的链式结构复杂的多,更不用说网状结构(网状缓存)。其难点也不难发现,例如如何让缓存更快地更新或废弃,如何更快代价更低地让客户端获取缓存。

若是下一级缓存有多个选择,那么这些选择组成的缓存美其名曰兄弟缓存。

这里咱们献上美图简单介绍本节内容:

总结

这里咱们针对上面所说作一个小总结。其实在上一节缓存层次结构已经跟你们过了一遍流程。咱们的口号是什么?No picture,say a J8!

大佬发现有问题,望指正,感激涕零!

实战

理论终究只是理论,咱们仍是要回到平常!因为本人是个地地道道的Android搬砖工,因此你懂的。代码讲解基于Retrofit+OkHttp+HTTP1.1。

根据咱们上面的分析,客户端首次请求的时候,服务端会返回一个Cache-Control响应首部来控制缓存。固然,后面咱们也了解到客户端其实能够发起一个Cache-Control请求首部来指望本身的缓存策略。

@Headers("Cache-Control: public, max-age=300")//缓存时间为5分钟
@GET("random/data/{type}/{count}")
Flowable<GankioEntity> getGankio(@Path("type") String type, @Path("count") int count);
复制代码

但最终的缓存策略仍是由服务器控制,假设服务器并无缓存策略呢?懵逼了吧?放心,困难老是没有办法多,OkHttp里面有个好玩的东西叫Interceptor,咱们能够在这里到服务器返回的信息并修改。

private static class CrazyDailyCacheNetworkInterceptor implements Interceptor {
	...
    @Override
    public Response intercept(Chain chain) throws IOException {
        final Request request = chain.request();
        final Response response = chain.proceed(request);
        final String requestHeader = request.header(CACHE_CONTROL);
        //判断条件最好加上TextUtils.isEmpty(response.header(CACHE_CONTROL))来判断服务器是否返回缓存策略,若是返回,就按服务器的来,我这里所有客户端控制了
        if (!TextUtils.isEmpty(requestHeader)) {
            ...
            return response.newBuilder().header(CACHE_CONTROL, requestHeader).removeHeader("Pragma").build();
        }
        return response;
    }
}
复制代码

首先咱们取到Request,很明显,这是客户端的请求信息,而后再拿到服务器的信息Response,调用header修改Cache-Control的值,这里记得调用removeHeader("Pragma"),为什么?还记得咱们分析的Pragma的优先级是最高的吗?既然比不过他,那么将他移除咱们就第一了。

那么如何告诉OkHttp呢?Android同窗确定很应手。

OkHttpClient.Builder builder = new OkHttpClient.Builder();
...
builder.addNetworkInterceptor(new CrazyDailyCacheNetworkInterceptor());
复制代码

可是咱们经常有在山洞的场景,那确定没网啊!为啥经常在山洞?这个咱们暂且放一边,没网确定不可能到服务器啊,那咱们也接受不到缓存策略。能不能在无网的时候,咱们客户端制定一个缓存策略,好比无网时缓存支持一天,若超过一天,那么error。

private static class CrazyDailyCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        CacheControl cacheControl = request.cacheControl();
        //header可控制不走这个逻辑
        boolean noCache = cacheControl.noCache() || cacheControl.noStore() || cacheControl.maxAgeSeconds() == 0;
        if (!noCache && !NetworkUtils.isNetworkAvailable()) {
            Request.Builder builder = request.newBuilder();
            ...
            CacheControl newCacheControl = new CacheControl.Builder().maxStale(1, TimeUnit.DAYS).build();
            request = builder.cacheControl(newCacheControl).build();
            return chain.proceed(request);
        }
        return chain.proceed(request);
    }
}

builder.addInterceptor(new CrazyDailyCacheInterceptor());
复制代码

有了上面的分析,我相信代码并不难理解,这里有个注意点就是判断有无网的时候最好用ping的方法去检测,但这玩意是阻塞的,要注意。

那么,问题又来了,addInterceptor和addNetworkInterceptor有什么区别呢?一图胜千言,很少BB(官方)。而想知道具体缘由,能够看个人玩一玩OkHttp缓存源码的扩展章节。

诶,是否是漏了什么?咱们缓存放在哪儿呢?

//设置缓存 20M
Cache cache = new Cache(new File(context.getExternalCacheDir(), CacheConstant.CACHE_DIR_API), 20 * 1024 * 1024);
builder.cache(cache);
复制代码

Android中的缓存实现就这么简单,简单?这是不可能的,这辈子都不可能,设计一套好的缓存策略是个大考验。

若是对OkHttp缓存源码实现感兴趣,能够看看我写的玩一玩OkHttp缓存源码

骚聊

又到了紧张刺激的骚聊环节。HTTP缓存并非什么新鲜的技术,但它却很重要,虽然我并无总结HTTP缓存到底有什么好处,但并不难发现,例如它减小了带宽,减小了服务器压力,提升了用户体验等等。咱们不要拘泥简单了解机制,而应该学习它的思想运用到咱们开发中,例如咱们老生常谈的图片三级缓存。人生就是痛并快乐着,加油吧,骚年!

最后,感谢一直支持个人人!

传送门

Github:github.com/crazysunj/

博客:crazysunj.com/

相关文章
相关标签/搜索