OkHttp 之缓存模块原理剖析

一、引言

在当今的移动应用和网络服务开发中,网络请求是一个不可或缺的部分。然而,频繁的网络请求不仅会增加服务器的负载,还会消耗用户的流量和设备的电量,同时也会导致应用的响应速度变慢。为了解决这些问题,缓存机制应运而生。OkHttp 作为一款广泛使用的高性能 HTTP 客户端库,提供了强大的缓存功能,能够显著提高应用的性能和用户体验。本文将深入分析 OkHttp 缓存模块的原理,从源码层面详细解读其实现机制,帮助开发者更好地理解和使用 OkHttp 的缓存功能。

二、HTTP 缓存基础

2.1 HTTP 缓存的概念和作用

HTTP 缓存是一种机制,用于减少对服务器的重复请求,提高响应速度和降低网络流量。当客户端(如浏览器或移动应用)发起一个 HTTP 请求时,它首先会检查本地缓存中是否存在该请求的响应。如果存在,并且缓存的响应仍然有效,客户端可以直接使用缓存的响应,而无需再次向服务器发送请求。这样可以节省时间和带宽,同时减轻服务器的负载。

HTTP 缓存的作用主要体现在以下几个方面:

  • 提高响应速度:直接从本地缓存中获取响应,避免了网络延迟和服务器处理时间,能够显著提高应用的响应速度。
  • 降低网络流量:减少了对服务器的重复请求,从而降低了用户的流量消耗,特别是在移动设备上,这一点尤为重要。
  • 减轻服务器负载:减少了服务器需要处理的请求数量,降低了服务器的压力,提高了服务器的性能和稳定性。

2.2 HTTP 缓存的分类

HTTP 缓存可以分为强缓存和协商缓存两种类型。

2.2.1 强缓存

强缓存是指客户端直接从本地缓存中获取响应,而无需向服务器发送请求。强缓存通过响应头中的 ExpiresCache-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),表示缓存的响应已经过期,客户端需要使用服务器返回的新响应。协商缓存通过响应头中的 ETagLast-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 缓存的工作流程可以分为以下几个步骤:

  1. 客户端发起请求:客户端向服务器发送一个 HTTP 请求。
  2. 检查强缓存:客户端首先检查本地缓存中是否存在该请求的响应,并且该响应是否仍然在强缓存的有效期内。如果是,则直接使用缓存的响应,请求结束;否则,进入下一步。
  3. 检查协商缓存 :客户端向服务器发送一个请求,询问缓存的响应是否仍然有效。请求头中会包含 If-None-MatchIf-Modified-Since 字段。
  4. 服务器响应 :服务器根据客户端发送的请求头信息,比较 ETagLast-Modified 值,判断缓存的响应是否仍然有效。如果有效,则返回 304 状态码;如果无效,则返回 200 状态码和新的响应。
  5. 更新缓存:如果服务器返回 200 状态码和新的响应,客户端会将新的响应存储到本地缓存中,并更新缓存的相关信息。

三、OkHttp 缓存模块概述

3.1 OkHttp 缓存模块的功能和优势

OkHttp 的缓存模块实现了完整的 HTTP 缓存机制,包括强缓存和协商缓存。它具有以下功能和优势:

  • 自动缓存管理:OkHttp 会自动根据 HTTP 响应头中的缓存指令,管理缓存的存储和使用。开发者无需手动处理缓存的过期时间、验证等问题,只需要配置好缓存目录和大小即可。
  • 支持多种缓存策略 :OkHttp 支持 Cache-Control 字段中的各种指令,如 max-ageno-cacheno-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 缓存模块的整体架构可以分为以下几个部分:

  1. 缓存配置 :开发者通过 Cache 类配置缓存的目录和大小。

  2. 拦截器处理CacheInterceptor 拦截器在请求发起之前检查本地缓存,在请求完成后将响应存储到缓存中。

  3. 缓存存储:使用 DiskLruCache 作为底层的缓存存储,负责缓存数据的存储和清理。

  4. 缓存验证:根据 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 方法首先根据请求生成一个缓存键,这一步和 getput 方法中的键生成逻辑一致,都是通过 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 类的 getputremove 方法中,会调用 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 通常位于拦截器链的中间位置,在 RetryAndFollowUpInterceptorBridgeInterceptor 之后,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) 方法尝试从缓存中获取该请求的响应。如果缓存中存在该请求的响应,则将其作为缓存候选响应;如果不存在,则 cacheCandidatenull

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-ControlExpiresETagLast-Modified 等)来判断是否需要发起网络请求以及是否可以使用缓存响应。networkRequest 表示需要发送的网络请求,如果不需要网络请求则为 nullcacheResponse 表示可以使用的缓存响应,如果没有可用的缓存响应则为 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();
}

如果 networkRequestcacheResponse 都为 null,说明请求无法满足(例如在 only-if-cached 模式下,缓存中没有有效的响应),此时返回一个状态码为 504 的响应。如果 networkRequestnull,说明可以直接使用缓存响应,对缓存响应进行一些处理后返回。

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) 方法将响应存储到缓存中。如果请求方法会使缓存失效(如 POSTPUTDELETE 等),则调用 Cache.remove(networkRequest) 方法从缓存中移除该请求对应的条目。

