原文出处
标题:OkHttp 精讲:CacheInterceptor
作者:Android轮子哥
原文链接:OkHttp 精讲:CacheInterceptor - 简书
前言
- 本文章所使用的 OkHttp 源码版本:3.12.10
源码解析
- 这个拦截器的作用正如它的名称一样,负责处理请求缓存的,接下来让我们看看这个拦截器是怎么处理缓存的
- 在这里我们可以很直观地看到 OkHttp 的缓存读取方式,直接将整个 Request 对象传入,最后返回一个缓存的 Response 对象
- 那么问题来了,OkHttp 是怎么缓存请求的?是通过内存缓存还是磁盘缓存?缓存的 key 是怎么计算的?最多能缓存多少数据?使用缓存需要满足哪些条件?缓存又是怎么进行更新的?
- 让我们带着这些疑惑正式进入今天的源码解析
- 通过这个方法可以得出我们今天第一个疑问,缓存的 key 原来是对 url 进行 md5 计算得来的,那么问题又来,如果我用的是 get 请求,那么这种方案是完全没有问题的,如果是 post 请求呢?参数并没有在 url 上面体现,只通过 url 作为缓存的 key 来获取缓存是否有些草率了?
- 要想知道这个问题的答案也很简单,我们刚看完获取缓存的方法,与之对应的有添加缓存的方法,或许我们能从中得到答案
- 我们可以看到这里对请求方法做了判断,如果不符合要求则不会缓存,那么是哪些请求方法不支持缓存呢?
- 在这里我们可以看到,post 请求已经被排除在缓存的范围之间了,那么问题又来了,为什么不行呢?
- 接下来让我们看看官方给的一段解释
- 原来 post 请求是可以被缓存的,但是由于实现的技术复杂度比较高,就算做了产生的价值并不高,所以 OkHttp 并没有去实现
- 我们可以设想一下,如果要我们来做 post 请求缓存,该如何实现?需要注意哪些细节?
- 如果是实现一个简易的,我觉得难度并不高,我们可以直接将 post 请求的参数直接拼接到 url 上,将缓存读写方式和 get 请求同步,但是 post 请求不仅仅是只能添加参数,还能添加各种各样的文件,这个时候的逻辑就复杂起来了,要面对的情况有三种:纯表单参数、纯文件上传、表单参数+文件上传,如果是文件上传的话我们可以通过计算文件流的 md5 来得出一部分 key,如果上传的文件比较多或者比较大,我们都知道 md5 是一种密集计算,需要遍历整个文件的内容,那么又该如何保证请求的效率不受 md5 计算的影响?
- 或许我们只需要思考一下这个问题就知道 OkHttp 为什么不做 post 请求的缓存了,从技术的角度上是可以实现,但不是一件简单的事,不仅处理的过程比较复杂,还有一些矛盾的问题需要解决。
- 在这段代码中,有一个单词吸引到了我们:DiskLruCache,从这个单词中我们可以获取到两个信息,缓存的方式是磁盘缓存,而缓存的算法是 LruCache
- 这里面的具体实现细节我们不去细究,因为这个不是我们今天的主题,所以点到为止
名称 | 说明 | 示例 |
Date | 请求发送的日期和时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
Expires | 响应过期的日期和时间 | Expires: Thu, 01 Dec 2010 16:00:00 GMT |
Last-Modified | 请求资源的最后修改时间 | Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT |
ETag | 请求变量的实体标签的当前值 | ETag: “737060cd8c284d8af7ad3082f209582d” |
Age | 从原始服务器到代理缓存形成的估算时间(以秒计,非负) | Age: 12 |
- 首先会从缓存的 Response 中取一些响应头,这些响应头的作用都是同一个,用于判断这个缓存的 Response 是否处于可复用的状态
- 这个 Factory.get() 方法相当于 Builder.build(),大家可以这么理解,在这个 get 方法里面将缓存策略创建出来了,如果在没有缓存的情况下,那么会直接传空进去
- 大家可能对 CacheStrategy 的构建函数多多少少有点疑惑,在这里先带大家理解一下
- 从注释的翻译可以看出,networkRequest 对象如果不等于空, 那么代表需要请求一次网络,如果等于空则不需要。而 cacheResponse 对象如果不等于空,那么代表这个缓存是可复用的,如果等于空则不能复用
- 如果这个是一个 Https 的请求,必须要经过身份认证才可以使用这个缓存
名称 | 说明 |
no-cache | 数据不能缓存,每次请求都重新访问服务器 |
no-store | 不仅不能使用缓存,就连暂存到临时文件夹下都不行 |
public | 可以被任何缓存区缓存,如客户端、代理服务器等 |
private | 这个请求返回的数据只能在客户端中缓存 |
max-age=60 | 缓存过期时间,以秒作为时间单位 |
only-if-cached | 客户端请求使用缓存数据,如果服务器返回 304 代表允许 |
- 这个方法主要用来判断缓存是否可用,前提条件是客户端和服务器必须没有禁止缓存,如果响应码是 302 或者 307,则还需要服务器有指定缓存的过期时间又或者服务器有明确允许客户端做缓存,否则这个缓存就是不能被复用的
- 如果服务器没有明确指明禁用缓存,并且缓存没有过期的话,那么就直接复用已缓存的 Response 对象
- 最后一种情况,客户端有缓存但是已经过期了,那么这个时候需要将我们之前缓存过的 Response 资源标识取出来,并把这个标识添加进请求头,然后重新请求一次服务器,让后台决定要不要复用这个已过期的缓存。
- 这里还做了特殊处理,如果当前需要请求网络,但是我们在请求的时候设置了只读取缓存,那么这个逻辑就是有问题的,全部返回 null 处理,也就是不请求网络,也不使用缓存,那么该怎么整?
那么 OkHttp 创建一个响应码为 504 的 Response 对象,并且没有携带 Body,意指在于告诉我们获取不到任何数据
- 如果不需要请求网络来判断缓存是否可用,那么直接返回这个缓存的 Response 对象,否则就获取网络上面的数据
响应码 | 描述 |
304 | 如果客户端发送了一个带条件的 GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。 该响应必须包含以下的头信息: Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器以及客户端可以自行将 Date 字段添加到接收到的响应头中去(正如RFC 2068中规定的一样),缓存机制将会正常工作。 ETag 和/或 Content-Location,假如同样的请求本应返回200响应。 Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。 假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的 GET 请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。 假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。 假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值。 |
- 就如我们刚刚讲过的,如果本地缓存过期了,那么我们会请求网络来询问服务器是否复用这个缓存,如果服务器返回响应码 304,那么就证明服务器允许了,但是服务器会给我们返回一个空 Body 的 Response 对象,所以我们需要对 networkResponse 中的 Body 替换成 cacheResponse 中的 Body,然后刷新本次请求的缓存,最后再将结果返回回去
- 刚刚那种情况是本地有缓存的情况下,那么我们再看看本地在没有任何缓存的情况下 OkHttp 会如何处理
- 还是常规操作,如果这个响应头有 Body 并且满足缓存的条件就对它进行缓存,但是如果请求的方式不符合缓存要求,那么也会将它从缓存中移除,最后将 Response 对象返回回去
源码总结
- 这个拦截器主要作用就是处理请求缓存,首先会根据 url 到磁盘中读取相应的缓存,还有一点需要注意,目前 OkHttp 不支持读取 Post 请求的缓存,如果读取到了缓存,还会判断这个缓存是否可用,前提条件是客户端和服务器都不能禁用缓存,否则将无法使用缓存功能,如果缓存过期了,那么 OkHttp 需要再发一次请求确定缓存是否可以复用,如果服务器返回 304 则代表缓存是可用的,但是 Body 是空的,需要对当前的 Response 对象和已缓存的 Response 对象结合成一个新的 Response 对象,最后再将新的 Response 对象更新到缓存中,如果在此之前没有任何缓存,那么就先判断是否满足缓存的条件,如果满足的话就直接写入到缓存中。