【Android】OkHttp 源码剖析系列(三)——缓存机制


我们知道,在 CacheInterceptor 中实现了 OkHttp 中对 Response 的缓存功能,CacheInterceptor 的具体逻辑在前面的博客已经分析过,但里面对缓存机制的详细实现没有进行介绍。这篇文章中我们将对 OkHttp 的缓存机制的具体实现进行详细的介绍。

本源码剖析系列基于 OkHttp 3.14

文章目录:

【Android】OkHttp 源码剖析系列(一)——请求的发起及拦截器机制概述

【Android】OkHttp 源码剖析系列(二)——拦截器大体流程分析

【Android】OkHttp 源码剖析系列(三)——缓存机制

【Android】OkHttp 源码剖析系列(四)——连接的建立概述

【Android】OkHttp 源码剖析系列(五)——路由选择与代理机制

【Android】OkHttp 源码剖析系列(六)——连接复用机制及连接的建立

【Android】OkHttp 源码剖析系列(七)——请求的发起及响应的读取

HTTP 中的缓存

我们先来了解一下 HTTP 协议中与缓存相关的知识。

Cache-Control

Cache-Control 相信大家都接触过,它是一个处于 Request 以及 Response 的 Headers 中的一个字段,对于请求的指令及响应的指令,它有如下不同的取值:

请求缓存指令

  • max-age=:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。
  • max-stale[=]:表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。
  • min-fresh=:表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。
  • no-cache :在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证。
  • no-store:缓存不应存储有关客户端请求的任何内容。
  • no-transform:不得对资源进行转换或转变,Content-EncodingContent-RangeContent-Type等 Header 不能由代理修改。
  • only-if-cached:表明客户端只接受已缓存的响应,并且不向原始服务器检查是否有更新的数据。

响应缓存指令

  • must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
  • no-cache:在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证
  • no-store:缓存不应存储有关服务器响应的任何内容。
  • no-transform:不得对资源进行转换或转变,Content-EncodingContent-RangeContent-Type等 Header 不能由代理修改。
  • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容(例如,该响应没有 max-age 指令或 Expires 消息头)。
  • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它),私有缓存可以缓存响应内容。
  • proxy-revalidate:与 must-revalidate 作用相同,但它仅适用于共享缓存(如代理),并被私有缓存忽略。
  • max-age=:设置缓存存储的最大周期,超过这个的时间缓存被认为过期,时间是相对于请求的时间。
  • s-maxage=:覆盖 max-age 或者 Expires 头,但它仅适用于共享缓存(如代理),并被私有缓存忽略。

其中我们常用的就是加粗的几个字段(max-agemax-staleno-cache)。

Expires

Expires 头是 HTTP1.0 中的内容,它的作用类似于 Cache-Control:max-age,它告诉浏览器缓存的过期时间,这段时间浏览器就可以不用直接再向服务器请求了。

Last-Modified / If-Modified-Since

这两个字段需要配合 Cache-Control 来使用

  • Last-Modified:该响应资源最后的修改时间,服务器在响应请求的时候可以填入该字段。
  • If-Modified-Since:客户端缓存过期时(max-age 到达),发现该资源具有 Last-Modified 字段,可以在 Header 中填入 If-Modified-Since 字段,表示当前请求时间。服务器收到该时间后会与该资源的最后修改时间进行比较,若最后修改的时间更新一些,则会对整个资源响应,否则说明该资源在访问时未被修改,响应 code 304,告知客户端使用缓存的资源,这也就是为什么之前看到 CacheInterceptor 中对 304 做了特殊处理。

Etag / If-None-Match

这两个字段同样需要配合 Cache-Control 使用

  • Etag:请求的资源在服务器中的唯一标识,规则由服务器决定
  • If-None-Match:若客户端在缓存过期时(max-age 到达),发现该资源具有 Etag 字段,就可以在 Header 中填入 If-None-Match 字段,它的值就是 Etag 中的值,之后服务器就会根据这个唯一标识来寻找对应的资源,根据其更新与否情况返回给客户端 200 或 304。

同时,这两个字段的优先级是比 Last-ModifiedIf-Modified-Since 两个字段的优先级要高的。

OkHttp 中的缓存机制

了解完 HTTP 协议的缓存相关 Header 之后,我们来学习一下 OkHttp 对缓存相关的实现。

InternalCache

首先我们通过之前的文章可以知道,CacheInterceptor 中通过 cache 这个 InternalCache 对象进行对缓存的 CRUD 操作。这里 InternalCache 只是一个接口,它定义了对 HTTP 请求的缓存的 CRUD 接口。让我们看看它的定义:

看到该接口的 JavaDoc 可以知道,官方禁止使用者实现这个接口,而是使用 Cache 这个类。

Cache

那么 Cache 难道是 InternalCache 的实现类么?让我们去看看 Cache 类。

代码非常多这里就不全部贴出来了,Cache 类并没有实现 InternalCache 这个类,而是在内部持有了一个实现了 InternalCache 的内部对象 internalCache

这里转调到了 Cache 类中的 CRUD 相关实现,这里采用了组合的方式,提高了设计的灵活性。

同时,在 Cache 类中,还可以看到一个熟悉的身影——DiskLruCache(关于它的原理这里不再进行详细分析,具体原理分析可以看我之前的博客 【Android】LRU 缓存——内存缓存与磁盘缓存),看来 OkHttp 的缓存的实现是基于 DiskLruCache 实现的。

现在可以大概猜测,Cache 中的 CRUD 操作都是在对 DiskLruCache 对象进行操作。

构建

而我们的 Cache 对象是何时构建的呢?其实是在 OkHttpClient 创建时构建并传入的:

我们看到 Cache 的构造函数,它最后调用到了 Cache(directory, maxSize, fileSystem),而 fileSystem 传入的是 FileSystem.SYSTEM

在它的构造函数中构造了一个 DiskLruCache 对象。

put

接着让我们看一下它的 put 方法是如何实现的:

它主要的实现就是根据 Response 构建 Entry,之后将其写入到 DiskLruCache.Editor 中,写入的过程中调用了 key 方法根据 url 产生了其存储的 key

同时从注释中可以看出,OkHttp 的作者认为虽然能够实现如 POST、HEAD 等请求的缓存,但其实现会比较复杂,且收益不高,因此只允许缓存 GET 请求的 Response

key 方法的实现如下:

其实就是将 url 转变为 UTF-8 编码后进行了 md5 加密。

接着我们看到 Entry 构造函数,看看它是如何存储 Response 相关的信息的:

主要是一些赋值操作,我们接着看到 Entry.writeTo 方法

这里主要是利用了 Okio 这个库中的 BufferedSink 实现了写入操作,将一些 Response 中的信息写入到 Editor。关于 Okio,会在后续文章中进行介绍

get

我们接着看到 get 方法的实现:

这里拿到了 DiskLruCache.Snapshot,之后通过它的 source 创建了 Entry,然后再通过 Entry 来获取其 Response

我们看看通过 Snapshot.source 是如何创建 Entry 的:

可以看到,同样是通过 Okio 进行了读取,看来 OkHttp 中的大部分 I/O 操作都使用到了 Okio。我们接着看到 Entry.response 方法:

其实就是根据 response 的相关信息重新构建了 Response 对象。

可以发现,写入和读取的过程都有用到 Entry 类,看来 Entry 类就是 OkHttp 中 Response 缓存的桥梁了,这里要注意的是,这里的 Entry 与 DiskLruCache 中的 Entry 是不同的

remove

remove 的实现非常简单,它直接调用了 DiskLruCache.remove

update

update 的实现也十分简单,这里不再解释,和 put 比较相似

CacheStrategy

我们前面介绍了缓存的使用,但还没有介绍在 CacheInterceptor 中使用到的缓存策略类 CacheStrategy。我们先看到 CacheStrategy.Factory 构造函数的实现:

这里主要是对一些变量的初始化,接着我们看到 Factory.get 方法,之前通过该方法我们就获得了 CacheStrategy 对象:

这里首先通过 getCandidate 方法获取到了对应的缓存策略

如果发现我们的请求中指定了禁止使用网络,只使用缓存(指定 CacheControlonly-if-cached ),则创建一个 networkRequestcacheResponse 均为 null 的缓存策略。

我们接着看到 getCandidate 方法:

在缓存策略的创建中,主要是以下几步:

  1. 没有缓存 response,直接进行寻常网络请求
  2. HTTPS 的 response 丢失了握手相关数据,丢弃缓存直接进行网络请求
  3. 缓存的 response 的 code 不支持缓存,则忽略缓存,直接进行寻常网络请求
  4. Cache-Control 中的字段进行处理,主要是计算缓存是否还能够使用(比如超过了 max-age 就不能再使用)
  5. If-None-MatchIf-Modified-Since 字段进行处理,填入相应 Header(同时可以看出 Etag 确实比 Last-Modified 优先级要高

我们可以发现,OkHttp 中实现了一个 CacheControl 类,用于以面向对象的形式表示 HTTP 协议中的 Cache-Control Header,从而支持获取 Cache-Control 中的值。

同时可以看出,我们的缓存策略主要存在以下几种情况:

  • request != null, response == null:执行寻常网络请求,忽略缓存
  • request == null, response != null:采用缓存数据,忽略网络数据
  • request != null, response != null:存在 Last-ModifiedEtag 等相关数据,结合 request 及缓存中的 response
  • request == null, response == null:不允许使用网络请求,且没有缓存,在 CacheInterceptor 中会构建一个 504 的 response

总结

OkHttp 的缓存机制主要是基于 DiskLruCache 这个开源库实现的,从而实现了缓存在磁盘中的 LRU 存储。通过在 OkHttpClient 中对 Cache 类的配置,我们可以实现对缓存位置及缓存空间大小的配置,同时 OkHttp 提供了 CacheStrategy 类对 Cache-Control 中的值进行处理,从而支持 HTTP 协议的缓存相关 Header。

参考资料

OKHTTP之缓存配置详解


Android Developer in GDUT