5.3 CacheInterceptor 类与其他拦截器的协作

在 OkHttp 的拦截器链中,CacheInterceptor 与其他拦截器密切协作,共同完成 HTTP 请求的处理。

  • RetryAndFollowUpInterceptor :该拦截器负责处理请求的重试和重定向逻辑。在 RetryAndFollowUpInterceptor 处理完请求的重试和重定向后,将请求传递给 CacheInterceptorCacheInterceptor 会检查缓存并根据缓存策略决定是否发起网络请求。

  • BridgeInterceptorBridgeInterceptor 负责将用户的请求转换为符合 HTTP 协议的请求,并添加必要的请求头。BridgeInterceptor 处理完请求后,将请求传递给 CacheInterceptor

  • ConnectInterceptorConnectInterceptor 负责建立与服务器的连接。如果 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 方法用于根据键获取缓存条目。它的主要步骤如下:

  1. 检查 DiskLruCache 是否已经关闭,以及键是否合法。
  2. 从 LRU 缓存中查找对应的条目,如果条目不存在或不可读,则返回 null
  3. 检查条目的所有值文件是否存在,如果有任何一个文件不存在,则返回 null
  4. 写入读取操作日志,记录该条目被读取的信息。
  5. 检查是否需要重建日志文件,如果需要,则提交一个重建日志的任务到线程池。
  6. 创建并返回一个 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 实例,用于写入或更新缓存条目。它的主要步骤如下:

  1. 检查 DiskLruCache 是否已经关闭,以及键是否合法。
  2. 从 LRU 缓存中查找对应的条目,如果条目已经有一个编辑器正在使用,则返回 null
  3. 如果条目不存在,则创建一个新的条目并添加到 LRU 缓存中。
  4. 如果条目已经存在且可读,则写入升级操作日志,标记该条目为脏数据。
  5. 创建一个新的 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 方法用于根据键移除缓存条目。它的主要步骤如下:

  1. 检查 DiskLruCache 是否已经关闭,以及键是否合法。
  2. 从 LRU 缓存中查找对应的条目,如果条目不存在或不可读,则返回 false
  3. 删除条目的所有值文件,如果删除失败则抛出异常。
  4. 写入删除操作日志,记录该条目被删除的信息。
  5. 从 LRU 缓存中移除该条目。
  6. 检查是否需要重建日志文件,如果需要,则提交一个重建日志的任务到线程池。
  7. 返回 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-ControlExpires 字段来实现。OkHttp 在 CacheInterceptor 中会根据这些字段判断缓存的响应是否仍然有效。

7.1.1 Cache-Control 字段验证

Cache-Control 字段提供了更灵活的缓存控制策略,常见的指令包括 max-ageno-cacheno-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-cacheno-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 协商缓存验证

协商缓存验证主要通过响应头中的 ETagLast-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 缓存验证的整体流程

缓存验证的整体流程如下:

  1. 强缓存验证 :在 CacheInterceptor 中,首先检查请求和响应的 Cache-Control 头,根据 max-ageno-cacheno-store 等指令判断是否可以使用缓存响应。如果 Cache-Control 头不存在或不包含有效指令,则检查 Expires 字段,根据过期时间判断是否可以使用缓存响应。

  2. 协商缓存验证 :如果强缓存验证失败,则向服务器发送一个协商请求。在请求头中添加 If-None-MatchIf-Modified-Since 字段,分别携带之前获取的 ETagLast-Modified 值。

  3. 服务器响应处理 :服务器根据客户端发送的请求头信息,比较 ETagLast-Modified 值,判断缓存的响应是否仍然有效。如果有效,则返回 304 状态码;如果无效,则返回 200 状态码和新的响应。

  4. 缓存更新:如果服务器返回 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 的缓存模块进行定制和扩展,实现更加灵活和高效的缓存管理。

相关推荐
inmK113 分钟前
蓝奏云官方版不好用?蓝云最后一版实测:轻量化 + 不限速(避更新坑) 蓝云、蓝奏云第三方安卓版、蓝云最后一版、蓝奏云无广告管理工具、安卓网盘轻量化 APP
android·工具·网盘工具
giaoho16 分钟前
Android 热点开发的相关api总结
android
咖啡の猫2 小时前
Android开发-常用布局
android·gitee
程序员老刘2 小时前
Google突然“变脸“,2026年要给全球开发者上“紧箍咒“?
android·flutter·客户端
Tans52 小时前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
雨白3 小时前
实现双向滑动的 ScalableImageView(下)
android
峥嵘life3 小时前
Android Studio新版本编译release版本apk实现
android·ide·android studio
studyForMokey5 小时前
【Android 消息机制】Handler
android
敲代码的鱼哇5 小时前
跳转原生系统设置插件 支持安卓/iOS/鸿蒙UTS组件
android·ios·harmonyos
翻滚丷大头鱼5 小时前
android View详解—动画
android