先摆上学习得到的结论:
- 头信息是存储在.0文件中,body信息是存储在.1文件中,所有的操作记录在 journal日志文件中。
- 只有客户端而没有服务端的支持也是可以实现缓存的,但要用Netword Interceptor,如果用Application Interceptor除非在request中设置onIfCached,也就是不使用网络请求只用缓存数据。
- url会做为缓存的key,在取出url对应的缓存后会进行校验以决定是否使用缓存,当networkRequest不为空时一定会请求服务端以校验或获取新的信息。
- OkHttp只有配置了cache才会启用缓存,且只支持Get的缓存。
- 开发者可以加两种拦截器,一个是Application Interceptrs通过addInterceptor()添加,这里的拦截器会在请求开始前调用,一个是NetworkInterceptors通过addNetworkInterceptor()添加,会在倒数第二执行,也就是请求服务端之前调用,在连接建立之后调用。
- OkHttp采用DiskLrucache实现磁盘缓存的。
- Interceptor按被添加的顺序依次执行,这是官网关于Interceptors的图,https://square.github.io/okhttp/interceptors/
本篇以如下问题为切入点来分析Okhttp的缓存原理:
- Http请求头字段必备基础知识
- 拦截器的执行入口
- 如何启用Okhttp的缓存?
- OkHttp支持缓存的请求有哪些?
- Okhttp如何取缓存以及如何判断是否使用缓存?
- 缓存如何保存以及缓存的文件格式
- 没有服务端支持是否可以实现缓存以及如何实现?
1.Http请求头字段必备基础知识
Http头字段基础知识,参与链接:https://zh.wikipedia.org/wiki/HTTP%E5%A4%B4%E5%AD%97%E6%AE%B5,
下表是常见的请求与回应字段,非常见的请到链接中查看。其中与缓存相关的字段已做加粗处理。比较特殊的字段是Pragma,Pragma:no-cache等同于Cache-Control: no-cache。
请求字段
协议头字段名 | 说明 | 示例 | 状态 |
---|---|---|---|
Accept | 能够接受的回应内容类型(Content-Types参见内容协商) | Accept: text/plain | 常设 |
Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 | 常设 |
Accept-Encoding | 能够接受的编码方式列表。参考HTTP压缩。 | Accept-Encoding: gzip, deflate | 常设 |
Accept-Language | 能够接受的回应内容的自然语言列表。参考 内容协商 。 | Accept-Language: en-US | 常设 |
Accept-Datetime | 能够接受的按照时间来表示的版本 | Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT | 临时 |
Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | 常设 |
Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache | 常设 |
Connection | 该浏览器想要优先使用的连接类型[8] | Connection: keep-aliveConnection: Upgrade | 常设 |
Cookie | 之前由服务器通过 Set- Cookie (下文详述)发送的一个 超文本传输协议 | Cookie Cookie: $Version=1; Skin=new; | 常设: 标准 |
Content-Length | 以 八位字节数组 (8位的字节)表示的请求体的长度 | Content-Length: 348 | 常设 |
Content-MD5 | 请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== | 过时的[9] |
Content-Type | 请求体的 多媒体类型 (用于POST和PUT请求中) | Content-Type: application/x-www-form-urlencoded | 常设 |
Date | 发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送) | Date: Tue, 15 Nov 1994 08:12:31 GMT | 常设 |
Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue | 常设 |
From | 发起此请求的用户的邮件地址 | From: [email protected] | 常设 |
Host | 服务器的域名(用于虚拟主机 ),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。[10] 自超文件传输协议版本1.1(HTTP/1.1)开始便是必需字段。 | Host: en.wikipedia.org:80Host: en.wikipedia.org | 常设 |
If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用时,用作像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。 | If-Match: "737060cd8c284d8af7ad3082f209582d" | 常设 |
If-Modified-Since | 允许在对应的内容未被修改的情况下返回304未修改( 304 Not Modified ) | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT | 常设 |
If-None-Match | 允许在对应的内容未被修改的情况下返回304未修改( 304 Not Modified ),参考 超文本传输协议 的实体标记 | If-None-Match: "737060cd8c284d8af7ad3082f209582d" | 常设 |
If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: "737060cd8c284d8af7ad3082f209582d" | 常设 |
If-Unmodified-Since | 仅当该实体自某个特定时间已来未被修改的情况下,才发送回应。 | If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT | 常设 |
Max-Forwards | 限制该消息可被代理及网关转发的次数。 | Max-Forwards: 10 | 常设 |
Origin | 发起一个针对 跨来源资源共享 的请求(要求服务器在回应中加入一个‘访问控制-允许来源’('Access-Control-Allow-Origin')字段)。 | Origin: http://www.example-social-network.com | 常设: 标准 |
Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache | 常设但不常用 |
Proxy-Authorization | 用来向代理进行认证的认证信息。 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | 常设 |
Range | 仅请求某个实体的一部分。字节偏移以0开始。参见字节服务。 | Range: bytes=500-999 | 常设 |
Referer [sic] [11] | 表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。 | Referer: http://en.wikipedia.org/wiki/Main_Page | 常设 |
TE | 浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值;另外还可用"trailers"(与"分块 "传输方式相关)这个值来表明浏览器希望在最后一个尺寸为0的块之后还接收到一些额外的字段。 | TE: trailers, deflate | 常设 |
User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 | 常设 |
Upgrade | 要求服务器升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | 常设 |
Via | 向服务器告知,这个请求是由哪些代理发出的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) | 常设 |
Warning | 一个一般性的警告,告知,在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | 常设 |
回应字段
字段名 | 说明 | 例子 | 状态 |
---|---|---|---|
Access-Control-Allow-Origin | 指定哪些网站可参与到跨来源资源共享过程中 | Access-Control-Allow-Origin: * | 临时 |
Accept-Patch[28] | 指定服务器支持的文件格式类型。 | Accept-Patch: text/example;charset=utf-8 | 常设 |
Accept-Ranges | 这个服务器支持哪些种类的部分内容范围 | Accept-Ranges: bytes | 常设 |
Age | 这个对象在代理缓存中存在的时间,以秒为单位 | Age: 12 | 常设 |
Allow | 对于特定资源有效的动作。针对HTTP/405这一错误代码而使用 | Allow: GET, HEAD | 常设 |
Cache-Control | 向从服务器直到客户端在内的所有缓存机制告知,它们是否可以缓存这个对象。其单位为秒 | Cache-Control: max-age=3600 | 常设 |
Connection | 针对该连接所预期的选项[8] | Connection: close | 常设 |
Content-Disposition[29] | 一个可以让客户端下载文件并建议文件名的头部。文件名需要用双引号包裹。 | Content-Disposition: attachment; filename="fname.ext" | 常设 |
Content-Encoding | 在数据上使用的编码类型。参考 超文本传输协议压缩 。 | Content-Encoding: gzip | 常设 |
Content-Language | 内容所使用的语言[30] | Content-Language: da | 常设 |
Content-Length | 回应消息体的长度,以 字节 (8位为一字节)为单位 | Content-Length: 348 | 常设 |
Content-Location | 所返回的数据的一个候选位置 | Content-Location: /index.htm | 常设 |
Content-MD5 | 回应内容的二进制 MD5 散列,以 Base64 方式编码 | Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== | 过时的[31] |
Content-Range | 这条部分消息是属于某条完整消息的哪个部分 | Content-Range: bytes 21010-47021/47022 | 常设 |
Content-Type | 当前内容的MIME类型 | Content-Type: text/html; charset=utf-8 | 常设 |
Date | 此条消息被发送时的日期和时间(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示) | Date: Tue, 15 Nov 1994 08:12:31 GMT | 常设 |
ETag | 对于某个资源的某个特定版本的一个标识符,通常是一个 消息散列 | ETag: "737060cd8c284d8af7ad3082f209582d" | 常设 |
Expires | 指定一个日期/时间,超过该时间则认为此回应已经过期 | Expires: Thu, 01 Dec 1994 16:00:00 GMT | 常设: 标准 |
Last-Modified | 所请求的对象的最后修改日期(按照 RFC 7231 中定义的“超文本传输协议日期”格式来表示) | Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT | 常设 |
Link | 用来表达与另一个资源之间的类型关系,此处所说的类型关系是在 RFC 5988 中定义的 | Link: ; rel="alternate"[32] | 常设 |
Location | 用来 进行重定向,或者在创建了某个新资源时使用。 | Location: http://www.w3.org/pub/WWW/People.html | 常设 |
P3P | 用于支持设置P3P策略,标准格式为“P3P:CP="your_compact_policy"”。然而P3P规范并不成功,[33]大部分现代浏览器没有完整实现该功能,而大量网站也将该值设为假值,从而足以用来欺骗浏览器的P3P插件功能并授权给第三方Cookies。 | P3P: CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info." | 常设 |
Pragma | 与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。 | Pragma: no-cache | 常设 |
Proxy-Authenticate | 要求在访问代理时提供身份认证信息。 | Proxy-Authenticate: Basic | 常设 |
Public-Key-Pins[34] | 用于缓解中间人攻击,声明网站认证使用的传输层安全协议证书的散列值 | Public-Key-Pins: max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; | 常设 |
Refresh | 用于设定可定时的重定向跳转。右边例子设定了5秒后跳转至“http://www.w3.org/pub/WWW/People.html”。 | Refresh: 5; url=http://www.w3.org/pub/WWW/People.html | 专利并非标准Netscape实现的扩展,但大部分网页浏览器也支持。 |
Retry-After | 如果某个实体临时不可用,则,此协议头用来告知客户端日后重试。其值可以是一个特定的时间段(以秒为单位)或一个超文本传输协议日期。 [35] Example 1: Retry-After: 120Example 2: | Retry-After: Fri, 07 Nov 2014 23:59:59 GMT | 常设 |
Server | 服务器的名字 | Server: Apache/2.4.1 (Unix) | 常设 |
Set-Cookie | HTTP cookie | Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1 | 常设: 标准 |
Status | 通用网关接口 协议头字段,用来说明当前这个超文本传输协议回应的 状态 。普通的超文本传输协议回应,会使用单独的“状态行”("Status-Line")作为替代,这一点是在 RFC 7230 中定义的。 [36] | Status: 200 OK | Not listed as a registered field name |
Strict-Transport-Security | HTTP 严格传输安全这一头部告知客户端缓存这一强制 HTTPS 策略的时间,以及这一策略是否适用于其子域名。 | Strict-Transport-Security: max-age=16070400; includeSubDomains | 常设: 标准 |
Trailer | 这个头部数值指示了在这一系列头部信息由由分块传输编码编码。 | Trailer: Max-Forwards | 常设 |
Transfer-Encoding | 用来将实体安全地传输给用户的编码形式。当前定义的方法包括:分块(chunked)、compress、deflate、gzip和identity。 | Transfer-Encoding: chunked | 常设 |
Upgrade | 要求客户端升级到另一个协议。 | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 | 常设 |
Vary | 告知下游的代理服务器,应当如何对未来的请求协议头进行匹配,以决定是否可使用已缓存的回应内容而不是重新从原始服务器请求新的内容。 | Vary: * | 常设 |
Via | 告知代理服务器的客户端,当前回应是通过什么途径发送的。 | Via: 1.0 fred, 1.1 example.com (Apache/1.1) | 常设 |
Warning | 一般性的警告,告知在实体内容体中可能存在错误。 | Warning: 199 Miscellaneous warning | 常设 |
WWW-Authenticate | 表明在请求获取这个实体时应当使用的认证模式。 | WWW-Authenticate: Basic | 常设 |
X-Frame-Options[37] | 点击劫持保护:deny:该页面不允许在 frame 中展示,即使是同域名内。sameorigin:该页面允许同域名内在 frame 中展示。allow-from uri:该页面允许在指定uri的 frame 中展示。allowall:允许任意位置的frame显示,非标准值。 | X-Frame-Options: deny | 过时的[38] |
2.Okhttp拦截器的执行入口
RealCall封装了OkHttp的请求request,当request被调用时会执行execute方法,这个方法返回最终的response结果,代码如下:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.timeoutEnter();
transmitter.callStart();
try {
client.dispatcher().executed(this);
return getResponseWithInterceptorChain();//开始添加和调用拦截器
} finally {
client.dispatcher().finished(this);
}
}
重点在于getResonseWithInterceptorChain()方法,这里是添加和调用拦截器的入口,而拦截器里对request所作处理的调用顺序与它们的添加顺序相同,而对response所作处理的调用顺序则与添加顺序相反。从源码中可知,CacheInterceptor不论是否配置了缓存都会添加到拦截器列表中,而最终是否缓存是由cache是否为空决定的。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());//开发者可添加的Application Interceptor
interceptors.add(new RetryAndFollowUpInterceptor(client));
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));//缓存拦截器是一定会加的
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());//开发者可添加的Network Intercepotr
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest);
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
3.如何启用Okhttp的缓存?
在OkhttpClient中设置Cache,Cache的构造方法需指定缓存文件的路径及最大容量,这样即可开启okhttp的缓存。由上面可知是否缓存取决于cache是否为空,CacheInterptor是一定会执行的。
File cacheDir = new File(getContext().getCacheDir(), "http_cache");
OkHttpClient.Builder builder = new OkHttpClient.Builder().cache(new Cache(cacheDir, 20 * 1024 * 1024));
OkHttpClient client = builder.build();
4.OkHttp支持缓存的请求有哪些?
如果OkHttpClient加了缓存机制,默认会缓存所有的Get缓存,且只缓存Get缓存,如果Get的url中包含了字符*,则也不支持缓存。
要知道OkHttp支持哪些请求的缓存,可以查看保存服务端返回结果时所做的判断。在得到服务端返回后,如果response不为空且客户端与服务端均未设置Cache-Control: no-cache则会将response写入缓存
//CacheInterceptor
if (cache != null) {//配置了缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {//这里判断返回消息体、状态码以及cache-control
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);//put方法还会检查url中是否包含*,如果有则不缓存。在put中同样也调用了invalidatesCache来判断是否缓存
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {//删除之前缓存的文件,有可能是相同url但是请求方式发生改变
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
public static boolean invalidatesCache(String method) {//如果是下面这些方法则会删除缓存
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
|| method.equals("DELETE")
|| method.equals("MOVE"); // WebDAV
}
在存缓存文件时Cache.put会判断,源码注释中也说明只支持Get方法,其它方法的缓存实现代价高收益低,所以不支持。此外如果url中包含*也不会缓存。
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
/**
* Returns true if a Vary header contains an asterisk. Such responses cannot be cached.
*/
public static boolean hasVaryAll(Headers responseHeaders) {
return varyFields(responseHeaders).contains("*");
}
5.Okhttp如何取缓存以及如何判断是否使用缓存?
要弄清楚缓存的存取,则要深入分析CacheInterceptor,缓存相关的操作就是在这里实现的。在开始前先了解CacheStrategy,这个类定义了缓存的策略,基本就是http缓存协议的实现。CacheStrategy的目标就是得到networkRequest和cacheResponse。根据是否有缓存、是否开启缓存配置、缓存是否失效等设置networkRequest和cacheResponse。根据networkRequest和cacheResponse是否为空,两两组合有四种情况:
networkRequest | cacheResponse | 结果 |
---|---|---|
null | null | 请求为空,又无缓存,所以配置了only-if-cached,返回504。不会请求服务端 |
null | non-null | 缓存有效,使用缓存,不会请求服务端 |
non-null | null | 无缓存或者失效,直接请求服务端 |
non-null | non-null | 缓存校验,请求服务端验证缓存有效性若失效将返回最新数据 |
CacheInterceptor的实现就是依据上面四种情况,从基础知识中可知,requst里与缓存有关的头字段有"Cache-Control"、"If-Modified-Sine"、"If-None-Match"、"If-Unmodifed-Since”、“Date”,而服务端返回头字段中与缓存有关的字段有"Age"、"Cache-Control"、“ETag”、“Expires”、"Last-Modified"、“Date”。
查看CacheInterceptor的intercept方法,缓存存储用的DiskLruCache,请求url的md5为存储的键,取出对应请求的Response后,再构建一个CacheStrategy,根据CacheStrategy中networkRequest和cacheResonse的值,会进行如上表中的使用缓存或请求服务。
//CacheInterceptor.java intercept()
Response cacheCandidate = cache != null
? cache.get(chain.request())//根据url的md5值取出对就的response
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();//这里去取缓存
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
if (cache != null) {
cache.trackResponse(strategy);//计算缓存命中次数
}
if (cacheCandidate != null && cacheResponse == null) {//无缓存
closeQuietly(cacheCandidate.body()); // The cache candidate wasn‘t applicable. Close it.
}
...
构建CacheStrategy的过程就是判断缓存是否可用的过程,通过一个内部类Factory来生成缓存策略对象CacheStrategy,Factory构造参数为原始request和当前请求缓存的response,在构造方法中初始化了Request、Response,并解析了缓存response中头字段的"Date","Expires","Last-Modified","ETag","Age"这些头字段都与缓存有关。
public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;
this.request = request;
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();//OkHttp-Sent-Millis: 1593768496550发起请求的时间
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();//OkHttp-Received-Millis: 1593768496690 收到请求的时间
Headers headers = cacheResponse.headers();//取出缓存的response里的头信息,取的头信息里并没有Cache-Control,常见的http缓存有Expires、Cache-control、Last-Modified / If-Modified-Since、Etag / If-None-Match
for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
初始化完成后,通过get方法来得到最终的CacheStrategy,在get中如果请求设置了只使用缓存,而networkRequest又不为空,此时cacheResponse为空,则会强制将networkRequest设为null,这种情况会返回错误码504。
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {//networdRequest不为null,说明要请求网络,但设置了只取缓存,从上表可以看出,此时缓存为空,故都返回空
// We're forbidden from using the network and the cache is insufficient.
return new CacheStrategy(null, null);
}
return candidate;
}
get方法又调用了getCandidate(),getCandidate代码很长,而且是根据RFC标准文档对http协议的实现。在getCondiate()中,有7处返回,依次来分析每一种返回的情况。一、无缓存,此时只能去请求服务端。二、https链接且缓存response无握手信息,不使用缓存而是请求服务端获取数据。三、缓存resonse中code为特定返回码,缓存response或请求中设置了cache-contral为nostore,则不使用缓存而是请求服务端。四、如果请求request设置Cache-Control:nostore,或头字段中设置了"If-Modified-Sice"、"If-None-Match"则不使用缓存。五、缓存response没有设置Cache-Control:nostore且缓存的reponse的年龄(从请求发出到现在时间+Age)+min-fresh< 缓存resonse无需刷新的时间(max-age或expires减去现在的时间或缓存response的服务端时间减去上次修改的时间或0)+最大有效时间must-revalidate为flase时的max-age值),那么就就使用网络请求而直接使用缓存,networkRequest为空,cacheResponse不为空。六、如果缓存头中的"ETag"为空,"Last-modified"为空,"Date"为空,则不使用缓存而是走网络请求服务端。七、如果缓存response的“ETag”不为空,会在新的请求头中加上"If-None-Match";如果缓存response的"Last-modified"不为空,会在新请求头中加上"If-Modified-Since"字段;如果缓存response的Date不为空,也会在新请求头中加上“If-Modified-Since”,值为Date;在这些情况下会networkRequest不为空,且cacheResopnse也不为空,会服务端校验缓存是否有效。
由于应用商店服务端没有缓存策略(广告有缓存的情况下会返回和上次相同的),故computFreshnessLifetime()方法返回的是0.且有网络的情况可知minFreshiMillis也是0,maxStateMillis也是0。故应用商店在有网络的情况下商店的缓存是失效的。缓存的cacheControl信息是存储在哪里的?在.1文件中吗?还是.0文件中?答案是在.0中。
/** Returns a strategy to use assuming the request can use the network. */
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
return new CacheStrategy(request, null);//第一种情况,无缓存
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {//第二种情况,是https连接且无握手信息,不使用缓存
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {;
return new CacheStrategy(request, null);//第三种情况,请求不可缓存
}
CacheControl requestCaching = request.cacheControl();//如果request设置了cacheControl
if (requestCaching.noCache() || hasConditions(request)) {//Cach-Control:nostore 或reqeust头字段中的If-Modified-Since不为空或 If-None-Match不为空,则需要进行网络请求
return new CacheStrategy(request, null);//第四种情况,不使用缓存
}
CacheControl responseCaching = cacheResponse.cacheControl();//缓存的response的缓存控制
//服务端的时间是Date 如:Date: Fri, 03 Jul 2020 09:28:14 GMT
long ageMillis = cacheResponseAge();//缓存的生命时长,(age||收到response的时间减去服务端的时间到收到缓存的时间)+本地发起请求到收到回复的时间+现在减去收到回复的时间
long freshMillis = computeFreshnessLifetime();//缓存的刷新时间,根据不同的配置的获取到的时间,maxAge或expires减去服务器回复时的时间或服务器时间减去上次修改的时间
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {//缓存时间+最小刷新时间<刷新时间+最大过期时间且缓存不需要去服务端验证,则直接使用缓存。
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());//第五种情况,缓存有效,使用缓存,不再请求服务端
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.第五种情况,没有以上标签,不使用缓存,直接请求服务端
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);//将头信息添加到Header builder中
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);//第七种情况,去服务端校验缓存并获得最新数据
}
得到CacheStrategy后,继续看CacheInterceptor中的intercept()中的实现,如果networdRequest和cacheResponse都为空,则返回504错误码。如果networkRequest为空,cacheResponse不为空,直接返回结果。如果如果networdRequest不为空,则继续将请求交给下一个拦截器执行并得到Response,如果返回码为304且缓存的response不为空则说明缓存仍有效只需要更新缓存信息即可。如果networkRequest不为空,cacheResponse为空,则将得到的返回结果缓存到本地。
//CacheInterceptor.intercept()
...
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
if (networkRequest == null) {//请求为空,缓存不为空,直接返回缓存结果
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//请求不为空的情况如下
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we‘re doing a conditional get.
if (cacheResponse != null) {//缓存不为空,且缓存内容未改变,更新缓存信息
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//缓存为空
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {//配置了缓存,且返回消息体不为,且是可缓存的,则将结果缓存到本地
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
okhttp中常见的返回码,总结一下就是2XX代表没问题,3XX代表重定向无修改,4XX代表客户端错误,5XX代表服务端错误
/* 2XX: generally "OK" */
/**
* HTTP Status-Code 200: OK.
*/
public static final int HTTP_OK = 200;
/**
* HTTP Status-Code 201: Created.
*/
public static final int HTTP_CREATED = 201;
/**
* HTTP Status-Code 202: Accepted.
*/
public static final int HTTP_ACCEPTED = 202;
/**
* HTTP Status-Code 203: Non-Authoritative Information.
*/
public static final int HTTP_NOT_AUTHORITATIVE = 203;
/**
* HTTP Status-Code 204: No Content.
*/
public static final int HTTP_NO_CONTENT = 204;
/**
* HTTP Status-Code 205: Reset Content.
*/
public static final int HTTP_RESET = 205;
/**
* HTTP Status-Code 206: Partial Content.
*/
public static final int HTTP_PARTIAL = 206;
/* 3XX: relocation/redirect */
/**
* HTTP Status-Code 300: Multiple Choices.
*/
public static final int HTTP_MULT_CHOICE = 300;
/**
* HTTP Status-Code 301: Moved Permanently.
*/
public static final int HTTP_MOVED_PERM = 301;
/**
* HTTP Status-Code 302: Temporary Redirect.
*/
public static final int HTTP_MOVED_TEMP = 302;
/**
* HTTP Status-Code 303: See Other.
*/
public static final int HTTP_SEE_OTHER = 303;
/**
* HTTP Status-Code 304: Not Modified.
*/
public static final int HTTP_NOT_MODIFIED = 304;
/**
* HTTP Status-Code 305: Use Proxy.
*/
public static final int HTTP_USE_PROXY = 305;
/* 4XX: client error */
/**
* HTTP Status-Code 400: Bad Request.
*/
public static final int HTTP_BAD_REQUEST = 400;
/**
* HTTP Status-Code 401: Unauthorized.
*/
public static final int HTTP_UNAUTHORIZED = 401;
/**
* HTTP Status-Code 402: Payment Required.
*/
public static final int HTTP_PAYMENT_REQUIRED = 402;
/**
* HTTP Status-Code 403: Forbidden.
*/
public static final int HTTP_FORBIDDEN = 403;
/**
* HTTP Status-Code 404: Not Found.
*/
public static final int HTTP_NOT_FOUND = 404;
/**
* HTTP Status-Code 405: Method Not Allowed.
*/
public static final int HTTP_BAD_METHOD = 405;
/**
* HTTP Status-Code 406: Not Acceptable.
*/
public static final int HTTP_NOT_ACCEPTABLE = 406;
/**
* HTTP Status-Code 407: Proxy Authentication Required.
*/
public static final int HTTP_PROXY_AUTH = 407;
/**
* HTTP Status-Code 408: Request Time-Out.
*/
public static final int HTTP_CLIENT_TIMEOUT = 408;
/**
* HTTP Status-Code 409: Conflict.
*/
public static final int HTTP_CONFLICT = 409;
/**
* HTTP Status-Code 410: Gone.
*/
public static final int HTTP_GONE = 410;
/**
* HTTP Status-Code 411: Length Required.
*/
public static final int HTTP_LENGTH_REQUIRED = 411;
/**
* HTTP Status-Code 412: Precondition Failed.
*/
public static final int HTTP_PRECON_FAILED = 412;
/**
* HTTP Status-Code 413: Request Entity Too Large.
*/
public static final int HTTP_ENTITY_TOO_LARGE = 413;
/**
* HTTP Status-Code 414: Request-URI Too Large.
*/
public static final int HTTP_REQ_TOO_LONG = 414;
/**
* HTTP Status-Code 415: Unsupported Media Type.
*/
public static final int HTTP_UNSUPPORTED_TYPE = 415;
/* 5XX: server error */
/**
* HTTP Status-Code 500: Internal Server Error.
* @deprecated it is misplaced and shouldn't have existed.
*/
@Deprecated
public static final int HTTP_SERVER_ERROR = 500;
/**
* HTTP Status-Code 500: Internal Server Error.
*/
public static final int HTTP_INTERNAL_ERROR = 500;
/**
* HTTP Status-Code 501: Not Implemented.
*/
public static final int HTTP_NOT_IMPLEMENTED = 501;
/**
* HTTP Status-Code 502: Bad Gateway.
*/
public static final int HTTP_BAD_GATEWAY = 502;
/**
* HTTP Status-Code 503: Service Unavailable.
*/
public static final int HTTP_UNAVAILABLE = 503;
/**
* HTTP Status-Code 504: Gateway Timeout.
*/
public static final int HTTP_GATEWAY_TIMEOUT = 504;
/**
* HTTP Status-Code 505: HTTP Version Not Supported.
*/
public static final int HTTP_VERSION = 505;
6.缓存如何保存以及缓存的文件格式
文件格式与内容
在配置了OkHttp的cache后,在配置的路径下会有三种格式的文件:.0;.1;journal。在缓存路径下会有如下的文件
journal
eefb65edb4347414a781ba68f5d7646f.0
eefb65edb4347414a781ba68f5d7646f.1
journal是DiskLruCache日志文件,以下是一个journal文件中的内容:
libcore.io.DiskLruCache //固定写libcor.io.DiskLruCache
1 //缓存版本
201105 //应用版本
2 //对应几个文件
DIRTY eefb65edb4347414a781ba68f5d7646f //操作记录
CLEAN eefb65edb4347414a781ba68f5d7646f 394 15039
READ eefb65edb4347414a781ba68f5d7646f
DIRTY eefb65edb4347414a781ba68f5d7646f
CLEAN eefb65edb4347414a781ba68f5d7646f 394 15040
接下来每一行是一次操作记录,每次操作Cache都会产生一条。
DIRTY:说明缓存数据正在创建或更新,每个成功的DIRTY都要对应一个CLEAN或REMOVE,如果对不上,说明操作失败,要清理
CLEAN:说明操作成功,每行后面记录value的长度
READ:一次读取
REMOVE:一次清除
.0文件保存的请求信息与头字段,以下是商店中的一个.0文件:
http://api-app.meizu.com/apps/public/guider?app_count=50&version=v2
GET
1
Accept-Encoding: gzip
HTTP/1.1 200 OK
10
Server: nginx
Date: Fri, 03 Jul 2020 09:23:12 GMT
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
Access-Control-Allow-Origin: 0
Content-Encoding: gzip
OkHttp-Sent-Millis: 1593768195088
OkHttp-Received-Millis: 1593768195238
.1文件中存储是真的缓存内容,经过加密,直接打开查看是乱码。
Cache
Cache是OkHttp中缓存文件的操作类,里面实现了缓存文件的增删改查,磁盘上的日志文件是如何关联Cache并支持增删改查的呢,我们从底层File开始,逐层解开okhttp对缓存数据的管理。
Cache内部使用了DiskLruCache,这个DiskLruCache是okhttp自己实现的,其中有三个重要的内部类:Entry、Editro、Snapshot。
第一步:FileSystem封装File的操作
FileSystem封装了File常用操作,没有使用java.io的InputStream和OutputStream作为输入输出流,取而代之的是File和okio。FileSystem是个接口,直接在interface里提供了个实现类SYSTEM。Sink是对字节流写操作的接口,Source是对字节流读的接口。Okio中将输入字节流输出为Sink或Source。
public interface FileSystem {
Source source(File file) throws FileNotFoundException;
Sink sink(File file) throws FileNotFoundException;
Sink appendingSink(File file) throws FileNotFoundException;
void delete(File file) throws IOException;
boolean exists(File file);
long size(File file);
void rename(File from, File to) throws IOException;
void deleteContents(File directory) throws IOException;
}
第二步:DiskLruCache.Entry和DiskLruCache.Editor
private final class Entry {
final String key;
final File[] cleanFiles;
final File[] dirtyFiles;
//...
}
DiskLruCache.Entry维护请求url对应的缓存文件,url的md5作为key,value_count说明对应几个文件,预设是2。cleanFiles和dirtyFiles就是对应上面讲的CLEAN和DIRTY,描述数据进入修改和已经稳定两种状态。
看上面我们实操得到的两个缓存文件,名字都是key,结尾不同:
.0:记录请求的内容和握手信息;
.1:真正缓存的内容。
Cache上定义了0和1常量以及valueCount 2:
private static final int ENTRY_METADATA = 0;
private static final int ENTRY_BODY = 1;
private static final int ENTRY_COUNT = 2;
操作DiskLruCache.Entry的是DiskLruCache.Editor,它的构造函数传入DiskLruCache.Entry对象,Editor中里面有两个方法:
public Source newSource(int index){}
public Sink newSink(int index){}
通过传入的index定位,读取cleanFiles,写入dirtyFiles,对外暴露okio的Source和Sink。于是,我们可以通过DiskLruCache.Editor读写磁盘上的缓存文件了。
第三步:Snapshot封装缓存结果
从DiskLruCache获取缓存结果,不是返回DiskLruCache.Entry,而是缓存快照Snapshot。我们只关心当前缓存的内容,其他东西知道得越少越好。
public final class Snapshot implements Closeable {
private final String key;
private final Source[] sources;
//...
}
Snapshot保存了key和sources,sources的来源通过FileSystem获取cleanFiles的Source。
//DiskLruCache.Entry.snapshot())
Snapshot snapshot() {
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
Source[] sources = new Source[valueCount];
long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
try {
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
//
}
}
缓存增删改查
Cache通过InternalCache供外部包调用,提供增删改查的能力,实质调用DiskLruCache对应方法。这个InternalCache支持开发自定义实现。
interceptors.add(new CacheInterceptor(client.internalCache()));
get -> get
put -> edit
update -> edit
remove -> remove
箭头左边是Cache的方法,右边是DiskLruCache的方法。
DiskLruCache核心的数据结构是LinkedHashMap,key是字符串,对应一个Entry,要注意Cache的Entry和DiskLruCache的Entry不是同一回事。
final LinkedHashMap lruEntries = new LinkedHashMap<>(0, 0.75f, true);
下面我们分析保存缓存文件的put方法以,首先将response封装到Cache.Entry,然后获取DiskLruCache.Editor。Entry中包含了response的头信息和内容信息,然后调用DiskLruCache的edit()方法将文件写入本地文件中:
@Nullable CacheRequest put(Response response) {
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
if (!requestMethod.equals("GET")) {
// Don't cache non-GET responses. We're technically allowed to cache
// HEAD requests and some POST requests, but the complexity of doing
// so is high and the benefit is low.
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
Entry entry = new Entry(response);//将response封装到Cache.Entry中
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);//返回一个CacherReques的实例对象
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
Entry的构造方法如下,可以看到所有知道的信息都在Entry对象中,注意并没有body信息,重点看头信息,varyheaders包含了request和response的信息,如果response中包含了与request相事的头信息,以reqeust中的值为准
Entry(Response response) {
this.url = response.request().url().toString();
this.varyHeaders = HttpHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
this.protocol = response.protocol();
this.code = response.code();
this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
this.sentRequestMillis = response.sentRequestAtMillis();
this.receivedResponseMillis = response.receivedResponseAtMillis();
}
再看DiskLruCache的edit方法,通过key获取editor,里面是一系列工作:
initialize初始化,关联journal文件并按格式读取;
journal写入DIRTY行;
获取或创建DiskLruCache.Entry;
创建Editor对象。
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry != null && entry.currentEditor != null) {
return null; // Another edit is in progress.
}
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
// The OS has become our enemy! If the trim job failed, it means we are storing more data than
// requested by the user. Do not allow edits so we do not go over that limit any further. If
// the journal rebuild failed, the journal writer will not be active, meaning we will not be
// able to record the edit, causing file leaks. In both cases, we want to retry the clean up
// so we can get out of this state!
executor.execute(cleanupRunnable);
return null;
}
// Flush the journal before creating files to prevent file leaks.
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
if (hasJournalErrors) {
return null; // Don't edit; the journal can't be written.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
具体写入文件有两步,第一步调用entry.writeTo(editor),里面是一堆write操作,写入目标是ENTRY_METADATA,也就是上面说过以.0结尾的文件。这里面具体写入的内容在方法Cache.writeTo()中可以看到。头信息就是这时写入.0文件的。详细方法如下:
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
for (int i = 0, size = varyHeaders.size(); i < size; i++) {//request response共有的头信息
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(new StatusLine(protocol, code, message).toString())
.writeByte('\n');
sink.writeDecimalLong(responseHeaders.size() + 2)
.writeByte('\n');
for (int i = 0, size = responseHeaders.size(); i < size; i++) {//response的头信息
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n');
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n');
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n');
if (isHttps()) {
sink.writeByte('\n');
sink.writeUtf8(handshake.cipherSuite().javaName())
.writeByte('\n');
writeCertList(sink, handshake.peerCertificates());
writeCertList(sink, handshake.localCertificates());
sink.writeUtf8(handshake.tlsVersion().javaName()).writeByte('\n');
}
sink.close();
}
第二步调用new CacheRequestImpl(editor),返回一个CacheRequest。这里是写入缓存内容,即response中的body信息。CacheRequestImpl在构造函数里直接执行逻辑,文件操作目标是ENTRY_BODY(具体的缓存数据).1文件。可以看到最后调用了Editor的commit方法。
CacheRequestImpl(final DiskLruCache.Editor editor) {
this.editor = editor;
this.cacheOut = editor.newSink(ENTRY_BODY);
this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
synchronized (Cache.this) {
if (done) {
return;
}
done = true;
writeSuccessCount++;
}
super.close();
editor.commit();
}
};
}
DiskLruCache中的Editor有commit和abort两个重要操作,这两个方法都调用了completeEdit方法,是将DIRTY改为CLEAN,并写入CLEAN行。
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
//..
for (int i = 0; i < valueCount; i++) {
File dirty = entry.dirtyFiles[i];
if (success) {
if (fileSystem.exists(dirty)) {
File clean = entry.cleanFiles[i];
fileSystem.rename(dirty, clean);
long oldLength = entry.lengths[i];
long newLength = fileSystem.size(clean);
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
fileSystem.delete(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.writeUtf8(CLEAN).writeByte(' ');
journalWriter.writeUtf8(entry.key);
entry.writeLengths(journalWriter);
journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.writeUtf8(REMOVE).writeByte(' ');
journalWriter.writeUtf8(entry.key);
journalWriter.writeByte('\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
}
返回了CacheRequest之后,在CacheInterceptor中调用最终的写入.1文件。.1文件的路径是在创建CacheRequestImpl时指定的ENTRY_BODY,这里通过Okio完成文件的写操作。
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
/**
* Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
* consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
* may never exhaust the source stream and therefore not complete the cached response.
*/
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
分析完了最复杂的put方法,了解写入journal和缓存文件的过程。其它三个方法(get(),remove(),update())省略介绍。
7.没有服务端支持是否可以实现纯客户端缓存以及如何实现?
没有服务端的支持可以实现纯客户端缓存,实现方式是增加一个网络拦截器,即通过OkHttpClient的addNetworkInterceptor添加拦截器,并在response的头中加上cache-control,即缓存时间设置成你希望的值,这样当CacheInterceptor执行的时候,会把这些头信息都写入.0文件,下次取出缓存并比较缓存有效后就可以直接使用缓存了。