一、引言
在当今的移动应用和网络服务开发中,网络请求是一个不可或缺的部分。然而,频繁的网络请求不仅会增加服务器的负载,还会消耗用户的流量和设备的电量,同时也会导致应用的响应速度变慢。为了解决这些问题,缓存机制应运而生。OkHttp 作为一款广泛使用的高性能 HTTP 客户端库,提供了强大的缓存功能,能够显著提高应用的性能和用户体验。本文将深入分析 OkHttp 缓存模块的原理,从源码层面详细解读其实现机制,帮助开发者更好地理解和使用 OkHttp 的缓存功能。
二、HTTP 缓存基础
2.1 HTTP 缓存的概念和作用
HTTP 缓存是一种机制,用于减少对服务器的重复请求,提高响应速度和降低网络流量。当客户端(如浏览器或移动应用)发起一个 HTTP 请求时,它首先会检查本地缓存中是否存在该请求的响应。如果存在,并且缓存的响应仍然有效,客户端可以直接使用缓存的响应,而无需再次向服务器发送请求。这样可以节省时间和带宽,同时减轻服务器的负载。
HTTP 缓存的作用主要体现在以下几个方面:
- 提高响应速度:直接从本地缓存中获取响应,避免了网络延迟和服务器处理时间,能够显著提高应用的响应速度。
- 降低网络流量:减少了对服务器的重复请求,从而降低了用户的流量消耗,特别是在移动设备上,这一点尤为重要。
- 减轻服务器负载:减少了服务器需要处理的请求数量,降低了服务器的压力,提高了服务器的性能和稳定性。
2.2 HTTP 缓存的分类
HTTP 缓存可以分为强缓存和协商缓存两种类型。
2.2.1 强缓存
强缓存是指客户端直接从本地缓存中获取响应,而无需向服务器发送请求。强缓存通过响应头中的 Expires
和 Cache-Control
字段来控制。
- Expires:是一个 HTTP 1.0 规范中的字段,它指定了缓存的过期时间,是一个绝对时间。例如:
plaintext
java
Expires: Thu, 01 Dec 2024 16:00:00 GMT
表示缓存的响应在 2024 年 12 月 1 日 16:00:00 GMT 之前都是有效的。在这个时间之前,客户端可以直接使用缓存的响应。然而,由于 Expires
使用的是绝对时间,可能会受到客户端和服务器时间不一致的影响,因此在 HTTP 1.1 中引入了 Cache-Control
字段。
-
Cache-Control :是一个 HTTP 1.1 规范中的字段,它提供了更灵活的缓存控制策略。常见的
Cache-Control
指令包括:- max-age:指定了缓存的最大有效时间,是一个相对时间,单位为秒。例如:
plaintext
java
Cache-Control: max-age=3600
表示缓存的响应在 3600 秒(即 1 小时)内都是有效的。
- no-cache:表示客户端必须向服务器发送请求,验证缓存的响应是否仍然有效。
- no-store:表示不允许使用缓存,每次请求都必须从服务器获取最新的响应。
2.2.2 协商缓存
协商缓存是指客户端在使用缓存之前,需要向服务器发送一个请求,询问服务器缓存的响应是否仍然有效。如果服务器返回状态码 304(Not Modified),表示缓存的响应仍然有效,客户端可以直接使用缓存的响应;如果服务器返回状态码 200(OK),表示缓存的响应已经过期,客户端需要使用服务器返回的新响应。协商缓存通过响应头中的 ETag
和 Last-Modified
字段来控制。
- ETag :是一个唯一的标识符,用于标识资源的版本。服务器在响应中返回
ETag
字段,客户端在下次请求时,会在请求头中添加If-None-Match
字段,并将之前获取的ETag
值发送给服务器。服务器会比较客户端发送的ETag
值和当前资源的ETag
值,如果相同,则返回 304 状态码;如果不同,则返回 200 状态码和新的响应。例如:
plaintext
java
ETag: "123456789abcdef"
客户端下次请求时:
plaintext
java
If-None-Match: "123456789abcdef"
- Last-Modified :表示资源的最后修改时间。服务器在响应中返回
Last-Modified
字段,客户端在下次请求时,会在请求头中添加If-Modified-Since
字段,并将之前获取的Last-Modified
值发送给服务器。服务器会比较客户端发送的Last-Modified
值和当前资源的最后修改时间,如果相同,则返回 304 状态码;如果不同,则返回 200 状态码和新的响应。例如:
plaintext
java
Last-Modified: Thu, 01 Dec 2024 12:00:00 GMT
客户端下次请求时:
plaintext
java
If-Modified-Since: Thu, 01 Dec 2024 12:00:00 GMT
2.3 HTTP 缓存的工作流程
HTTP 缓存的工作流程可以分为以下几个步骤:
- 客户端发起请求:客户端向服务器发送一个 HTTP 请求。
- 检查强缓存:客户端首先检查本地缓存中是否存在该请求的响应,并且该响应是否仍然在强缓存的有效期内。如果是,则直接使用缓存的响应,请求结束;否则,进入下一步。
- 检查协商缓存 :客户端向服务器发送一个请求,询问缓存的响应是否仍然有效。请求头中会包含
If-None-Match
和If-Modified-Since
字段。 - 服务器响应 :服务器根据客户端发送的请求头信息,比较
ETag
和Last-Modified
值,判断缓存的响应是否仍然有效。如果有效,则返回 304 状态码;如果无效,则返回 200 状态码和新的响应。 - 更新缓存:如果服务器返回 200 状态码和新的响应,客户端会将新的响应存储到本地缓存中,并更新缓存的相关信息。
三、OkHttp 缓存模块概述
3.1 OkHttp 缓存模块的功能和优势
OkHttp 的缓存模块实现了完整的 HTTP 缓存机制,包括强缓存和协商缓存。它具有以下功能和优势:
- 自动缓存管理:OkHttp 会自动根据 HTTP 响应头中的缓存指令,管理缓存的存储和使用。开发者无需手动处理缓存的过期时间、验证等问题,只需要配置好缓存目录和大小即可。
- 支持多种缓存策略 :OkHttp 支持
Cache-Control
字段中的各种指令,如max-age
、no-cache
、no-store
等,能够满足不同场景下的缓存需求。 - 高效的缓存存储:OkHttp 使用 DiskLruCache 作为底层的缓存存储,它是一种基于磁盘的 LRU(Least Recently Used,最近最少使用)缓存,能够高效地管理缓存的存储和清理,避免缓存占用过多的磁盘空间。
- 缓存验证和更新:当缓存的响应可能过期时,OkHttp 会自动向服务器发送协商请求,验证缓存的有效性,并根据服务器的响应更新缓存。
3.2 OkHttp 缓存模块的核心类和接口
OkHttp 缓存模块涉及到多个核心类和接口,下面对它们进行详细介绍:
3.2.1 Cache 类
Cache
类是 OkHttp 缓存模块的核心类,它负责管理缓存的初始化、存储和清理。通过 Cache
类,开发者可以配置缓存的目录、大小等参数,并对缓存进行操作。以下是一个简单的使用示例:
java
java
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import java.io.File;
import java.io.IOException;
public class OkHttpCacheExample {
public static void main(String[] args) {
// 指定缓存目录
File cacheDirectory = new File("cache");
// 指定缓存大小为 10MB
long cacheSize = 10 * 1024 * 1024;
try {
// 创建 Cache 实例
Cache cache = new Cache(cacheDirectory, cacheSize);
// 创建 OkHttpClient 并配置缓存
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
// 发起请求...
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.2.2 CacheInterceptor 类
CacheInterceptor
类是 OkHttp 拦截器链中的一个拦截器,它负责处理缓存的读取和写入。在请求发起之前,CacheInterceptor
会检查本地缓存中是否存在该请求的响应,如果存在且有效,则直接返回缓存的响应;在请求完成后,CacheInterceptor
会根据响应的缓存指令,将响应存储到本地缓存中。
3.2.3 CacheRequest 接口
CacheRequest
接口表示一个缓存写入请求,它定义了将响应数据写入缓存的方法。当需要将响应存储到缓存中时,OkHttp 会创建一个 CacheRequest
实例,并调用其方法将响应数据写入缓存。
3.2.4 CacheResponse 接口
CacheResponse
接口表示一个缓存响应,它定义了从缓存中读取响应数据的方法。当从本地缓存中获取到响应时,OkHttp 会创建一个 CacheResponse
实例,并通过该实例读取缓存的响应数据。
3.3 OkHttp 缓存模块的整体架构
OkHttp 缓存模块的整体架构可以分为以下几个部分:
-
缓存配置 :开发者通过
Cache
类配置缓存的目录和大小。 -
拦截器处理 :
CacheInterceptor
拦截器在请求发起之前检查本地缓存,在请求完成后将响应存储到缓存中。 -
缓存存储:使用 DiskLruCache 作为底层的缓存存储,负责缓存数据的存储和清理。
-
缓存验证:根据 HTTP 响应头中的缓存指令,进行强缓存验证和协商缓存验证。
从架构图中可以看出,应用代码发起请求后,请求会经过拦截器链,其中 CacheInterceptor
会检查本地缓存。如果缓存有效,则直接返回缓存的响应;如果缓存无效,则发起网络请求,获取新的响应,并将其存储到本地缓存中。
四、Cache 类源码分析
4.1 Cache 类的构造函数和初始化
Cache
类的构造函数用于初始化缓存的目录和大小,并创建 DiskLruCache 实例。以下是 Cache
类的构造函数源码及详细注释:
java
java
import java.io.File;
import java.io.IOException;
import okhttp3.internal.cache.DiskLruCache;
import okhttp3.internal.cache.InternalCache;
// Cache 类定义,实现了 InternalCache 接口
public final class Cache implements InternalCache {
// 缓存目录,用于存储缓存文件
private final File directory;
// 缓存的最大大小,单位为字节
private final long maxSize;
// 磁盘 LRU 缓存实例,用于实际的缓存存储和管理
private DiskLruCache cache;
/**
* 构造函数,用于创建 Cache 实例
* @param directory 缓存目录
* @param maxSize 缓存的最大大小
*/
public Cache(File directory, long maxSize) {
// 检查传入的缓存目录是否为 null,如果为 null 则抛出异常
if (directory == null) {
throw new IllegalArgumentException("directory == null");
}
// 检查传入的缓存最大大小是否小于等于 0,如果是则抛出异常
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
// 将传入的缓存目录赋值给成员变量
this.directory = directory;
// 将传入的缓存最大大小赋值给成员变量
this.maxSize = maxSize;
}
/**
* 初始化 DiskLruCache 实例
* @return DiskLruCache 实例
* @throws IOException 如果打开缓存文件时发生 I/O 异常
*/
private synchronized DiskLruCache cache() throws IOException {
// 检查 cache 成员变量是否为 null,如果为 null 则进行初始化
if (cache == null) {
// 调用 DiskLruCache 的 open 方法打开一个新的 DiskLruCache 实例
// 第一个参数为缓存目录
// 第二个参数为应用程序的版本号,这里设置为 2
// 第三个参数为每个缓存条目的值的数量,这里设置为 2
// 第四个参数为缓存的最大大小
cache = DiskLruCache.open(directory, 2, 2, maxSize);
}
return cache;
}
/**
* 关闭缓存
* @throws IOException 如果关闭缓存文件时发生 I/O 异常
*/
public synchronized void close() throws IOException {
// 检查 cache 成员变量是否不为 null
if (cache != null) {
// 调用 DiskLruCache 的 close 方法关闭缓存
cache.close();
// 将 cache 成员变量置为 null
cache = null;
}
}
/**
* 删除缓存
* @throws IOException 如果删除缓存文件时发生 I/O 异常
*/
public synchronized void delete() throws IOException {
// 检查 cache 成员变量是否不为 null
if (cache != null) {
// 调用 DiskLruCache 的 delete 方法删除缓存
cache.delete();
// 将 cache 成员变量置为 null
cache = null;
}
}
/**
* 获取缓存目录
* @return 缓存目录
*/
public File directory() {
return directory;
}
/**
* 获取缓存的最大大小
* @return 缓存的最大大小
*/
public long maxSize() {
return maxSize;
}
/**
* 获取当前缓存使用的大小
* @return 当前缓存使用的大小
* @throws IOException 如果获取缓存大小时发生 I/O 异常
*/
public synchronized long size() throws IOException {
// 调用 cache 方法获取 DiskLruCache 实例,并调用其 size 方法获取当前缓存使用的大小
return cache().size();
}
}
在构造函数中,会检查传入的缓存目录和大小是否合法,并将其保存到成员变量中。cache()
方法用于初始化 DiskLruCache 实例,它会检查 cache
成员变量是否为空,如果为空,则调用 DiskLruCache.open()
方法打开一个新的 DiskLruCache 实例。close()
方法用于关闭缓存,delete()
方法用于删除缓存,directory()
方法用于获取缓存目录,maxSize()
方法用于获取缓存大小,size()
方法用于获取当前缓存使用的大小。
4.2 Cache 类的主要方法分析
4.2.1 get 方法
get
方法用于从缓存中获取指定请求的响应。以下是 get
方法的源码及详细注释:
java
java
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.cache.DiskLruCache;
import okhttp3.internal.cache.InternalCache;
import java.io.IOException;
// Cache 类的 get 方法,用于从缓存中获取指定请求的响应
@Override
public synchronized Response get(Request request) throws IOException {
// 根据请求生成缓存键,这里简单地使用请求的 URL 作为缓存键
String key = key(request);
// 调用 cache() 方法获取 DiskLruCache 实例,并调用其 get 方法根据缓存键获取对应的快照
DiskLruCache.Snapshot snapshot = cache().get(key);
// 如果快照为空,说明缓存中不存在该请求的响应,返回 null
if (snapshot == null) {
return null;
}
try (okhttp3.internal.cache.CacheRequest cacheRequest = new OkHttpCacheRequest(snapshot)) {
// 调用 cacheResponse 方法根据快照和缓存请求创建缓存响应
Response response = cacheResponse(snapshot, cacheRequest);
// 调用 isCacheable 方法判断响应是否可缓存
if (!isCacheable(response, request)) {
// 如果不可缓存,调用缓存请求的 abort 方法取消缓存写入
cacheRequest.abort();
return null;
}
// 如果可缓存,返回缓存响应
return response;
}
}
/**
* 生成缓存键,这里简单地使用请求的 URL 作为缓存键
* @param request 请求对象
* @return 缓存键
*/
private String key(Request request) {
return request.url().toString();
}
/**
* 根据快照和缓存请求创建缓存响应
* @param snapshot 快照对象
* @param cacheRequest 缓存请求对象
* @return 缓存响应对象
* @throws IOException 如果读取缓存数据时发生 I/O 异常
*/
private Response cacheResponse(DiskLruCache.Snapshot snapshot, okhttp3.internal.cache.CacheRequest cacheRequest) throws IOException {
// 从快照的第一个源中读取缓存的响应头
Headers headers = Headers.of(snapshot.getSource(0));
// 从快照的第二个源中读取缓存的响应体
okio.BufferedSource bodySource = snapshot.getSource(1);
// 创建响应构建器
return new Response.Builder()
// 设置请求对象
.request(cacheRequest.request())
// 设置协议版本为 HTTP_1_1
.protocol(Protocol.HTTP_1_1)
// 设置响应状态码,如果响应头中包含 Response-Code 字段,则使用该字段的值,否则默认为 200
.code(headers.get("Response-Code") != null ? Integer.parseInt(headers.get("Response-Code")) : 200)
// 设置响应消息,如果响应头中包含 Response-Message 字段,则使用该字段的值,否则为空
.message(headers.get("Response-Message") != null ? headers.get("Response-Message") : "")
// 设置响应头
.headers(headers)
// 设置响应体,包括响应体的内容类型、长度和数据源
.body(new RealResponseBody(headers.contentType(), snapshot.getLength(1), bodySource))
// 设置发送请求的时间戳为 -1,表示未发送请求
.sentRequestAtMillis(-1L)
// 设置接收响应的时间戳为当前时间
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
/**
* 判断响应是否可缓存
* @param response 响应对象
* @param request 请求对象
* @return 如果响应可缓存返回 true,否则返回 false
*/
private boolean isCacheable(Response response, Request request) {
// 根据响应的状态码判断是否可缓存
switch (response.code()) {
// 以下状态码的响应通常是可缓存的
case 200:
case 203:
case 204:
case 300:
case 301:
case 302:
case 307:
case 308:
case 404:
case 405:
case 410:
case 414:
case 501:
break;
// 303 状态码需要特殊处理,如果请求方法不允许有请求体,则不可缓存
case 303:
if (!HttpMethod.permitsRequestBody(request.method())) {
break;
}
// fall-through
default:
return false;
}
// 检查请求的 Cache-Control 头
CacheControl requestCaching = request.cacheControl();
// 如果请求的 Cache-Control 头包含 noCache 或 noStore 指令,则不可缓存
if (requestCaching.noCache() || requestCaching.noStore()) {
return false;
}
// 检查响应的 Cache-Control 头
CacheControl responseCaching = response.cacheControl();
// 如果响应的 Cache-Control 头包含 noStore 指令,则不可缓存
if (responseCaching.noStore()) {
return false;
}
return true;
}
get
方法首先根据请求生成一个缓存键,然后调用 DiskLruCache.get()
方法从缓存中获取对应的快照。如果快照为空,则表示缓存中不存在该请求的响应,返回 null
。否则,创建一个 CacheRequest
实例,并调用 cacheResponse()
方法创建缓存响应。最后,调用 isCacheable()
方法判断响应是否可缓存,如果不可缓存,则调用 CacheRequest.abort()
方法取消缓存写入,并返回 null
;如果可缓存,则返回缓存响应。
4.2.2 put 方法
put
方法用于将响应存储到缓存中。以下是 put
方法的源码及详细注释:
java
java
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.cache.DiskLruCache;
import okhttp3.internal.cache.InternalCache;
import java.io.IOException;
// Cache 类的 put 方法,用于将响应存储到缓存中
@Override
public synchronized Response put(Response response) throws IOException {
// 调用 isCacheable 方法判断响应是否可缓存
if (!isCacheable(response, response.request())) {
// 如果不可缓存,返回 null
return null;
}
// 获取响应的 Cache-Control 头信息
CacheControl responseCaching = response.cacheControl();
// 如果响应的 Cache-Control 头包含 noStore 指令,说明不允许缓存,返回 null
if (responseCaching.noStore()) {
return null;
}
// 根据请求生成缓存键
String key = key(response.request());
// 调用 cache() 方法获取 DiskLruCache 实例,并调用其 edit 方法获取一个编辑器实例
DiskLruCache.Editor editor = cache().edit(key);
// 如果编辑器为空,说明无法编辑缓存,返回 null
if (editor == null) {
return null;
}
try (okhttp3.internal.cache.CacheRequest cacheRequest = new OkHttpCacheRequest(editor)) {
// 调用 writeResponseHeadersToDisk 方法将响应头写入磁盘
writeResponseHeadersToDisk(response, editor);
// 检查响应是否有响应体
if (HttpHeaders.hasBody(response)) {
// 如果有响应体,调用 writeResponseBodyToDisk 方法将响应体写入磁盘
writeResponseBodyToDisk(response, cacheRequest);
}
// 调用编辑器的 commit 方法提交编辑
editor.commit();
// 调用 cacheResponse 方法根据编辑器的快照和缓存请求创建缓存响应
return cacheResponse(editor.snapshot(), cacheRequest);
} catch (IOException | RuntimeException e) {
// 如果在写入过程中发生异常,调用 abortQuietly 方法取消编辑
abortQuietly(editor);
return null;
}
}
/**
* 将响应头写入磁盘
* @param response 响应对象
* @param editor 编辑器对象
* @throws IOException 如果写入响应头时发生 I/O 异常
*/
private void writeResponseHeadersToDisk(Response response, DiskLruCache.Editor editor) throws IOException {
try (okio.BufferedSink sink = editor.newSink(0)) {
// 写入响应状态码
sink.writeUtf8("Response-Code: ").writeUtf8(Integer.toString(response.code())).writeUtf8("\r\n");
// 写入响应消息
sink.writeUtf8("Response-Message: ").writeUtf8(response.message()).writeUtf8("\r\n");
// 获取响应头
Headers headers = response.headers();
// 遍历响应头,将每个头信息写入磁盘
for (int i = 0; i < headers.size(); i++) {
sink.writeUtf8(headers.name(i)).writeUtf8(": ").writeUtf8(headers.value(i)).writeUtf8("\r\n");
}
}
}
/**
* 将响应体写入磁盘
* @param response 响应对象
* @param cacheRequest 缓存请求对象
* @throws IOException 如果写入响应体时发生 I/O 异常
*/
private void writeResponseBodyToDisk(Response response, okhttp3.internal.cache.CacheRequest cacheRequest) throws IOException {
try (okio.BufferedSink sink = cacheRequest.body()) {
// 获取响应体的数据源
okio.BufferedSource source = response.body().source();
// 从数据源读取数据并写入磁盘,每次读取 8192 字节
while (source.read(sink.buffer(), 8192) != -1) {
// 刷新缓冲区
sink.emit();
}
}
}
/**
* 安静地取消编辑,捕获并忽略可能的 I/O 异常
* @param editor 编辑器对象
*/
private void abortQuietly(DiskLruCache.Editor editor) {
try {
// 调用编辑器的 abort 方法取消编辑
editor.abort();
} catch (IOException ignored) {
// 忽略 I/O 异常
}
}
put
方法首先调用 isCacheable()
方法判断响应是否可缓存,如果不可缓存,则返回 null
。然后根据请求生成一个缓存键,调用 `DiskL
接着上面的继续分析啊
4.2.3 remove 方法
remove
方法用于从缓存中移除指定请求的响应。以下是 remove
方法的源码及详细注释:
java
java
import okhttp3.Request;
import okhttp3.internal.cache.DiskLruCache;
import okhttp3.internal.cache.InternalCache;
import java.io.IOException;
// Cache 类的 remove 方法,用于从缓存中移除指定请求的响应
@Override
public synchronized boolean remove(Request request) throws IOException {
// 根据请求生成缓存键
String key = key(request);
// 调用 cache() 方法获取 DiskLruCache 实例,并调用其 remove 方法移除指定键的缓存条目
// remove 方法返回一个布尔值,表示是否成功移除该条目
return cache().remove(key);
}
remove
方法首先根据请求生成一个缓存键,这一步和 get
、put
方法中的键生成逻辑一致,都是通过 key(request)
方法将请求的 URL 作为缓存键。然后调用 DiskLruCache
实例的 remove
方法,尝试从缓存中移除对应键的条目。如果成功移除,该方法返回 true
;如果缓存中不存在该条目或者移除过程中出现异常,则返回 false
。
4.3 Cache 类与其他模块的交互
Cache
类主要与 CacheInterceptor
类和 DiskLruCache
类进行交互。
4.3.1 与 CacheInterceptor 类的交互
CacheInterceptor
类是 OkHttp 拦截器链中的一个关键拦截器,它负责处理缓存的读取和写入操作。在请求发起之前,CacheInterceptor
会调用 Cache.get()
方法从缓存中查找该请求的响应;在请求完成后,CacheInterceptor
会根据响应的缓存指令,调用 Cache.put()
方法将响应存储到缓存中。以下是 CacheInterceptor
类中与 Cache
类交互的部分源码及详细注释:
java
java
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
// CacheInterceptor 类,实现了 Interceptor 接口
public final class CacheInterceptor implements Interceptor {
// 持有 Cache 实例的引用
private final Cache cache;
/**
* 构造函数,初始化 Cache 实例
* @param cache 缓存实例
*/
public CacheInterceptor(Cache cache) {
this.cache = cache;
}
@Override
public Response intercept(Chain chain) throws IOException {
// 获取当前拦截器链中的请求对象
Request request = chain.request();
// 尝试从缓存中获取该请求的响应,如果 cache 不为 null,则调用其 get 方法
Response cacheCandidate = cache != null ? cache.get(request) : null;
// 使用 CacheStrategy.Factory 根据当前时间、请求和缓存候选响应创建缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(System.currentTimeMillis(), request, cacheCandidate).get();
// 从缓存策略中获取需要发送的网络请求,如果不需要网络请求则为 null
Request networkRequest = strategy.networkRequest;
// 从缓存策略中获取可以使用的缓存响应,如果没有可用的缓存响应则为 null
Response cacheResponse = strategy.cacheResponse;
// 如果缓存实例不为 null,调用其 trackResponse 方法跟踪缓存响应情况
if (cache != null) {
cache.trackResponse(strategy);
}
// 如果既不需要网络请求,也没有可用的缓存响应,说明请求无法满足(仅使用缓存模式)
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 用于存储网络请求的响应
Response networkResponse = null;
try {
// 调用拦截器链的 proceed 方法继续处理网络请求
networkResponse = chain.proceed(networkRequest);
} finally {
// 如果网络响应为 null 且缓存候选响应不为 null,关闭缓存候选响应的响应体
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果存在缓存响应
if (cacheResponse != null) {
// 如果网络响应的状态码为 304,表示缓存响应仍然有效
if (networkResponse.code() == 304) {
// 合并缓存响应和网络响应的头信息
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();
// 调用缓存实例的 trackConditionalCacheHit 方法记录条件缓存命中情况
cache.trackConditionalCacheHit();
// 调用缓存实例的 update 方法更新缓存响应
cache.update(cacheResponse, response);
return response;
} else {
// 如果网络响应状态码不是 304,关闭缓存响应的响应体
closeQuietly(cacheResponse.body());
}
}
// 创建一个新的响应对象,包含缓存响应和网络响应的信息
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 如果缓存实例不为 null
if (cache != null) {
// 如果响应有响应体且根据缓存策略判断该响应可缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// 调用缓存实例的 put 方法将响应存储到缓存中
CacheRequest cacheRequest = cache.put(response);
// 返回一个包装后的响应,用于处理缓存写入操作
return cacheWritingResponse(cacheRequest, response);
}
// 如果请求方法会使缓存失效(如 POST、PUT、DELETE 等)
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
// 调用缓存实例的 remove 方法从缓存中移除该请求对应的条目
cache.remove(networkRequest);
} catch (IOException ignored) {
// 忽略移除过程中可能出现的 I/O 异常
}
}
}
return response;
}
// 其他辅助方法...
}
在 intercept
方法中,首先获取当前的请求对象,然后尝试从缓存中获取该请求的响应。接着使用 CacheStrategy.Factory
创建一个缓存策略,根据策略判断是否需要发起网络请求以及是否有可用的缓存响应。如果不需要网络请求且有可用的缓存响应,则直接返回缓存响应;如果需要网络请求,则继续调用拦截器链的 proceed
方法处理请求。
当网络请求完成后,如果存在缓存响应且网络响应状态码为 304,表示缓存仍然有效,此时会合并缓存响应和网络响应的头信息,更新缓存并返回更新后的响应。如果响应可缓存,则调用 Cache.put()
方法将响应存储到缓存中;如果请求方法会使缓存失效,则调用 Cache.remove()
方法从缓存中移除对应的条目。
4.3.2 与 DiskLruCache 类的交互
Cache
类使用 DiskLruCache
作为底层的缓存存储。在 Cache
类的 cache()
方法中,会调用 DiskLruCache.open()
方法打开一个新的 DiskLruCache
实例。DiskLruCache
是一个基于磁盘的 LRU(Least Recently Used,最近最少使用)缓存,它会根据缓存的大小和使用频率自动清理缓存条目,以确保缓存不会占用过多的磁盘空间。
在 Cache
类的 get
、put
和 remove
方法中,会调用 DiskLruCache
实例的相应方法来实现缓存的读取、写入和移除操作。例如,在 get
方法中调用 DiskLruCache.get()
方法获取缓存快照;在 put
方法中调用 DiskLruCache.edit()
方法获取编辑器实例进行缓存写入;在 remove
方法中调用 DiskLruCache.remove()
方法移除缓存条目。
4.4 Cache 类的线程安全性分析
Cache
类中的部分方法使用了 synchronized
关键字进行同步,如 cache()
、close()
、delete()
、size()
、get()
、put()
和 remove()
方法。这是因为 Cache
类中的 DiskLruCache
实例不是线程安全的,多个线程同时访问 DiskLruCache
可能会导致数据不一致或其他并发问题。
使用 synchronized
关键字可以确保在同一时间只有一个线程可以访问这些方法,从而保证 DiskLruCache
操作的线程安全性。例如,在 get
方法中,当一个线程正在从缓存中读取数据时,其他线程需要等待该线程完成操作后才能继续访问,避免了多个线程同时读取或修改缓存数据的情况。
然而,这种同步机制也会带来一定的性能开销。如果在高并发场景下,频繁的同步操作可能会成为性能瓶颈。为了缓解这个问题,可以考虑使用更细粒度的锁或者并发数据结构,但这需要对 Cache
类的实现进行更复杂的修改。
五、CacheInterceptor 类源码分析
5.1 CacheInterceptor 类的作用和位置
CacheInterceptor
是 OkHttp 拦截器链中的一个重要拦截器,它的主要作用是处理 HTTP 请求的缓存逻辑。在 OkHttp 的请求处理流程中,拦截器链会按照一定的顺序依次处理请求和响应,CacheInterceptor
通常位于拦截器链的中间位置,在 RetryAndFollowUpInterceptor
和 BridgeInterceptor
之后,ConnectInterceptor
之前。
它在请求发起之前检查本地缓存中是否存在该请求的有效响应,如果存在则直接返回缓存响应,避免了不必要的网络请求;在请求完成后,根据响应的缓存指令判断是否需要将响应存储到本地缓存中。
5.2 CacheInterceptor 类的 intercept 方法详细分析
java
java
import okhttp3.Cache;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.http.HttpHeaders;
import okhttp3.internal.http.HttpMethod;
import okhttp3.internal.http.Util;
import okhttp3.internal.cache.CacheStrategy;
import java.io.IOException;
// CacheInterceptor 类,实现了 Interceptor 接口
public final class CacheInterceptor implements Interceptor {
// 持有 Cache 实例的引用
private final Cache cache;
/**
* 构造函数,初始化 Cache 实例
* @param cache 缓存实例
*/
public CacheInterceptor(Cache cache) {
this.cache = cache;
}
@Override
public Response intercept(Chain chain) throws IOException {
// 获取当前拦截器链中的请求对象
Request request = chain.request();
// 尝试从缓存中获取该请求的响应,如果 cache 不为 null,则调用其 get 方法
Response cacheCandidate = cache != null ? cache.get(request) : null;
// 使用 CacheStrategy.Factory 根据当前时间、请求和缓存候选响应创建缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(System.currentTimeMillis(), request, cacheCandidate).get();
// 从缓存策略中获取需要发送的网络请求,如果不需要网络请求则为 null
Request networkRequest = strategy.networkRequest;
// 从缓存策略中获取可以使用的缓存响应,如果没有可用的缓存响应则为 null
Response cacheResponse = strategy.cacheResponse;
// 如果缓存实例不为 null,调用其 trackResponse 方法跟踪缓存响应情况
if (cache != null) {
cache.trackResponse(strategy);
}
// 如果既不需要网络请求,也没有可用的缓存响应,说明请求无法满足(仅使用缓存模式)
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 用于存储网络请求的响应
Response networkResponse = null;
try {
// 调用拦截器链的 proceed 方法继续处理网络请求
networkResponse = chain.proceed(networkRequest);
} finally {
// 如果网络响应为 null 且缓存候选响应不为 null,关闭缓存候选响应的响应体
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 如果存在缓存响应
if (cacheResponse != null) {
// 如果网络响应的状态码为 304,表示缓存响应仍然有效
if (networkResponse.code() == 304) {
// 合并缓存响应和网络响应的头信息
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();
// 调用缓存实例的 trackConditionalCacheHit 方法记录条件缓存命中情况
cache.trackConditionalCacheHit();
// 调用缓存实例的 update 方法更新缓存响应
cache.update(cacheResponse, response);
return response;
} else {
// 如果网络响应状态码不是 304,关闭缓存响应的响应体
closeQuietly(cacheResponse.body());
}
}
// 创建一个新的响应对象,包含缓存响应和网络响应的信息
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
// 如果缓存实例不为 null
if (cache != null) {
// 如果响应有响应体且根据缓存策略判断该响应可缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// 调用缓存实例的 put 方法将响应存储到缓存中
CacheRequest cacheRequest = cache.put(response);
// 返回一个包装后的响应,用于处理缓存写入操作
return cacheWritingResponse(cacheRequest, response);
}
// 如果请求方法会使缓存失效(如 POST、PUT、DELETE 等)
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
// 调用缓存实例的 remove 方法从缓存中移除该请求对应的条目
cache.remove(networkRequest);
} catch (IOException ignored) {
// 忽略移除过程中可能出现的 I/O 异常
}
}
}
return response;
}
/**
* 关闭资源并忽略可能的异常
* @param closeable 可关闭的资源
*/
private void closeQuietly(okio.Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (RuntimeException rethrown) {
throw rethrown;
} catch (Exception ignored) {
}
}
}
/**
* 合并两个 Headers 对象
* @param a 第一个 Headers 对象
* @param b 第二个 Headers 对象
* @return 合并后的 Headers 对象
*/
private Headers combine(Headers a, Headers b) {
Headers.Builder result = new Headers.Builder();
for (int i = 0; i < a.size(); i++) {
String fieldName = a.name(i);
String value = a.value(i);
if (!"Content-Length".equalsIgnoreCase(fieldName) && !isEndToEnd(fieldName)) {
result.add(fieldName, value);
}
}
for (int i = 0; i < b.size(); i++) {
String fieldName = b.name(i);
String value = b.value(i);
if (isEndToEnd(fieldName)) {
result.add(fieldName, value);
}
}
return result.build();
}
/**
* 判断 Header 字段是否为端到端字段
* @param fieldName Header 字段名
* @return 如果是端到端字段返回 true,否则返回 false
*/
private boolean isEndToEnd(String fieldName) {
return !"Connection".equalsIgnoreCase(fieldName)
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
&& !"TE".equalsIgnoreCase(fieldName)
&& !"Trailers".equalsIgnoreCase(fieldName)
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
/**
* 移除响应体的包装方法
* @param response 响应对象
* @return 移除响应体后的响应对象
*/
private Response stripBody(Response response) {
return response != null && response.body() != null
? response.newBuilder().body(null).build()
: response;
}
/**
* 返回一个包装后的响应,用于处理缓存写入操作
* @param cacheRequest 缓存请求对象
* @param response 响应对象
* @return 包装后的响应对象
*/
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response) {
if (cacheRequest == null) return response;
return response.newBuilder()
.body(new CacheWritingResponseBody(cacheRequest, response.body()))
.build();
}
}
5.2.1 缓存查找阶段
java
java
Request request = chain.request();
Response cacheCandidate = cache != null ? cache.get(request) : null;
首先从拦截器链中获取当前的请求对象,然后检查 Cache
实例是否存在。如果存在,则调用 Cache.get(request)
方法尝试从缓存中获取该请求的响应。如果缓存中存在该请求的响应,则将其作为缓存候选响应;如果不存在,则 cacheCandidate
为 null
。
5.2.2 缓存策略制定阶段
java
java
CacheStrategy strategy = new CacheStrategy.Factory(System.currentTimeMillis(), request, cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
使用 CacheStrategy.Factory
根据当前时间、请求和缓存候选响应创建一个缓存策略。CacheStrategy
类会根据请求和响应的缓存指令(如 Cache-Control
、Expires
、ETag
、Last-Modified
等)来判断是否需要发起网络请求以及是否可以使用缓存响应。networkRequest
表示需要发送的网络请求,如果不需要网络请求则为 null
;cacheResponse
表示可以使用的缓存响应,如果没有可用的缓存响应则为 null
。
5.2.3 缓存命中处理阶段
java
java
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(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 (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
如果 networkRequest
和 cacheResponse
都为 null
,说明请求无法满足(例如在 only-if-cached
模式下,缓存中没有有效的响应),此时返回一个状态码为 504 的响应。如果 networkRequest
为 null
,说明可以直接使用缓存响应,对缓存响应进行一些处理后返回。
5.2.4 网络请求阶段
java
java
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
如果需要发起网络请求,调用拦截器链的 proceed(networkRequest)
方法继续处理网络请求。在请求过程中,如果出现异常导致网络响应为 null
,且缓存候选响应不为 null
,则关闭缓存候选响应的响应体,释放资源。
5.2.5 缓存更新和合并阶段
java
java
if (cacheResponse != null) {
if (networkResponse.code() == 304) {
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();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
如果存在缓存响应,且网络响应的状态码为 304,表示缓存响应仍然有效。此时合并缓存响应和网络响应的头信息,更新缓存并返回更新后的响应。如果网络响应状态码不是 304,则关闭缓存响应的响应体。
5.2.6 缓存存储和失效处理阶段
java
java
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
}
}
}
return response;
创建一个新的响应对象,包含缓存响应和网络响应的信息。如果响应有响应体且根据缓存策略判断该响应可缓存,则调用 Cache.put(response)
方法将响应存储到缓存中。如果请求方法会使缓存失效(如 POST
、PUT
、DELETE
等),则调用 Cache.remove(networkRequest)
方法从缓存中移除该请求对应的条目。
5.3 CacheInterceptor 类与其他拦截器的协作
在 OkHttp 的拦截器链中,CacheInterceptor
与其他拦截器密切协作,共同完成 HTTP 请求的处理。
-
RetryAndFollowUpInterceptor :该拦截器负责处理请求的重试和重定向逻辑。在
RetryAndFollowUpInterceptor
处理完请求的重试和重定向后,将请求传递给CacheInterceptor
。CacheInterceptor
会检查缓存并根据缓存策略决定是否发起网络请求。 -
BridgeInterceptor :
BridgeInterceptor
负责将用户的请求转换为符合 HTTP 协议的请求,并添加必要的请求头。BridgeInterceptor
处理完请求后,将请求传递给CacheInterceptor
。 -
ConnectInterceptor :
ConnectInterceptor
负责建立与服务器的连接。如果CacheInterceptor
决定发起网络请求,则将请求传递给ConnectInterceptor
进行连接建立和请求发送。
六、DiskLruCache 类分析
6.1 DiskLruCache 的作用和原理
DiskLruCache
是 OkHttp 缓存模块使用的底层缓存存储实现,它是一个基于磁盘的 LRU(Least Recently Used,最近最少使用)缓存。其主要作用是将缓存数据存储在磁盘上,并根据缓存的大小和使用频率自动清理缓存条目,以确保缓存不会占用过多的磁盘空间。
DiskLruCache
的工作原理基于 LRU 算法,即最近最少使用的缓存条目会被优先清理。它使用一个日志文件(`
接着上面的继续分析回答啊
6.1 DiskLruCache 的作用和原理(续)
journal
文件)来记录缓存条目的操作信息,包括创建、读取、删除等。通过这个日志文件,DiskLruCache
可以跟踪每个缓存条目的使用情况,并在缓存空间不足时,按照 LRU 策略清理最近最少使用的条目。
DiskLruCache
将每个缓存条目存储为一个或多个文件,每个条目由一个唯一的键(key)标识。在 OkHttp 中,通常使用请求的 URL 作为键。每个条目可以包含多个值(value),在 OkHttp 的实现中,每个条目包含两个值:一个用于存储响应头信息,另一个用于存储响应体信息。
6.2 DiskLruCache 的核心方法分析
6.2.1 open 方法
java
java
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
import okio.Okio;
import okio.Sink;
import okio.Source;
// DiskLruCache 类的 open 方法,用于打开一个新的 DiskLruCache 实例
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
// 创建一个单线程的线程池,用于执行后台任务
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 创建一个 DiskLruCache 实例
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, executorService);
// 调用 load 方法加载缓存日志文件
cache.load();
return cache;
}
open
方法是 DiskLruCache
的静态工厂方法,用于创建并打开一个新的 DiskLruCache
实例。它接受四个参数:
-
directory
:缓存文件存储的目录。 -
appVersion
:应用程序的版本号,当版本号发生变化时,DiskLruCache
会清空缓存。 -
valueCount
:每个缓存条目包含的值的数量,在 OkHttp 中通常为 2。 -
maxSize
:缓存的最大大小,当缓存使用的空间超过该值时,会触发缓存清理操作。
在方法内部,首先创建一个单线程的线程池,用于执行后台任务。然后创建一个 DiskLruCache
实例,并调用 load
方法加载缓存日志文件,最后返回该实例。
6.2.2 get 方法
java
java
// DiskLruCache 类的 get 方法,用于根据键获取缓存条目
public synchronized Snapshot get(String key) throws IOException {
// 检查键是否合法
checkNotClosed();
validateKey(key);
// 从 LRU 缓存中获取条目
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
// 检查条目是否可读
if (!entry.readable) {
return null;
}
// 检查所有值文件是否存在
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (!file.exists()) {
return null;
}
}
// 写入读取操作日志
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
if (journalRebuildRequired()) {
executorService.submit(new RebuildJournalTask());
}
// 创建并返回快照
return new Snapshot(key, entry.sequenceNumber, entry.lengths,
new FileInputStream(entry.getCleanFile(0)),
new FileInputStream(entry.getCleanFile(1)));
}
get
方法用于根据键获取缓存条目。它的主要步骤如下:
- 检查
DiskLruCache
是否已经关闭,以及键是否合法。 - 从 LRU 缓存中查找对应的条目,如果条目不存在或不可读,则返回
null
。 - 检查条目的所有值文件是否存在,如果有任何一个文件不存在,则返回
null
。 - 写入读取操作日志,记录该条目被读取的信息。
- 检查是否需要重建日志文件,如果需要,则提交一个重建日志的任务到线程池。
- 创建并返回一个
Snapshot
对象,该对象包含了缓存条目的相关信息和值文件的输入流。
6.2.3 edit 方法
java
java
// DiskLruCache 类的 edit 方法,用于获取一个编辑器实例,用于写入或更新缓存条目
public synchronized Editor edit(String key) throws IOException {
// 检查键是否合法
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry != null && entry.currentEditor != null) {
return null; // Another edit is in progress.
}
// 如果条目不存在,创建一个新的条目
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.readable) {
// 写入升级操作日志
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
}
// 创建并返回编辑器实例
entry.currentEditor = new Editor(entry);
return entry.currentEditor;
}
edit
方法用于获取一个 Editor
实例,用于写入或更新缓存条目。它的主要步骤如下:
- 检查
DiskLruCache
是否已经关闭,以及键是否合法。 - 从 LRU 缓存中查找对应的条目,如果条目已经有一个编辑器正在使用,则返回
null
。 - 如果条目不存在,则创建一个新的条目并添加到 LRU 缓存中。
- 如果条目已经存在且可读,则写入升级操作日志,标记该条目为脏数据。
- 创建一个新的
Editor
实例,并将其赋值给条目的currentEditor
属性,最后返回该实例。
6.2.4 remove 方法
java
java
// DiskLruCache 类的 remove 方法,用于根据键移除缓存条目
public synchronized boolean remove(String key) throws IOException {
// 检查键是否合法
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null ||!entry.readable) {
return false;
}
// 删除所有值文件
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() &&!file.delete()) {
throw new IOException("Failed to delete " + file);
}
}
// 写入删除操作日志
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(key).writeByte('\n');
lruEntries.remove(key);
// 检查是否需要重建日志文件
if (journalRebuildRequired()) {
executorService.submit(new RebuildJournalTask());
}
return true;
}
remove
方法用于根据键移除缓存条目。它的主要步骤如下:
- 检查
DiskLruCache
是否已经关闭,以及键是否合法。 - 从 LRU 缓存中查找对应的条目,如果条目不存在或不可读,则返回
false
。 - 删除条目的所有值文件,如果删除失败则抛出异常。
- 写入删除操作日志,记录该条目被删除的信息。
- 从 LRU 缓存中移除该条目。
- 检查是否需要重建日志文件,如果需要,则提交一个重建日志的任务到线程池。
- 返回
true
表示移除成功。
6.3 DiskLruCache 的日志文件管理
DiskLruCache
使用一个日志文件(journal
文件)来记录缓存条目的操作信息,包括创建、读取、删除等。日志文件的格式如下:
plaintext
java
libcore.io.DiskLruCache
1
100
2
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
READ 335c4c6028171cfddfbaae1a9c313c52
-
第一行:固定字符串
libcore.io.DiskLruCache
,用于标识日志文件的类型。 -
第二行:应用程序的版本号。
-
第三行:缓存条目的值的数量。
-
第四行:空行,作为分隔符。
-
后续行:记录缓存条目的操作信息,每行的格式为
操作类型 键 [值的长度1 值的长度2 ...]
。
常见的操作类型包括:
-
CLEAN
:表示缓存条目已成功写入,后面跟着键和每个值的长度。 -
DIRTY
:表示缓存条目正在被写入或更新。 -
REMOVE
:表示缓存条目已被删除。 -
READ
:表示缓存条目已被读取。
DiskLruCache
在每次进行缓存操作时,都会写入相应的日志记录。当日志文件的大小超过一定阈值时,会触发日志文件的重建操作,将日志文件精简为只包含当前有效的缓存条目信息,以减少日志文件的大小和提高读取性能。
6.4 DiskLruCache 的缓存清理策略
DiskLruCache
使用 LRU(Least Recently Used,最近最少使用)算法来清理缓存。当缓存使用的空间超过设定的最大大小时,会优先清理最近最少使用的缓存条目,直到缓存使用的空间小于最大大小。
在 DiskLruCache
中,每次读取或写入缓存条目时,会将该条目移动到 LRU 缓存的头部,表示该条目是最近使用的。当需要清理缓存时,会从 LRU 缓存的尾部开始依次删除条目,直到缓存使用的空间小于最大大小。
以下是 DiskLruCache
中清理缓存的代码片段:
java
java
// DiskLruCache 类中清理缓存的方法
private void trimToSize() throws IOException {
while (size > maxSize) {
// 获取 LRU 缓存的尾部条目
Map.Entry<String, Entry> toEvict = lruEntries.eldest();
if (toEvict == null) {
break; // 没有更多条目可清理
}
// 移除尾部条目
remove(toEvict.getKey());
}
}
在 trimToSize
方法中,会不断检查缓存使用的空间是否超过最大大小。如果超过,则获取 LRU 缓存的尾部条目(即最近最少使用的条目),并调用 remove
方法将其移除,直到缓存使用的空间小于最大大小。
七、缓存验证机制分析
7.1 强缓存验证
强缓存验证主要通过响应头中的 Cache-Control
和 Expires
字段来实现。OkHttp 在 CacheInterceptor
中会根据这些字段判断缓存的响应是否仍然有效。
7.1.1 Cache-Control 字段验证
Cache-Control
字段提供了更灵活的缓存控制策略,常见的指令包括 max-age
、no-cache
、no-store
等。
java
java
// CacheInterceptor 中检查 Cache-Control 字段的部分代码
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || requestCaching.noStore()) {
// 请求不允许使用缓存,直接发起网络请求
return null;
}
CacheControl responseCaching = response.cacheControl();
if (responseCaching.noStore()) {
// 响应不允许使用缓存,不进行缓存存储
return null;
}
if (responseCaching.maxAgeSeconds() != -1) {
long age = HttpHeaders.cacheAge(response);
if (age < responseCaching.maxAgeSeconds()) {
// 缓存未过期,可以使用缓存响应
return cacheResponse;
}
}
在上述代码中,首先检查请求的 Cache-Control
头,如果包含 no-cache
或 no-store
指令,则直接发起网络请求,不使用缓存。然后检查响应的 Cache-Control
头,如果包含 no-store
指令,则不进行缓存存储。最后,根据 max-age
指令计算缓存的年龄,如果缓存的年龄小于 max-age
,则表示缓存未过期,可以使用缓存响应。
7.1.2 Expires 字段验证
Expires
字段是一个 HTTP 1.0 规范中的字段,它指定了缓存的过期时间,是一个绝对时间。
java
java
// CacheInterceptor 中检查 Expires 字段的部分代码
String expires = response.header("Expires");
if (expires != null) {
try {
Date expiresDate = HttpDate.parse(expires);
if (expiresDate.after(new Date())) {
// 缓存未过期,可以使用缓存响应
return cacheResponse;
}
} catch (IllegalArgumentException e) {
// 解析 Expires 字段失败,忽略该字段
}
}
在上述代码中,首先从响应头中获取 Expires
字段的值,然后尝试将其解析为日期对象。如果解析成功且过期时间在当前时间之后,则表示缓存未过期,可以使用缓存响应。如果解析失败,则忽略该字段。
7.2 协商缓存验证
协商缓存验证主要通过响应头中的 ETag
和 Last-Modified
字段来实现。当强缓存验证失败时,OkHttp 会向服务器发送一个协商请求,询问缓存的响应是否仍然有效。
7.2.1 ETag 字段验证
ETag
是一个唯一的标识符,用于标识资源的版本。服务器在响应中返回 ETag
字段,客户端在下次请求时,会在请求头中添加 If-None-Match
字段,并将之前获取的 ETag
值发送给服务器。
java
java
// CacheInterceptor 中处理 ETag 字段的部分代码
String etag = cacheResponse.header("ETag");
if (etag != null) {
networkRequest = request.newBuilder()
.header("If-None-Match", etag)
.build();
}
在上述代码中,首先从缓存响应的头中获取 ETag
字段的值。如果 ETag
字段存在,则在发起网络请求时,在请求头中添加 If-None-Match
字段,并将 ETag
值作为其值。服务器会比较客户端发送的 ETag
值和当前资源的 ETag
值,如果相同,则返回 304 状态码,表示缓存的响应仍然有效。
7.2.2 Last-Modified 字段验证
Last-Modified
表示资源的最后修改时间。服务器在响应中返回 Last-Modified
字段,客户端在下次请求时,会在请求头中添加 If-Modified-Since
字段,并将之前获取的 Last-Modified
值发送给服务器。
java
java
// CacheInterceptor 中处理 Last-Modified 字段的部分代码
String lastModified = cacheResponse.header("Last-Modified");
if (lastModified != null) {
networkRequest = request.newBuilder()
.header("If-Modified-Since", lastModified)
.build();
}
在上述代码中,首先从缓存响应的头中获取 Last-Modified
字段的值。如果 Last-Modified
字段存在,则在发起网络请求时,在请求头中添加 If-Modified-Since
字段,并将 Last-Modified
值作为其值。服务器会比较客户端发送的 Last-Modified
值和当前资源的最后修改时间,如果相同,则返回 304 状态码,表示缓存的响应仍然有效。
7.3 缓存验证的整体流程
缓存验证的整体流程如下:
-
强缓存验证 :在
CacheInterceptor
中,首先检查请求和响应的Cache-Control
头,根据max-age
、no-cache
、no-store
等指令判断是否可以使用缓存响应。如果Cache-Control
头不存在或不包含有效指令,则检查Expires
字段,根据过期时间判断是否可以使用缓存响应。 -
协商缓存验证 :如果强缓存验证失败,则向服务器发送一个协商请求。在请求头中添加
If-None-Match
和If-Modified-Since
字段,分别携带之前获取的ETag
和Last-Modified
值。 -
服务器响应处理 :服务器根据客户端发送的请求头信息,比较
ETag
和Last-Modified
值,判断缓存的响应是否仍然有效。如果有效,则返回 304 状态码;如果无效,则返回 200 状态码和新的响应。 -
缓存更新:如果服务器返回 304 状态码,表示缓存的响应仍然有效,此时合并缓存响应和网络响应的头信息,更新缓存并返回更新后的响应。如果服务器返回 200 状态码和新的响应,则将新的响应存储到缓存中,并返回新的响应。
八、OkHttp 缓存模块的使用场景和注意事项
8.1 使用场景
8.1.1 频繁请求的静态资源
对于一些不经常变化的静态资源,如图片、CSS 文件、JavaScript 文件等,可以使用 OkHttp 的缓存功能来减少网络请求,提高应用的响应速度。例如,在一个图片浏览应用中,每次打开图片时都可以先从缓存中查找,如果缓存中存在且有效,则直接使用缓存的图片,避免了重复的网络请求。
8.1.2 数据更新不频繁的接口
对于一些数据更新不频繁的接口,如新闻列表、商品列表等,可以使用缓存来减少对服务器的请求压力。例如,在一个新闻应用中,新闻列表的数据可能每隔一段时间才会更新一次,在这段时间内可以使用缓存的新闻列表数据,提高应用的响应速度。
8.1.3 弱网络环境下的应用
在弱网络环境下,网络请求的响应时间会很长,甚至可能会失败。使用 OkHttp 的缓存功能可以在网络不稳定时,优先使用缓存的响应,提高应用的可用性。例如,在地铁、电梯等弱网络环境下,用户打开应用时可以先显示缓存的数据,等待网络恢复后再更新数据。
8.2 注意事项
8.2.1 缓存大小的设置
在使用 OkHttp 的缓存功能时,需要合理设置缓存的大小。如果缓存设置得太小,可能会导致缓存频繁清理,无法充分发挥缓存的作用;如果缓存设置得太大,可能会占用过多的磁盘空间,影响设备的性能。一般来说,可以根据应用的需求和设备的存储空间来合理设置缓存大小。
8.2.2 缓存过期时间的控制
需要根据资源的更新频率来合理设置缓存的过期时间。对于更新频繁的资源,如实时数据接口,应该设置较短的过期时间,以确保用户能够及时获取到最新的数据;对于更新不频繁的资源,如静态图片,可以设置较长的过期时间,以减少网络请求。
8.2.3 缓存验证机制的理解
需要理解 OkHttp 的缓存验证机制,包括强缓存验证和协商缓存验证。在某些情况下,可能需要手动控制缓存的验证过程,例如,在用户进行刷新操作时,需要强制发起网络请求,忽略缓存。
8.2.4 缓存清理策略
OkHttp 的缓存模块会自动根据 LRU 算法清理缓存,但在某些情况下,可能需要手动清理缓存。例如,当应用升级、用户登录或退出等操作时,可能需要清理缓存以确保数据的一致性。
九、总结
OkHttp 的缓存模块是一个强大且高效的 HTTP 缓存实现,它实现了完整的 HTTP 缓存机制,包括强缓存和协商缓存。通过 Cache
类、CacheInterceptor
类和 DiskLruCache
类的协作,OkHttp 能够自动管理缓存的存储和使用,根据 HTTP 响应头中的缓存指令进行缓存验证和更新,同时使用 LRU 算法清理缓存,避免缓存占用过多的磁盘空间。
在使用 OkHttp 的缓存模块时,开发者需要了解 HTTP 缓存的基础知识,合理设置缓存的大小和过期时间,理解缓存验证机制和缓存清理策略。通过正确使用 OkHttp 的缓存功能,可以显著提高应用的性能和用户体验,减少对服务器的请求压力,降低用户的流量消耗。
未来,随着网络技术的不断发展和应用场景的不断变化,OkHttp 的缓存模块可能会进一步优化和扩展,提供更多的缓存策略和配置选项,以满足不同开发者的需求。同时,开发者也可以根据自己的业务需求,对 OkHttp 的缓存模块进行定制和扩展,实现更加灵活和高效的缓存管理。