揭秘 Android Volley 缓存模块
一、引言
在移动应用开发中,网络请求是必不可少的一部分。然而,频繁的网络请求不仅会增加用户的流量消耗,还会影响应用的响应速度和用户体验。为了解决这些问题,Android Volley 框架提供了强大的缓存机制,可以有效减少不必要的网络请求,提高应用性能。
本文将深入剖析 Android Volley 框架的缓存模块,从源码级别详细解析其实现原理和工作流程。我们将探讨缓存的基本概念、Volley 缓存模块的架构设计、核心类和接口的实现,以及缓存策略、LRU 算法、磁盘存储等关键技术点。通过本文的学习,读者将对 Volley 缓存模块有一个全面而深入的理解,能够在实际开发中更加灵活和高效地使用缓存机制。
二、缓存基础概念
2.1 缓存的定义和作用
缓存是一种数据存储技术,它将经常访问的数据存储在高速存储介质中,以便下次访问时可以更快地获取数据。在 Android 应用开发中,缓存主要用于减少网络请求、提高应用响应速度、降低服务器负载和节省用户流量。
2.2 缓存的分类
根据缓存的位置和存储介质,缓存可以分为以下几类:
- 内存缓存:将数据存储在内存中,访问速度最快,但容量有限,且应用退出后数据会丢失。
- 磁盘缓存:将数据存储在磁盘上,访问速度较慢,但容量较大,且应用退出后数据不会丢失。
- 网络缓存:将数据存储在服务器或中间代理服务器上,减少客户端与服务器之间的直接通信。
2.3 缓存的基本原理
缓存的基本原理是利用数据的局部性原理,即程序在一段时间内访问的数据往往集中在一小部分数据上。通过将这部分数据存储在缓存中,可以显著提高数据的访问速度。
缓存的工作流程通常包括以下几个步骤:
- 应用程序发起数据请求。
- 缓存系统检查请求的数据是否存在于缓存中。
- 如果数据存在于缓存中(缓存命中),则直接从缓存中返回数据。
- 如果数据不存在于缓存中(缓存未命中),则从数据源(如网络或数据库)获取数据,并将数据存入缓存中,以便下次访问。
2.4 缓存的优缺点
缓存的优点:
- 提高数据访问速度,减少响应时间。
- 降低服务器负载,提高系统吞吐量。
- 节省用户流量,特别是在移动网络环境下。
- 提高应用的可用性,即使在网络不稳定的情况下也能正常工作。
缓存的缺点:
- 占用额外的存储空间,无论是内存还是磁盘。
- 缓存数据可能与原始数据不一致,需要合理的缓存更新策略。
- 实现和维护缓存系统需要额外的开发工作和复杂度。
三、Volley 缓存模块概述
3.1 Volley 框架简介
Android Volley 是 Google 开发的一个轻量级网络通信框架,专为 Android 应用设计,用于快速处理网络请求和响应。Volley 提供了简单易用的 API,可以方便地进行 HTTP 请求,同时还具备高效的缓存机制、请求优先级处理、网络请求队列管理等功能。
3.2 Volley 缓存模块的作用
Volley 缓存模块是 Volley 框架的重要组成部分,主要用于缓存网络请求的响应数据,避免重复的网络请求,提高应用的性能和用户体验。通过合理配置缓存策略,开发者可以控制哪些数据需要缓存、缓存的有效期、缓存的存储位置等。
3.3 Volley 缓存模块的架构
Volley 缓存模块的架构主要由以下几个部分组成:
- Cache 接口:定义了缓存操作的基本接口,包括读取缓存、写入缓存、失效缓存等方法。
- DiskBasedCache 类:Volley 提供的默认磁盘缓存实现,基于文件系统存储缓存数据。
- CacheHeader 类:用于存储缓存条目的元数据,如缓存键、ETag、服务器时间、过期时间等。
- CacheDispatcher 类:缓存调度器,负责从缓存队列中取出请求并处理缓存命中或未命中的情况。
- NetworkDispatcher 类:网络调度器,负责处理缓存未命中的请求,从网络获取数据。
3.4 Volley 缓存模块的工作流程
Volley 缓存模块的工作流程大致如下:
- 应用程序发起网络请求,将请求添加到 RequestQueue 中。
- CacheDispatcher 从缓存队列中取出请求,检查该请求是否有缓存数据。
- 如果缓存命中且缓存数据有效,则直接从缓存中获取数据并返回给应用程序。
- 如果缓存未命中或缓存数据已过期,则将请求转发给 NetworkDispatcher 处理。
- NetworkDispatcher 从网络获取数据,并将数据返回给 CacheDispatcher。
- CacheDispatcher 将从网络获取的数据存入缓存中,并将数据返回给应用程序。
四、Cache 接口分析
4.1 Cache 接口定义
Cache 接口是 Volley 缓存模块的核心接口,定义了缓存操作的基本方法。所有缓存实现类都必须实现这个接口。
下面是 Cache 接口的源码:
/**
* 缓存接口,定义了缓存操作的基本方法
*/
public interface Cache {
/**
* 从缓存中获取指定键的数据条目
*
* @param key 缓存键,通常是请求的 URL
* @return 缓存条目,如果不存在则返回 null
*/
public Entry get(String key);
/**
* 将数据条目放入缓存中
*
* @param key 缓存键
* @param entry 缓存条目
*/
public void put(String key, Entry entry);
/**
* 使指定键的缓存条目失效
*
* @param key 缓存键
* @param fullExpire 如果为 true,表示完全失效,下次请求需要从网络获取
* 如果为 false,表示需要刷新,下次请求会先使用缓存数据,同时从网络获取最新数据
*/
public void invalidate(String key, boolean fullExpire);
/**
* 从缓存中移除指定键的条目
*
* @param key 缓存键
*/
public void remove(String key);
/**
* 清空整个缓存
*/
public void clear();
/**
* 缓存条目类,包含了缓存数据及其元数据
*/
public static class Entry {
/** 缓存数据的字节数组 */
public byte[] data;
/** 从服务器获取的 ETag,用于判断数据是否有更新 */
public String etag;
/** 服务器响应的时间戳(从 epoch 开始的毫秒数) */
public long serverDate;
/** 数据的最后修改时间(从 epoch 开始的毫秒数) */
public long lastModified;
/** 缓存的软过期时间(从 epoch 开始的毫秒数) */
public long softTtl;
/** 缓存的硬过期时间(从 epoch 开始的毫秒数) */
public long ttl;
/** 响应头信息 */
public Map<String, String> responseHeaders = Collections.emptyMap();
/**
* 判断缓存是否已过期
*
* @return 如果当前时间超过了硬过期时间,则返回 true,否则返回 false
*/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* 判断缓存是否需要刷新
*
* @return 如果当前时间超过了软过期时间,但还未超过硬过期时间,则返回 true,否则返回 false
*/
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
}
4.2 Cache.Entry 类分析
Cache.Entry 类是 Cache 接口的静态内部类,用于存储缓存条目的数据和元数据。它包含了以下重要字段:
- data:缓存的实际数据,以字节数组形式存储。
- etag:从服务器获取的 ETag,用于标识资源的版本,当服务器资源发生变化时,ETag 也会相应变化。
- serverDate:服务器响应的时间戳,用于计算缓存的过期时间。
- lastModified:数据的最后修改时间,同样用于缓存验证。
- softTtl:软过期时间,当超过这个时间但未超过硬过期时间时,缓存数据仍可使用,但需要从网络刷新。
- ttl:硬过期时间,当超过这个时间时,缓存数据被认为已过期,必须从网络重新获取。
- responseHeaders:服务器响应的头信息,包含了各种有用的元数据。
Cache.Entry 类还提供了两个重要方法:
- isExpired():判断缓存是否已过期,即当前时间是否超过了硬过期时间。
- refreshNeeded():判断缓存是否需要刷新,即当前时间是否超过了软过期时间但未超过硬过期时间。
这两个方法在缓存处理流程中起着关键作用,决定了是使用缓存数据还是从网络获取数据。
五、DiskBasedCache 类分析
5.1 DiskBasedCache 类概述
DiskBasedCache 是 Volley 框架提供的默认磁盘缓存实现类,它实现了 Cache 接口,将缓存数据存储在文件系统中。这个类使用 LRU(最近最少使用)算法来管理缓存空间,当缓存空间不足时,会优先删除最久未使用的缓存条目。
5.2 DiskBasedCache 类的成员变量
下面是 DiskBasedCache 类的主要成员变量及其作用:
/**
* 基于磁盘的缓存实现,使用 LRU 算法管理缓存条目
*/
public class DiskBasedCache implements Cache {
/** 缓存目录 */
private final File mRootDirectory;
/** 缓存的最大大小(字节) */
private final int mMaxCacheSizeInBytes;
/** 当前缓存的总大小(字节) */
private long mTotalSize = 0;
/** 缓存条目映射,键为缓存键,值为缓存头部信息 */
private final HashMap<String, CacheHeader> mEntries = new HashMap<String, CacheHeader>();
/** 用于生成文件名的哈希函数 */
private final MessageDigest mDigest;
/** 缓存条目被删除时的监听器 */
private CacheEvictionListener mCacheEvictionListener;
/** 默认的缓存大小(5MB) */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
/** 用于清理缓存时的最大尝试次数 */
private static final int MAX_REMOVAL_ATTEMPTS = 4;
/** 文件名的最大长度 */
private static final int MAX_FILENAME_LENGTH = 128;
// 其他常量和成员变量...
}
5.3 DiskBasedCache 类的构造函数
DiskBasedCache 类提供了多个构造函数,允许开发者指定缓存目录和缓存大小:
/**
* 创建一个新的 DiskBasedCache 实例
*
* @param rootDirectory 缓存目录
* @param maxCacheSizeInBytes 缓存的最大大小(字节)
*/
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
// 初始化 SHA-1 哈希算法,用于生成文件名
try {
mDigest = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
// 这种情况不应该发生,因为 SHA-1 是标准算法
throw new RuntimeException("Could not initialize MessageDigest", e);
}
}
/**
* 创建一个新的 DiskBasedCache 实例,使用默认的缓存大小
*
* @param rootDirectory 缓存目录
*/
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
5.4 DiskBasedCache 类的初始化方法
initialize() 方法用于初始化缓存系统,主要是从磁盘加载已有的缓存条目:
/**
* 初始化缓存,从磁盘加载所有缓存条目
*/
@Override
public synchronized void initialize() {
// 如果缓存目录不存在,则创建它
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
// 获取缓存目录下的所有文件
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
// 遍历所有文件,加载有效的缓存条目
for (File file : files) {
FileInputStream fis = null;
try {
// 跳过目录和隐藏文件
if (file.isDirectory() || file.isHidden() || !file.canRead()) {
continue;
}
// 打开文件输入流
fis = new FileInputStream(file);
// 从文件中读取缓存头部信息
CacheHeader entry = CacheHeader.readHeader(fis);
// 设置条目的大小为文件的实际大小
entry.size = file.length();
// 将缓存条目添加到内存映射中
putEntry(entry.key, entry);
} catch (IOException e) {
// 如果读取文件时出错,删除该文件
if (file != null) {
file.delete();
}
} finally {
// 关闭文件输入流
closeQuietly(fis);
}
}
}
5.5 CacheHeader 类分析
CacheHeader 类是 DiskBasedCache 的内部类,用于存储缓存条目的元数据,并且负责将这些元数据序列化到文件和从文件反序列化:
/**
* 缓存头部信息,包含了缓存条目的元数据
*/
static class CacheHeader {
/** 缓存键 */
public String key;
/** ETag 用于验证缓存 */
public String etag;
/** 服务器响应的时间戳 */
public long serverDate;
/** 数据的最后修改时间 */
public long lastModified;
/** 软过期时间 */
public long softTtl;
/** 硬过期时间 */
public long ttl;
/** 响应头信息 */
public Map<String, String> responseHeaders;
/** 缓存条目的大小 */
public long size;
/**
* 构造函数,用于创建新的缓存头部信息
*/
private CacheHeader(String key, Entry entry) {
this.key = key;
this.etag = entry.etag;
this.serverDate = entry.serverDate;
this.lastModified = entry.lastModified;
this.softTtl = entry.softTtl;
this.ttl = entry.ttl;
this.responseHeaders = entry.responseHeaders;
this.size = entry.data.length;
}
/**
* 从输入流中读取缓存头部信息
*
* @param is 输入流
* @return 解析后的缓存头部信息
* @throws IOException 如果读取过程中发生错误
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
try {
// 使用 DataInputStream 读取二进制数据
DataInputStream dis = new DataInputStream(new BufferedInputStream(is, 16));
// 读取缓存键
String key = dis.readUTF();
// 读取 ETag
String etag = dis.readUTF();
if (etag.equals("")) {
etag = null;
}
// 读取服务器时间
long serverDate = dis.readLong();
// 读取最后修改时间
long lastModified = dis.readLong();
// 读取软过期时间
long softTtl = dis.readLong();
// 读取硬过期时间
long ttl = dis.readLong();
// 读取响应头的数量
int headerCount = dis.readInt();
// 读取响应头信息
Map<String, String> headers = new HashMap<String, String>(headerCount);
for (int i = 0; i < headerCount; i++) {
String name = dis.readUTF();
String value = dis.readUTF();
headers.put(name, value);
}
// 创建并返回缓存头部对象
CacheHeader entry = new CacheHeader();
entry.key = key;
entry.etag = etag;
entry.serverDate = serverDate;
entry.lastModified = lastModified;
entry.softTtl = softTtl;
entry.ttl = ttl;
entry.responseHeaders = headers;
return entry;
} catch (EOFException e) {
// 如果读取到文件末尾,抛出异常
throw new IOException(e);
}
}
/**
* 将缓存头部信息写入输出流
*
* @param os 输出流
* @throws IOException 如果写入过程中发生错误
*/
public void writeHeader(OutputStream os) throws IOException {
// 使用 DataOutputStream 写入二进制数据
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(os, 16));
// 写入缓存键
dos.writeUTF(key);
// 写入 ETag
dos.writeUTF(etag == null ? "" : etag);
// 写入服务器时间
dos.writeLong(serverDate);
// 写入最后修改时间
dos.writeLong(lastModified);
// 写入软过期时间
dos.writeLong(softTtl);
// 写入硬过期时间
dos.writeLong(ttl);
// 写入响应头的数量
dos.writeInt(responseHeaders.size());
// 写入响应头信息
for (Map.Entry<String, String> header : responseHeaders.entrySet()) {
dos.writeUTF(header.getKey());
dos.writeUTF(header.getValue());
}
// 刷新输出流
dos.flush();
}
/**
* 从缓存头部信息创建完整的缓存条目
*
* @param data 缓存的实际数据
* @return 完整的缓存条目
*/
public Entry toCacheEntry(byte[] data) {
Entry entry = new Entry();
entry.data = data;
entry.etag = etag;
entry.serverDate = serverDate;
entry.lastModified = lastModified;
entry.softTtl = softTtl;
entry.ttl = ttl;
entry.responseHeaders = responseHeaders;
return entry;
}
// 私有构造函数,用于内部创建对象
private CacheHeader() {}
}
5.6 DiskBasedCache 类的 get() 方法
get() 方法用于从缓存中获取指定键的数据:
/**
* 从缓存中获取指定键的缓存条目
*
* @param key 缓存键
* @return 缓存条目,如果不存在或已损坏则返回 null
*/
@Override
public synchronized Entry get(String key) {
// 从内存映射中获取缓存头部信息
CacheHeader entry = mEntries.get(key);
// 如果缓存头部信息不存在,返回 null
if (entry == null) {
return null;
}
// 获取缓存文件
File file = getFileForKey(key);
// 检查文件是否存在
if (!file.exists()) {
// 文件不存在,可能已被手动删除,从内存映射中移除该条目
mEntries.remove(key);
return null;
}
// 尝试读取缓存文件内容
FileInputStream fis = null;
try {
// 打开文件输入流
fis = new FileInputStream(file);
// 创建计数输入流,用于跟踪已读取的字节数
CountingInputStream cis = new CountingInputStream(fis);
// 读取并丢弃缓存头部信息(已经在内存中)
CacheHeader.readHeader(cis);
// 计算剩余字节数(即实际的缓存数据)
int dataLength = (int) (file.length() - cis.bytesRead);
// 从输入流中读取缓存数据
byte[] data = streamToBytes(cis, dataLength);
// 将缓存头部信息转换为完整的缓存条目
return entry.toCacheEntry(data);
} catch (IOException e) {
// 读取文件时出错,删除该文件并从内存映射中移除该条目
remove(key);
return null;
} finally {
// 关闭文件输入流
closeQuietly(fis);
}
}
5.7 DiskBasedCache 类的 put() 方法
put() 方法用于将数据存入缓存:
/**
* 将数据存入缓存
*
* @param key 缓存键
* @param entry 缓存条目
*/
@Override
public synchronized void put(String key, Entry entry) {
// 如果缓存目录不存在,尝试创建它
pruneIfNeeded(entry.data.length);
// 获取缓存文件
File file = getFileForKey(key);
// 创建临时文件,用于原子性写入
FileOutputStream fos = null;
try {
// 创建缓存头部信息
CacheHeader header = new CacheHeader(key, entry);
// 创建临时文件
File tmpFile = new File(file.getAbsolutePath() + ".tmp");
// 打开临时文件输出流
fos = new FileOutputStream(tmpFile);
// 将缓存头部信息写入临时文件
header.writeHeader(fos);
// 将缓存数据写入临时文件
fos.write(entry.data);
// 刷新输出流
fos.flush();
// 原子性地将临时文件重命名为最终的缓存文件
if (!tmpFile.renameTo(file)) {
throw new IOException("Rename failed!");
}
// 更新内存中的缓存条目映射
putEntry(key, header);
return;
} catch (IOException e) {
// 写入文件时出错
} finally {
// 关闭文件输出流
closeQuietly(fos);
}
// 如果写入失败,删除临时文件
File tmpFile = new File(file.getAbsolutePath() + ".tmp");
if (tmpFile.exists()) {
tmpFile.delete();
}
// 删除缓存文件
file.delete();
}
5.8 DiskBasedCache 类的 invalidate() 方法
invalidate() 方法用于使指定键的缓存条目失效:
/**
* 使指定键的缓存条目失效
*
* @param key 缓存键
* @param fullExpire 如果为 true,表示完全失效,下次请求需要从网络获取
* 如果为 false,表示需要刷新,下次请求会先使用缓存数据,同时从网络获取最新数据
*/
@Override
public synchronized void invalidate(String key, boolean fullExpire) {
// 从内存映射中获取缓存头部信息
Entry entry = get(key);
// 如果缓存条目存在
if (entry != null) {
// 设置软过期时间为 0,确保下次请求会检查是否需要刷新
entry.softTtl = 0;
// 如果需要完全失效,设置硬过期时间为过去的时间
if (fullExpire) {
entry.ttl = 0;
}
// 将修改后的缓存条目重新存入缓存
put(key, entry);
}
}
5.9 DiskBasedCache 类的 remove() 方法
remove() 方法用于从缓存中移除指定键的条目:
/**
* 从缓存中移除指定键的条目
*
* @param key 缓存键
*/
@Override
public synchronized void remove(String key) {
// 从内存映射中获取缓存头部信息
CacheHeader entry = mEntries.get(key);
// 如果缓存头部信息存在
if (entry != null) {
// 从内存映射中移除该条目
mEntries.remove(key);
// 减少当前缓存总大小
mTotalSize -= entry.size;
// 获取缓存文件
File file = getFileForKey(key);
// 删除缓存文件
boolean deleted = file.delete();
// 如果删除失败,记录日志
if (!deleted) {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
key, getFilenameForKey(key));
}
}
}
5.10 DiskBasedCache 类的 clear() 方法
clear() 方法用于清空整个缓存:
/**
* 清空整个缓存
*/
@Override
public synchronized void clear() {
// 获取缓存目录下的所有文件
File[] files = mRootDirectory.listFiles();
// 如果缓存目录存在且不为空
if (files != null) {
// 遍历所有文件并删除
for (File file : files) {
file.delete();
}
}
// 重置缓存总大小
mTotalSize = 0;
// 清空内存中的缓存条目映射
mEntries.clear();
// 记录日志
VolleyLog.d("Cache cleared.");
}
5.11 DiskBasedCache 类的辅助方法
DiskBasedCache 类还包含了一些辅助方法,用于处理缓存文件和管理缓存空间:
/**
* 根据缓存键生成文件名
*
* @param key 缓存键
* @return 生成的文件名
*/
private String getFilenameForKey(String key) {
// 使用 SHA-1 哈希算法计算缓存键的哈希值
byte[] hashBytes = mDigest.digest(key.getBytes());
// 将哈希值转换为十六进制字符串
String hashStr = bytesToHexString(hashBytes);
// 使用缓存键的原始值和哈希值组合作为文件名,确保文件名在合理长度内
String filename = key.replaceAll("[^a-zA-Z0-9_-]", "_");
// 如果文件名太长,截断并添加哈希值
if (filename.length() > MAX_FILENAME_LENGTH) {
filename = filename.substring(0, MAX_FILENAME_LENGTH) + "_" + hashStr;
}
return filename;
}
/**
* 根据缓存键获取对应的缓存文件
*
* @param key 缓存键
* @return 缓存文件
*/
private File getFileForKey(String key) {
return new File(mRootDirectory, getFilenameForKey(key));
}
/**
* 如果需要,清理缓存以腾出足够的空间
*
* @param neededSpace 需要的空间大小(字节)
*/
private void pruneIfNeeded(int neededSpace) {
// 如果缓存目录不存在,创建它
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
// 如果所需空间超过最大缓存大小,直接返回,无法腾出足够空间
if (neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.w("Attempted to put large object in cache. Size: %d, max: %d",
neededSpace, mMaxCacheSizeInBytes);
return;
}
// 如果当前缓存大小加上所需空间超过最大缓存大小,需要清理缓存
while (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
// 如果缓存已空但仍无法满足需求,返回
if (mEntries.isEmpty()) {
return;
}
// 查找最久未使用的缓存条目
long oldestEntry = Long.MAX_VALUE;
CacheHeader mostLikelyToRemove = null;
// 遍历所有缓存条目,找到最久未使用的条目
for (CacheHeader entry : mEntries.values()) {
if (entry.serverDate < oldestEntry) {
oldestEntry = entry.serverDate;
mostLikelyToRemove = entry;
}
}
// 如果找到要移除的条目
if (mostLikelyToRemove != null) {
// 移除该条目
remove(mostLikelyToRemove.key);
// 如果有缓存驱逐监听器,通知它
if (mCacheEvictionListener != null) {
mCacheEvictionListener.onCacheEviction(mostLikelyToRemove.key,
mostLikelyToRemove.toCacheEntry(new byte[0]));
}
}
}
}
六、CacheDispatcher 类分析
6.1 CacheDispatcher 类概述
CacheDispatcher 是 Volley 框架中负责处理缓存请求的调度器。它从缓存请求队列中取出请求,检查缓存中是否有对应的条目,并根据缓存状态决定是直接返回缓存数据还是将请求转发给网络调度器处理。
6.2 CacheDispatcher 类的成员变量
下面是 CacheDispatcher 类的主要成员变量及其作用:
/**
* 缓存调度器,负责从缓存队列中处理请求
*/
public class CacheDispatcher extends Thread {
/** 缓存队列,用于存放需要检查缓存的请求 */
private final BlockingQueue<Request<?>> mCacheQueue;
/** 网络队列,用于存放需要从网络获取数据的请求 */
private final BlockingQueue<Request<?>> mNetworkQueue;
/** 缓存实现 */
private final Cache mCache;
/** 响应分发器,用于将响应分发到主线程 */
private final ResponseDelivery mDelivery;
/** 线程是否应该继续运行的标志 */
private volatile boolean mQuit = false;
// 其他成员变量...
}
6.3 CacheDispatcher 类的构造函数
CacheDispatcher 类的构造函数接收缓存队列、网络队列、缓存实现和响应分发器作为参数:
/**
* 创建一个新的缓存调度器
*
* @param cacheQueue 缓存请求队列
* @param networkQueue 网络请求队列
* @param cache 缓存实现
* @param delivery 响应分发器
*/
public CacheDispatcher(
BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
6.4 CacheDispatcher 类的 run() 方法
run() 方法是 CacheDispatcher 类的核心方法,它在单独的线程中运行,不断从缓存队列中取出请求并处理:
/**
* 线程的主运行方法
*/
@Override
public void run() {
// 设置线程优先级为后台线程
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 初始化缓存
mCache.initialize();
// 循环处理缓存队列中的请求
while (true) {
try {
// 从缓存队列中取出一个请求(阻塞操作,队列为空时会等待)
final Request<?> request = mCacheQueue.take();
// 标记请求开始处理
request.addMarker("cache-queue-take");
// 如果请求已被取消,跳过处理
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// 尝试从缓存中获取数据
Cache.Entry entry = mCache.get(request.getCacheKey());
// 如果缓存中没有该请求的数据,将请求添加到网络队列
if (entry == null) {
request.addMarker("cache-miss");
mNetworkQueue.put(request);
continue;
}
// 如果缓存数据已过期,将请求添加到网络队列,但先返回缓存数据
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// 缓存命中且数据有效,解析缓存数据并返回
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
// 检查缓存数据是否需要刷新
if (entry.refreshNeeded()) {
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// 标记响应为中间结果
response.intermediate = true;
// 将请求发送到网络进行刷新,同时分发缓存响应
final Request<?> finalRequest = request;
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
// 将请求添加到网络队列进行刷新
mNetworkQueue.put(finalRequest);
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
}
}
});
} else {
// 缓存数据不需要刷新,直接分发响应
mDelivery.postResponse(request, response);
}
} catch (InterruptedException e) {
// 如果线程被中断,检查是否需要退出
if (mQuit) {
Thread.currentThread().interrupt();
return;
}
VolleyLog.e("Ignoring spurious interrupt of CacheDispatcher thread; " +
"use quit() to terminate it");
}
}
}
6.5 CacheDispatcher 类的 quit() 方法
quit() 方法用于停止缓存调度器的运行:
/**
* 停止缓存调度器的运行
*/
public void quit() {
mQuit = true;
interrupt();
}
七、缓存策略分析
7.1 缓存策略概述
缓存策略决定了何时使用缓存数据、何时从网络获取数据以及如何验证缓存数据的有效性。Volley 框架提供了灵活的缓存策略机制,开发者可以通过设置请求的缓存头信息和处理服务器响应来实现不同的缓存策略。
7.2 缓存控制头信息
HTTP 协议定义了多个用于控制缓存的头信息,Volley 缓存模块主要使用以下几个头信息:
- Cache-Control:指定缓存机制的行为,如 max-age、no-cache、no-store 等。
- Expires:指定缓存数据的过期时间,是一个绝对时间。
- ETag:资源的实体标签,用于验证缓存数据是否与服务器上的资源一致。
- Last-Modified:资源的最后修改时间,同样用于验证缓存数据。
7.3 Volley 中的缓存策略实现
在 Volley 中,缓存策略主要通过 Cache.Entry 类的字段和方法实现:
public static class Entry {
// 其他字段和方法...
/**
* 判断缓存是否已过期
*
* @return 如果当前时间超过了硬过期时间(ttl),返回 true,否则返回 false
*/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* 判断缓存是否需要刷新
*
* @return 如果当前时间超过了软过期时间(softTtl)但未超过硬过期时间,返回 true,否则返回 false
*/
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
7.4 缓存验证机制
当缓存数据的软过期时间已过但硬过期时间未过时,Volley 会执行缓存验证,向服务器发送请求以确认缓存数据是否仍然有效。验证请求通常会包含以下头信息:
- If-None-Match:包含缓存的 ETag,用于让服务器比较资源的当前 ETag。
- If-Modified-Since:包含缓存的 Last-Modified 时间,用于让服务器比较资源的最后修改时间。
如果服务器返回 304 (Not Modified) 状态码,表示缓存数据仍然有效,可以继续使用。
7.5 自定义缓存策略
开发者可以通过继承 Request 类并重写 getCacheKey()、getHeaders() 等方法来实现自定义的缓存策略:
public class CustomCacheRequest extends Request<String> {
private final Response.Listener<String> mListener;
public CustomCacheRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
// 自定义请求头,设置缓存控制
Map<String, String> headers = super.getHeaders();
if (headers == null || headers.isEmpty()) {
headers = new HashMap<>();
}
headers.put("Cache-Control", "max-age=3600"); // 缓存1小时
return headers;
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
// 自定义缓存条目
Cache.Entry cacheEntry = HttpHeaderParser.parseCacheHeaders(response);
// 返回解析结果和缓存条目
return Response.success(jsonString, cacheEntry);
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
}
}
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
}
八、LRU 算法在缓存中的应用
8.1 LRU 算法概述
LRU (Least Recently Used) 算法是一种缓存淘汰策略,当缓存空间不足时,会优先淘汰最久未使用的数据。这种算法基于"最近使用的数据很可能在未来也会被频繁使用"的假设,能够有效提高缓存命中率。
8.2 DiskBasedCache 中的 LRU 实现
在 Volley 的 DiskBasedCache 类中,LRU 算法主要通过以下方式实现:
- 使用
HashMap<String, CacheHeader>存储缓存条目,便于快速查找。 - 每个 CacheHeader 包含
serverDate字段,表示该缓存条目最后一次被服务器响应的时间。 - 当需要淘汰缓存条目时,遍历所有条目,找到
serverDate最小(即最久未使用)的条目进行删除。
下面是 DiskBasedCache 中实现 LRU 淘汰的核心代码:
/**
* 如果需要,清理缓存以腾出足够的空间
*
* @param neededSpace 需要的空间大小(字节)
*/
private void pruneIfNeeded(int neededSpace) {
// 如果所需空间超过最大缓存大小,直接返回
if (neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.w("Attempted to put large object in cache. Size: %d, max: %d",
neededSpace, mMaxCacheSizeInBytes);
return;
}
// 如果当前缓存大小加上所需空间超过最大缓存大小,需要清理缓存
while (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
// 如果缓存已空但仍无法满足需求,返回
if (mEntries.isEmpty()) {
return;
}
// 查找最久未使用的缓存条目
long oldestEntry = Long.MAX_VALUE;
CacheHeader mostLikelyToRemove = null;
// 遍历所有缓存条目,找到 serverDate 最小的条目(即最久未使用的条目)
for (CacheHeader entry : mEntries.values()) {
if (entry.serverDate < oldestEntry) {
oldestEntry = entry.serverDate;
mostLikelyToRemove = entry;
}
}
// 如果找到要移除的条目
if (mostLikelyToRemove != null) {
// 移除该条目
remove(mostLikelyToRemove.key);
// 如果有缓存驱逐监听器,通知它
if (mCacheEvictionListener != null) {
mCacheEvictionListener.onCacheEviction(mostLikelyToRemove.key,
mostLikelyToRemove.toCacheEntry(new byte[0]));
}
}
}
}
8.3 LRU 算法的优缺点
LRU 算法的优点:
- 实现简单,逻辑清晰。
- 能够较好地反映程序的局部性原理,提高缓存命中率。
LRU 算法的缺点:
- 需要维护缓存条目的访问顺序,可能带来额外的开销。
- 在某些特定的访问模式下,可能会导致缓存性能下降。
8.4 优化 LRU 算法的方法
针对 LRU 算法的缺点,可以采用以下优化方法:
- LRU-K 算法:记录每个缓存条目最近 K 次的访问历史,淘汰策略更加复杂但更精确。
- 2Q 算法:结合 FIFO 和 LRU 的优点,使用两个队列来管理缓存条目。
- 时钟算法(Clock Algorithm):使用环形链表和引用位来实现近似 LRU 的功能,减少维护开销。
九、缓存性能优化
9.1 缓存大小配置优化
合理配置缓存大小对性能至关重要。如果缓存太小,会导致频繁的缓存未命中和网络请求;如果缓存太大,则会占用过多的磁盘空间,甚至影响应用的整体性能。
在 Volley 中,可以通过以下方式配置缓存大小:
// 创建 DiskBasedCache 时指定缓存大小
File cacheDir = new File(context.getCacheDir(), "volley");
int cacheSize = 10 * 1024 * 1024; // 10MB
Cache cache = new DiskBasedCache(cacheDir, cacheSize);
// 使用自定义缓存创建 RequestQueue
RequestQueue queue = new RequestQueue(cache, new BasicNetwork(new HurlStack()));
queue.start();
9.2 缓存文件管理优化
DiskBasedCache 使用文件系统来存储缓存数据,因此文件的组织和管理对性能有很大影响。以下是一些优化建议:
-
使用哈希值作为文件名:Volley 已经采用了这种方式,通过对 URL 进行哈希计算生成文件名,避免了特殊字符和过长文件名的问题。
-
批量删除文件:在清理缓存时,批量删除文件比逐个删除更高效。
-
文件预分配:在写入大文件时,可以考虑预先分配空间,减少文件碎片。
9.3 缓存读取和写入优化
缓存的读取和写入操作是性能关键,可以通过以下方式优化: