深入解析Android Volley缓存配置:从源码到实战的全面指南
一、引言
在移动应用开发中,网络请求是不可或缺的一部分。然而,频繁的网络请求不仅会消耗用户的流量,还会影响应用的响应速度和性能。为了解决这些问题,缓存机制应运而生。Android Volley作为一款强大的网络请求库,提供了灵活且高效的缓存策略,能够显著提升应用的性能和用户体验。
本文将深入剖析Android Volley的缓存配置机制,从源码级别详细分析其实现原理、配置选项和各种缓存策略的应用场景。通过本文的学习,你将全面掌握Volley缓存配置的核心原理,学会如何根据不同的应用场景选择合适的缓存策略,以及如何自定义缓存配置来满足特定的需求。
二、Volley缓存配置概述
2.1 缓存配置的基本概念
Volley的缓存配置是指通过设置各种参数和选项,来控制Volley如何存储、管理和使用缓存数据的过程。合理的缓存配置可以有效减少网络请求,提高应用的响应速度,降低用户流量消耗。
Volley的缓存配置主要涉及以下几个方面:
- 缓存存储位置和大小
- 缓存过期策略
- 缓存键生成规则
- 缓存优先级设置
- 条件请求配置
- 自定义缓存实现
2.2 缓存配置的重要性
合理的缓存配置对于移动应用的性能优化至关重要:
- 减少网络流量:通过缓存可以避免重复请求相同的数据,从而减少网络流量消耗。
- 提高响应速度:直接从缓存中获取数据比从网络获取数据要快得多,可以显著提高应用的响应速度。
- 支持离线模式:即使在没有网络连接的情况下,应用也可以从缓存中获取数据,提供基本的功能支持。
- 降低服务器负载:减少对服务器的请求次数,降低服务器的负载压力。
三、Volley缓存配置的核心组件
3.1 Cache接口
Cache接口是Volley缓存机制的核心接口,定义了缓存操作的基本方法:
/**
* 缓存接口,定义了缓存操作的基本方法
*/
public interface Cache {
/**
* 从缓存中获取指定键的数据
* @param key 缓存键
* @return 缓存条目,如果不存在则返回null
*/
public Entry get(String key);
/**
* 将数据存入缓存
* @param key 缓存键
* @param entry 缓存条目
*/
public void put(String key, Entry entry);
/**
* 刷新指定缓存条目的状态
* @param key 缓存键
* @param fullExpire 如果为true,表示完全过期,需要重新请求
*/
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;
/** 服务器响应的日期(毫秒) */
public long serverDate;
/** 缓存的软过期时间(毫秒) */
public long softTtl;
/** 缓存的硬过期时间(毫秒) */
public long ttl;
/** 响应头信息 */
public Map<String, String> responseHeaders;
/**
* 判断缓存是否已硬过期
* @return 如果已硬过期返回true,否则返回false
*/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* 判断缓存是否需要刷新
* @return 如果需要刷新返回true,否则返回false
*/
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
}
从上面的源码可以看出,Cache接口定义了五个基本方法:get、put、invalidate、remove和clear,分别用于获取缓存、存入缓存、刷新缓存、删除缓存和清空缓存。同时,Cache接口内部定义了一个静态类Entry,用于表示缓存中的一个条目,包含了缓存数据的元信息。
3.2 DiskBasedCache类
DiskBasedCache是Volley默认的缓存实现,它基于磁盘存储,将缓存数据存储在应用的文件系统中。
/**
* 基于磁盘的缓存实现
*/
public class DiskBasedCache implements Cache {
/** 默认的缓存目录 */
private static final String DEFAULT_CACHE_DIR = "volley";
/** 缓存目录 */
private final File mRootDirectory;
/** 缓存的最大大小(字节) */
private final int mMaxCacheSizeInBytes;
/** 当前缓存的大小(字节) */
private long mTotalSize = 0;
/** 缓存条目映射表,键为缓存键,值为缓存文件 */
private final Map<String, CacheHeader> mEntries = new LinkedHashMap<String, CacheHeader>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Entry<String, CacheHeader> eldest) {
return mTotalSize > mMaxCacheSizeInBytes;
}
};
// 其他成员变量和方法...
}
DiskBasedCache类的构造函数允许我们配置缓存目录和缓存大小:
/**
* 创建一个新的磁盘缓存
* @param rootDirectory 缓存目录
* @param maxCacheSizeInBytes 缓存的最大大小(字节)
*/
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
/**
* 创建一个新的磁盘缓存,使用默认的缓存大小(5MB)
* @param rootDirectory 缓存目录
*/
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
/**
* 创建一个新的磁盘缓存,使用默认的缓存目录和大小
*/
public DiskBasedCache() {
this(new File(Volley.getDefaultCacheDir(Volley.getContext()), DEFAULT_CACHE_DIR));
}
3.3 Request类
Request类是Volley中所有请求的基类,它包含了与缓存相关的配置选项:
/**
* 请求基类
*/
public abstract class Request<T> implements Comparable<Request<T>> {
/** 是否应该缓存此请求的响应 */
private boolean mShouldCache = true;
/** 请求的缓存优先级 */
private Priority mPriority = Priority.NORMAL;
// 其他成员变量...
/**
* 设置此请求是否应该被缓存
* @param shouldCache 如果为true,则缓存此请求的响应
*/
public final Request<T> setShouldCache(boolean shouldCache) {
mShouldCache = shouldCache;
return this;
}
/**
* 返回此请求是否应该被缓存
*/
public final boolean shouldCache() {
return mShouldCache;
}
/**
* 设置请求的优先级
*/
public final Request<T> setPriority(Priority priority) {
mPriority = priority;
return this;
}
/**
* 返回请求的优先级
*/
public Priority getPriority() {
return mPriority;
}
/**
* 返回此请求的缓存键
*/
public String getCacheKey() {
// 默认使用请求的URL作为缓存键
return getUrl();
}
}
3.4 HttpHeaderParser类
HttpHeaderParser类是一个工具类,用于解析HTTP响应头中的缓存相关信息:
/**
* HTTP头解析工具类
*/
public class HttpHeaderParser {
/**
* 解析HTTP响应头,提取缓存相关信息
* @param response 网络响应
* @return 缓存条目
*/
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
long now = System.currentTimeMillis();
// 获取响应头
Map<String, String> headers = response.headers;
// 初始化服务器日期、过期时间、软过期时间等
long serverDate = 0;
long serverExpires = 0;
long softExpire = 0;
long maxAge = 0;
boolean hasCacheControl = false;
boolean mustRevalidate = false;
// 解析Date头
String serverEtag;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = parseDateAsEpoch(headerValue);
}
// 解析Cache-Control头
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (String token : tokens) {
token = token.trim();
if (token.equals("no-cache") || token.equals("no-store")) {
// 如果是no-cache或no-store,直接返回null,表示不缓存
return null;
} else if (token.startsWith("max-age=")) {
try {
// 解析max-age值
maxAge = Long.parseLong(token.substring("max-age=".length()));
} catch (NumberFormatException e) {
// 忽略解析错误
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
// 标记必须重新验证
mustRevalidate = true;
}
}
}
// 解析Expires头
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
// 获取ETag
serverEtag = headers.get("ETag");
// 计算缓存时间
// Cache-Control头优先级高于Expires头
if (hasCacheControl) {
// 如果有Cache-Control头,使用它来计算缓存时间
softExpire = now + maxAge * 1000;
// 如果是must-revalidate,软过期时间和硬过期时间相同
if (mustRevalidate) {
softExpire = now + maxAge * 1000;
}
} else if (serverDate > 0 && serverExpires >= serverDate) {
// 如果没有Cache-Control头,但有Expires头,使用它来计算缓存时间
softExpire = now + (serverExpires - serverDate);
}
// 创建缓存条目
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.serverDate = serverDate;
entry.softTtl = softExpire;
entry.ttl = softExpire; // 默认情况下,硬过期时间和软过期时间相同
entry.responseHeaders = headers;
entry.hasCacheControl = hasCacheControl;
entry.mustRevalidate = mustRevalidate;
return entry;
}
}
四、缓存目录与大小配置
4.1 配置缓存目录
Volley默认的缓存目录是应用内部存储的"volley"文件夹。我们可以通过DiskBasedCache的构造函数来指定自定义的缓存目录:
// 获取应用的缓存目录
File cacheDir = new File(context.getCacheDir(), "my-custom-cache");
// 创建基于磁盘的缓存,指定缓存目录
Cache cache = new DiskBasedCache(cacheDir);
// 创建请求队列,使用自定义的缓存
RequestQueue requestQueue = new RequestQueue(cache, new BasicNetwork(new HurlStack()));
requestQueue.start();
4.2 配置缓存大小
Volley默认的缓存大小是5MB。我们可以通过DiskBasedCache的构造函数来指定更大的缓存大小:
// 获取应用的缓存目录
File cacheDir = new File(context.getCacheDir(), "volley");
// 指定缓存大小为10MB
int cacheSize = 10 * 1024 * 1024; // 10MB
// 创建基于磁盘的缓存,指定缓存目录和大小
Cache cache = new DiskBasedCache(cacheDir, cacheSize);
// 创建请求队列,使用自定义的缓存
RequestQueue requestQueue = new RequestQueue(cache, new BasicNetwork(new HurlStack()));
requestQueue.start();
4.3 源码分析
DiskBasedCache类的构造函数和相关方法的源码如下:
/**
* 基于磁盘的缓存实现
*/
public class DiskBasedCache implements Cache {
/** 默认的缓存大小(字节) */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; // 5MB
/** 缓存目录 */
private final File mRootDirectory;
/** 缓存的最大大小(字节) */
private final int mMaxCacheSizeInBytes;
/** 当前缓存的大小(字节) */
private long mTotalSize = 0;
/**
* 创建一个新的磁盘缓存
* @param rootDirectory 缓存目录
* @param maxCacheSizeInBytes 缓存的最大大小(字节)
*/
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
// 其他方法...
/**
* 将数据存入缓存
* @param key 缓存键
* @param entry 缓存条目
*/
@Override
public synchronized void put(String key, Entry entry) {
// 在存入缓存前,检查是否需要清理缓存以腾出空间
pruneIfNeeded(entry.data.length);
// 其他存入缓存的逻辑...
}
/**
* 如果需要,清理缓存以腾出空间
* @param neededSpace 需要的空间大小(字节)
*/
private void pruneIfNeeded(int neededSpace) {
// 如果当前缓存大小加上需要的空间超过最大缓存大小
if ((mTotalSize + neededSpace) > mMaxCacheSizeInBytes) {
// 计算需要清理的目标大小(最大缓存大小的90%)
int targetSize = (int) (mMaxCacheSizeInBytes * 0.9f);
// 遍历缓存条目,按访问顺序删除最老的条目
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext() && mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
// 删除对应的缓存文件
boolean deleted = e.file.delete();
// 更新缓存大小
if (deleted) {
mTotalSize -= e.size;
}
// 从缓存映射表中移除该条目
iterator.remove();
}
// 如果清理后仍然没有足够的空间,抛出异常
if (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.e("Failed to clear space in cache");
}
}
}
}
五、缓存过期策略配置
5.1 HTTP标准缓存头
Volley默认使用HTTP标准的缓存头来决定缓存的过期时间,主要包括:
-
Cache-Control:控制缓存的行为,常见的值包括:
- no-cache:表示必须先与服务器确认缓存的有效性才能使用缓存。
- no-store:表示禁止缓存该响应。
- max-age:表示缓存的最大有效时间(秒)。
- must-revalidate:表示一旦缓存过期,必须重新向服务器验证。
-
Expires:指定缓存的过期时间,是一个具体的日期和时间。
5.2 自定义缓存过期时间
我们可以通过自定义Request类来覆盖默认的缓存过期时间:
// 自定义StringRequest,设置自定义的缓存过期时间
public class CustomCacheRequest extends StringRequest {
private long mSoftTtl;
private long mTtl;
public CustomCacheRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener, long softTtl, long ttl) {
super(method, url, listener, errorListener);
mSoftTtl = softTtl;
mTtl = ttl;
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
// 调用父类的解析方法
Response<String> parsedResponse = super.parseNetworkResponse(response);
// 如果解析成功,修改缓存条目的过期时间
if (parsedResponse != null && parsedResponse.cacheEntry != null) {
// 设置软过期时间(毫秒)
parsedResponse.cacheEntry.softTtl = System.currentTimeMillis() + mSoftTtl;
// 设置硬过期时间(毫秒)
parsedResponse.cacheEntry.ttl = System.currentTimeMillis() + mTtl;
}
return parsedResponse;
}
}
使用自定义的Request类:
// 创建自定义缓存请求,设置软过期时间为1分钟,硬过期时间为5分钟
CustomCacheRequest request = new CustomCacheRequest(
Request.Method.GET,
"https://api.example.com/data",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// 处理响应
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// 处理错误
}
},
60 * 1000, // 软过期时间:1分钟
5 * 60 * 1000 // 硬过期时间:5分钟
);
// 将请求添加到队列
requestQueue.add(request);
5.3 源码分析
HttpHeaderParser类的parseCacheHeaders方法源码如下:
/**
* 解析HTTP响应头,提取缓存相关信息
* @param response 网络响应
* @return 缓存条目
*/
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
long now = System.currentTimeMillis();
// 获取响应头
Map<String, String> headers = response.headers;
// 初始化服务器日期、过期时间、软过期时间等
long serverDate = 0;
long serverExpires = 0;
long softExpire = 0;
long maxAge = 0;
boolean hasCacheControl = false;
boolean mustRevalidate = false;
// 解析Date头
String serverEtag;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = parseDateAsEpoch(headerValue);
}
// 解析Cache-Control头
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (String token : tokens) {
token = token.trim();
if (token.equals("no-cache") || token.equals("no-store")) {
// 如果是no-cache或no-store,直接返回null,表示不缓存
return null;
} else if (token.startsWith("max-age=")) {
try {
// 解析max-age值
maxAge = Long.parseLong(token.substring("max-age=".length()));
} catch (NumberFormatException e) {
// 忽略解析错误
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
// 标记必须重新验证
mustRevalidate = true;
}
}
}
// 解析Expires头
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
// 获取ETag
serverEtag = headers.get("ETag");
// 计算缓存时间
// Cache-Control头优先级高于Expires头
if (hasCacheControl) {
// 如果有Cache-Control头,使用它来计算缓存时间
softExpire = now + maxAge * 1000;
// 如果是must-revalidate,软过期时间和硬过期时间相同
if (mustRevalidate) {
softExpire = now + maxAge * 1000;
}
} else if (serverDate > 0 && serverExpires >= serverDate) {
// 如果没有Cache-Control头,但有Expires头,使用它来计算缓存时间
softExpire = now + (serverExpires - serverDate);
}
// 创建缓存条目
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.serverDate = serverDate;
entry.softTtl = softExpire;
entry.ttl = softExpire; // 默认情况下,硬过期时间和软过期时间相同
entry.responseHeaders = headers;
entry.hasCacheControl = hasCacheControl;
entry.mustRevalidate = mustRevalidate;
return entry;
}
六、缓存键生成规则配置
6.1 默认缓存键
Request类的getCacheKey方法默认使用请求的URL作为缓存键:
/**
* 返回此请求的缓存键
*/
public String getCacheKey() {
// 默认使用请求的URL作为缓存键
return getUrl();
}
6.2 自定义缓存键
我们可以通过重写getCacheKey方法来定义自定义的缓存键:
// 自定义请求类,重写getCacheKey方法
public class CustomCacheKeyRequest extends StringRequest {
private String mCustomCacheKey;
public CustomCacheKeyRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener, String customCacheKey) {
super(method, url, listener, errorListener);
mCustomCacheKey = customCacheKey;
}
@Override
public String getCacheKey() {
// 使用自定义的缓存键
return mCustomCacheKey;
}
}
使用自定义缓存键的示例:
// 创建自定义缓存键的请求
CustomCacheKeyRequest request = new CustomCacheKeyRequest(
Request.Method.GET,
"https://api.example.com/data?page=1",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// 处理响应
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// 处理错误
}
},
"custom_cache_key_data_page_1" // 自定义缓存键
);
// 将请求添加到队列
requestQueue.add(request);
6.3 处理请求参数的缓存键
当请求包含参数时,我们可能需要将参数也包含在缓存键中:
// 自定义请求类,包含请求参数的缓存键
public class ParameterizedRequest extends StringRequest {
private Map<String, String> mParams;
public ParameterizedRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener, Map<String, String> params) {
super(method, url, listener, errorListener);
mParams = params;
}
@Override
protected Map<String, String> getParams() throws AuthFailureError {
return mParams;
}
@Override
public String getCacheKey() {
// 生成包含请求参数的缓存键
StringBuilder sb = new StringBuilder(getUrl());
if (mParams != null && !mParams.isEmpty()) {
sb.append("?");
boolean first = true;
for (Map.Entry<String, String> entry : mParams.entrySet()) {
if (!first) {
sb.append("&");
}
sb.append(entry.getKey()).append("=").append(entry.getValue());
first = false;
}
}
return sb.toString();
}
}
七、缓存优先级设置
7.1 请求优先级枚举
Volley定义了一个Priority枚举,用于表示请求的优先级:
/**
* 请求优先级枚举
*/
public enum Priority {
LOW,
NORMAL,
HIGH,
IMMEDIATE
}
7.2 设置请求优先级
我们可以通过Request类的setPriority方法来设置请求的优先级:
// 创建请求并设置优先级为HIGH
StringRequest request = new StringRequest(
Request.Method.GET,
"https://api.example.com/data",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// 处理响应
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// 处理错误
}
}
);
// 设置请求优先级为HIGH
request.setPriority(Priority.HIGH);
// 将请求添加到队列
requestQueue.add(request);
7.3 优先级对缓存的影响
虽然优先级主要影响请求的执行顺序,但在缓存方面也有一定的影响。例如,当缓存空间不足时,Volley会优先保留高优先级请求的缓存数据。
7.4 源码分析
RequestQueue类的add方法源码如下:
/**
* 将请求添加到队列中
* @param request 请求对象
* @return 返回请求对象,便于链式调用
*/
public <T> Request<T> add(Request<T> request) {
// 将请求标记为已添加到队列
request.setRequestQueue(this);
// 同步添加请求到队列
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// 设置请求的默认优先级
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// 如果请求不应该被缓存,直接发送到网络
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// 否则,将请求放入缓存队列
mCacheQueue.add(request);
return request;
}
NetworkDispatcher类的run方法中处理请求优先级的源码如下:
/**
* 网络调度器线程的运行方法
*/
@Override
public void run() {
// 设置线程优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 循环处理请求
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
// 从网络请求队列中获取请求(阻塞操作)
request = mQueue.take();
} catch (InterruptedException e) {
// 如果被中断,检查是否应该退出
if (mQuit) {
return;
}
continue;
}
try {
// 标记请求开始
request.addMarker("network-queue-take");
// 如果请求已经被取消,跳过处理
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
// 执行网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-attempt");
// 处理HTTP重定向
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// 解析网络响应
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// 如果请求需要缓存,将响应放入缓存
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// 标记请求已交付响应
request.markDelivered();
// 分发响应到主线程
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
// 处理网络错误
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
// 处理其他异常
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
八、条件请求配置
8.1 条件请求的概念
条件请求是一种HTTP请求,客户端在请求中包含一个或多个条件,服务器只有在满足这些条件时才返回资源。条件请求通常用于验证缓存的有效性。
8.2 使用ETag进行条件请求
ETag是服务器为资源生成的唯一标识符,当资源发生变化时,ETag也会随之变化。我们可以通过重写Request类的getHeaders方法来添加ETag条件:
// 自定义请求类,添加ETag条件请求
public class ETagRequest extends StringRequest {
private String mETag;
public ETagRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener, String eTag) {
super(method, url, listener, errorListener);
mETag = eTag;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = super.getHeaders();
// 如果有ETag,添加到请求头中
if (mETag != null) {
headers.put("If-None-Match", mETag);
}
return headers;
}
}
使用ETag进行条件请求的示例:
// 从缓存中获取ETag
Cache.Entry entry = requestQueue.getCache().get("cache_key");
String eTag = entry != null ? entry.etag : null;
// 创建条件请求,添加ETag
ETagRequest request = new ETagRequest(
Request.Method.GET,
"https://api.example.com/data",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// 处理响应
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// 如果返回304状态码,表示缓存有效
if (error.networkResponse != null && error.networkResponse.statusCode == 304) {
// 使用缓存的响应
Cache.Entry cachedEntry = requestQueue.getCache().get("cache_key");
if (cachedEntry != null) {
String cachedResponse = new String(cachedEntry.data);
// 处理缓存的响应
}
} else {
// 处理其他错误
}
}
},
eTag // 传递ETag
);
// 将请求添加到队列
requestQueue.add(request);
8.3 使用Last-Modified进行条件请求
Last-Modified是服务器返回的资源最后修改时间。我们可以通过重写Request类的getHeaders方法来添加Last-Modified条件:
// 自定义请求类,添加Last-Modified条件请求
public class LastModifiedRequest extends StringRequest {
private long mLastModified;
public LastModifiedRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener, long lastModified) {
super(method, url, listener, errorListener);
mLastModified = lastModified;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = super.getHeaders();
// 如果有Last-Modified时间,添加到请求头中
if (mLastModified > 0) {
headers.put("If-Modified-Since", formatDate(mLastModified));
}
return headers;
}
/**
* 将时间戳格式化为HTTP日期格式
*/
private String formatDate(long timestamp) {
// 使用HTTP标准日期格式
SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(new Date(timestamp));
}
}
8.4 源码分析
Request类的getHeaders方法源码如下:
/**
* 返回此请求的HTTP头
*/
public Map<String, String> getHeaders() throws AuthFailureError {
return Collections.emptyMap();
}
NetworkDispatcher类处理条件请求的源码如下:
/**
* 网络调度器线程的运行方法
*/
@Override
public void run() {
// 其他代码...
try {
// 执行网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-attempt");
// 处理HTTP 304(未修改)状态码
if (networkResponse.statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
// 获取缓存中的条目
Cache.Entry entry = mCache.get(request.getCacheKey());
// 如果缓存条目存在,使用缓存的响应
if (entry != null) {
request.addMarker("network-304-not-modified");
// 创建一个新的响应,使用缓存的数据
Response<?> response = Response.success(
request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders)).result,
entry
);
// 标记请求已交付响应
request.markDelivered();
// 分发响应到主线程
mDelivery.postResponse(request, response);
return;
}
}
// 其他代码...
} catch (Exception e) {
// 处理异常
}
}
九、自定义缓存实现
9.1 实现Cache接口
如果Volley默认的DiskBasedCache不能满足需求,我们可以实现自己的缓存系统:
// 自定义内存缓存实现
public class MemoryCache implements Cache {
/** 用于存储缓存条目的映射表 */
private final LinkedHashMap<String, Entry> mCache = new LinkedHashMap<String, Entry>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Entry<String, Entry> eldest) {
// 当缓存条目超过最大大小时,移除最老的条目
return size() > MAX_CACHE_SIZE;
}
};
/** 最大缓存条目数 */
private static final int MAX_CACHE_SIZE = 100;
@Override
public Entry get(String key) {
// 从缓存中获取条目
return mCache.get(key);
}
@Override
public void put(String key, Entry entry) {
// 将条目放入缓存
mCache.put(key, entry);
}
@Override
public void invalidate(String key, boolean fullExpire) {
// 获取缓存条目
Entry entry = mCache.get(key);
// 如果缓存条目存在,更新其过期时间
if (entry != null) {
// 设置软过期时间为当前时间
entry.softTtl = System.currentTimeMillis();
// 如果需要完全过期,设置硬过期时间为当前时间
if (fullExpire) {
entry.ttl = System.currentTimeMillis();
}
// 更新缓存条目
mCache.put(key, entry);
}
}
@Override
public void remove(String key) {
// 从缓存中移除条目
mCache.remove(key);
}
@Override
public void clear() {
// 清空缓存
mCache.clear();
}
}
9.2 使用自定义缓存实现
创建RequestQueue时使用自定义的缓存实现:
// 创建自定义内存缓存
Cache cache = new MemoryCache();
// 创建请求队列,使用自定义的缓存
RequestQueue requestQueue = new RequestQueue(cache
十二、缓存配置的性能优化
12.1 缓存读取性能优化
在高并发场景下,缓存读取性能至关重要。Volley默认的DiskBasedCache使用文件系统存储缓存数据,虽然有一定的优化,但在频繁读取时仍可能成为性能瓶颈。
从源码角度分析,DiskBasedCache的get方法实现如下:
@Override
public synchronized Entry get(String key) {
// 根据缓存键生成文件名
File file = getFileForKey(key);
// 检查文件是否存在
if (!file.exists()) {
return null;
}
// 读取文件内容
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
// 读取缓存头部信息
CacheHeader header = CacheHeader.readHeader(fis);
// 如果头部信息无效,删除文件并返回null
if (header == null) {
VolleyLog.d("Cache header corruption for %s", file.getAbsolutePath());
file.delete();
return null;
}
// 读取缓存数据
byte[] data = streamToBytes(fis, (int) (file.length() - fis.available()));
// 创建缓存条目
Entry entry = new Entry();
entry.data = data;
entry.etag = header.etag;
entry.softTtl = header.softTtl;
entry.ttl = header.ttl;
entry.serverDate = header.serverDate;
entry.responseHeaders = header.responseHeaders;
return entry;
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
// 发生异常时删除文件
remove(key);
return null;
} finally {
// 关闭输入流
if (fis != null) {
try {
fis.close();
} catch (IOException ignored) {
}
}
}
}
可以看到,每次读取缓存都需要进行文件IO操作,这在频繁读取场景下会影响性能。
优化方案:
- 内存缓存层:在应用层添加内存缓存层,使用
LruCache或ConcurrentHashMap等数据结构缓存最近使用的响应。例如:
// 添加内存缓存层
private final LruCache<String, Cache.Entry> mMemoryCache = new LruCache<String, Cache.Entry>(1024 * 1024) { // 1MB内存缓存
@Override
protected int sizeOf(String key, Cache.Entry entry) {
return entry.data.length;
}
};
// 在请求时优先从内存缓存获取
public Cache.Entry getFromCache(String key) {
// 先从内存缓存获取
Cache.Entry entry = mMemoryCache.get(key);
if (entry != null) {
return entry;
}
// 再从磁盘缓存获取
entry = mDiskBasedCache.get(key);
if (entry != null) {
// 将磁盘缓存的条目放入内存缓存
mMemoryCache.put(key, entry);
}
return entry;
}
- 异步读取:对于非UI线程的缓存读取,可以使用异步方式,避免阻塞主线程。例如:
// 异步读取缓存
public void getCacheAsync(final String key, final CacheCallback callback) {
new Thread(new Runnable() {
@Override
public void run() {
final Cache.Entry entry = mDiskBasedCache.get(key);
if (callback != null) {
// 将结果回调到主线程
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(new Runnable() {
@Override
public void run() {
callback.onCacheLoaded(entry);
}
});
}
}
}).start();
}
// 回调接口
public interface CacheCallback {
void onCacheLoaded(Cache.Entry entry);
}
12.2 缓存写入性能优化
缓存写入操作同样可能成为性能瓶颈,特别是在处理大量数据或高并发写入时。
DiskBasedCache的put方法源码如下:
@Override
public synchronized void put(String key, Entry entry) {
// 检查是否需要清理缓存以腾出空间
pruneIfNeeded(entry.data.length);
// 生成缓存文件
File file = getFileForKey(key);
// 创建临时文件用于写入
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
// 创建临时文件
File tmpFile = getTempFileForKey(key);
// 写入缓存头部信息
fos = new FileOutputStream(tmpFile);
bos = new BufferedOutputStream(fos);
CacheHeader header = new CacheHeader(key, entry);
header.writeHeader(bos);
// 写入缓存数据
bos.write(entry.data);
// 刷新并关闭流
bos.flush();
// 将临时文件重命名为正式缓存文件
if (!tmpFile.renameTo(file)) {
VolleyLog.e("ERROR: rename failed, tmpFile=%s, file=%s",
tmpFile.getAbsolutePath(), file.getAbsolutePath());
throw new IOException("Rename failed!");
}
// 更新缓存映射表
putEntry(key, header);
} catch (IOException e) {
// 删除临时文件
if (file.exists()) {
if (!file.delete()) {
VolleyLog.e("Could not clean up file %s", file.getAbsolutePath());
}
}
VolleyLog.e("Failed to write cache entry for key=%s, filename=%s: %s",
key, file.getAbsolutePath(), e.getMessage());
} finally {
// 关闭流
if (bos != null) {
try {
bos.close();
} catch (IOException ignored) {
}
} else if (fos != null) {
try {
fos.close();
} catch (IOException ignored) {
}
}
}
}
可以看到,写入操作涉及文件IO和临时文件操作,比较耗时。
优化方案:
- 批量写入:将多个小的缓存写入操作合并为一个大的写入操作,减少文件IO次数。例如:
// 批量写入缓存
public void batchPut(final Map<String, Cache.Entry> entries) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (DiskBasedCache.this) {
for (Map.Entry<String, Cache.Entry> entry : entries.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
}
}).start();
}
- 异步写入:将缓存写入操作放到后台线程执行,避免阻塞主线程。可以使用线程池管理写入线程:
// 创建线程池用于异步写入
private final ExecutorService mWriteExecutor = Executors.newSingleThreadExecutor();
// 异步写入缓存
public void putAsync(final String key, final Cache.Entry entry) {
mWriteExecutor.submit(new Runnable() {
@Override
public void run() {
put(key, entry);
}
});
}
12.3 缓存清理策略优化
Volley默认的缓存清理策略是当缓存空间不足时,按照LRU(最近最少使用)算法删除最老的缓存条目。但在某些场景下,这种策略可能不够灵活。
DiskBasedCache的pruneIfNeeded方法源码如下:
private void pruneIfNeeded(int neededSpace) {
// 如果当前缓存大小加上需要的空间超过最大缓存大小
if ((mTotalSize + neededSpace) > mMaxCacheSizeInBytes) {
// 计算需要清理的目标大小(最大缓存大小的90%)
int targetSize = (int) (mMaxCacheSizeInBytes * 0.9f);
// 遍历缓存条目,按访问顺序删除最老的条目
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext() && mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
// 删除对应的缓存文件
boolean deleted = e.file.delete();
// 更新缓存大小
if (deleted) {
mTotalSize -= e.size;
}
// 从缓存映射表中移除该条目
iterator.remove();
}
// 如果清理后仍然没有足够的空间,抛出异常
if (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.e("Failed to clear space in cache");
}
}
}
优化方案:
- 基于优先级的清理策略:为不同类型的缓存条目设置优先级,在清理时优先保留高优先级的缓存。可以扩展
CacheHeader类添加优先级字段:
// 扩展CacheHeader类添加优先级字段
public static class PriorityCacheHeader extends CacheHeader {
public int priority;
public PriorityCacheHeader(String key, Entry entry, int priority) {
super(key, entry);
this.priority = priority;
}
// 其他方法...
}
// 修改pruneIfNeeded方法,优先删除低优先级的缓存
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) > mMaxCacheSizeInBytes) {
// 先按优先级排序缓存条目
List<Map.Entry<String, CacheHeader>> entries = new ArrayList<>(mEntries.entrySet());
Collections.sort(entries, new Comparator<Map.Entry<String, CacheHeader>>() {
@Override
public int compare(Map.Entry<String, CacheHeader> e1, Map.Entry<String, CacheHeader> e2) {
// 按优先级升序排列
return Integer.compare(getPriority(e1.getValue()), getPriority(e2.getValue()));
}
private int getPriority(CacheHeader header) {
if (header instanceof PriorityCacheHeader) {
return ((PriorityCacheHeader) header).priority;
}
return 0; // 默认优先级
}
});
// 按优先级顺序删除缓存条目
for (Map.Entry<String, CacheHeader> entry : entries) {
if (mTotalSize + neededSpace <= mMaxCacheSizeInBytes) {
break;
}
CacheHeader e = entry.getValue();
boolean deleted = e.file.delete();
if (deleted) {
mTotalSize -= e.size;
}
mEntries.remove(entry.getKey());
}
if (mTotalSize + neededSpace > mMaxCacheSizeInBytes) {
VolleyLog.e("Failed to clear space in cache");
}
}
}
- 基于时间窗口的清理策略:对于某些时效性要求高的数据,可以设置更严格的时间窗口,超过时间窗口的缓存优先清理。
十三、缓存配置的安全与隐私考虑
13.1 敏感数据缓存问题
在移动应用中,可能会涉及到用户的敏感数据,如个人信息、令牌等。如果这些数据被错误地缓存,可能会导致安全风险。
从源码角度看,Volley默认会缓存所有设置了shouldCache=true的请求响应,包括可能包含敏感数据的响应。
安全建议:
- 避免缓存敏感数据:对于包含敏感信息的请求,确保将
shouldCache设置为false:
// 包含敏感信息的请求不缓存
StringRequest sensitiveRequest = new StringRequest(
Request.Method.GET,
"https://api.example.com/user/profile",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// 处理响应
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// 处理错误
}
}
);
sensitiveRequest.setShouldCache(false); // 禁止缓存
requestQueue.add(sensitiveRequest);
- 加密敏感缓存数据:如果确实需要缓存敏感数据,应在存入缓存前对数据进行加密。可以扩展
DiskBasedCache类实现加密功能:
// 加密缓存实现
public class EncryptedDiskBasedCache extends DiskBasedCache {
private static final String ENCRYPTION_ALGORITHM = "AES";
private final SecretKeySpec mSecretKey;
public EncryptedDiskBasedCache(File rootDirectory, int maxCacheSizeInBytes, String encryptionKey) {
super(rootDirectory, maxCacheSizeInBytes);
// 生成加密密钥
try {
byte[] keyBytes = encryptionKey.getBytes("UTF-8");
// 使用SHA-256生成256位密钥
MessageDigest sha = MessageDigest.getInstance("SHA-256");
keyBytes = sha.digest(keyBytes);
mSecretKey = new SecretKeySpec(keyBytes, ENCRYPTION_ALGORITHM);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new RuntimeException("Failed to initialize encryption key", e);
}
}
@Override
public synchronized void put(String key, Entry entry) {
// 加密数据
try {
byte[] encryptedData = encrypt(entry.data);
entry.data = encryptedData;
} catch (Exception e) {
VolleyLog.e("Failed to encrypt cache data: %s", e.getMessage());
return; // 加密失败则不缓存
}
super.put(key, entry);
}
@Override
public synchronized Entry get(String key) {
Entry entry = super.get(key);
if (entry != null) {
// 解密数据
try {
byte[] decryptedData = decrypt(entry.data);
entry.data = decryptedData;
} catch (Exception e) {
VolleyLog.e("Failed to decrypt cache data: %s", e.getMessage());
remove(key); // 解密失败则删除缓存
return null;
}
}
return entry;
}
// 加密方法
private byte[] encrypt(byte[] data) throws Exception {
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, mSecretKey);
return cipher.doFinal(data);
}
// 解密方法
private byte[] decrypt(byte[] encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, mSecretKey);
return cipher.doFinal(encryptedData);
}
}
13.2 缓存注入攻击
缓存注入攻击是指攻击者通过操纵请求参数,使应用缓存恶意数据,从而影响其他用户的安全漏洞。
从源码角度,Volley本身不会对缓存数据进行额外的安全验证,只是简单地存储和返回响应数据。
防范建议:
- 验证缓存数据来源:在使用缓存数据前,确保数据来源可靠。可以在
Request类中添加验证逻辑:
// 验证缓存数据来源
public class SecureRequest extends StringRequest {
public SecureRequest(int method, String url, Response.Listener<String> listener,
Response.ErrorListener errorListener) {
super(method, url, listener, errorListener);
}
@Override
public void deliverResponse(String response) {
// 验证响应数据是否可信
if (isResponseTrusted(response)) {
super.deliverResponse(response);
} else {
// 数据不可信,不使用缓存
super.deliverError(new VolleyError("Untrusted cache data"));
}
}
private boolean isResponseTrusted(String response) {
// 实现验证逻辑,如检查签名、数据格式等
// 这里只是示例,实际实现需要根据具体业务需求
return response != null && response.startsWith("{\"trusted\":true}");
}
}
- 使用安全的缓存键:避免使用用户可控的参数作为缓存键的一部分,防止攻击者构造恶意的缓存键。可以在生成缓存键时进行规范化处理:
@Override
public String getCacheKey() {
String originalKey = super.getCacheKey();
// 规范化处理,如去除特殊字符
return originalKey.replaceAll("[^a-zA-Z0-9]", "_");
}
13.3 缓存数据的生命周期管理
合理管理缓存数据的生命周期,确保数据在过期后及时清理,避免旧数据被误用。
从源码角度,Volley通过Entry类的softTtl和ttl字段管理缓存的生命周期,但在某些场景下可能需要更精细的控制。
管理建议:
- 设置合理的过期时间:根据数据的时效性,设置合理的软过期和硬过期时间。
- 主动清理过期缓存:在应用启动时或定期清理过期缓存:
// 清理过期缓存
public void clearExpiredCache() {
Iterator<Map.Entry<String, Cache.Entry>> iterator = mCache.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Cache.Entry> entry = iterator.next();
if (entry.getValue().isExpired()) {
mCache.remove(entry.getKey());
}
}
}
- 用户登出时清理相关缓存:当用户登出应用时,清理与该用户相关的缓存数据,保护用户隐私:
// 用户登出时清理缓存
public void onUserLogout() {
// 获取当前用户ID作为前缀
String userIdPrefix = getCurrentUserId() + "_";
// 清理所有以用户ID为前缀的缓存
Iterator<Map.Entry<String, Cache.Entry>> iterator = mCache.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Cache.Entry> entry = iterator.next();
if (entry.getKey().startsWith(userIdPrefix)) {
mCache.remove(entry.getKey());
}
}
}
十四、与其他缓存库的对比
14.1 与Glide缓存的对比
Glide是Android上常用的图片加载库,也提供了强大的缓存功能。
缓存策略:
- Volley:主要针对HTTP响应进行缓存,支持多种缓存过期策略,缓存数据以文件形式存储。
- Glide:专门针对图片进行缓存,有内存缓存、磁盘缓存两级缓存,支持图片尺寸转换和格式优化。
源码实现:
- Volley:通过
DiskBasedCache类实现磁盘缓存,使用LRU算法清理缓存。 - Glide:使用自定义的
DiskLruCache实现磁盘缓存,内存缓存使用LruResourceCache,并支持弱引用缓存。
适用场景:
- Volley:适合缓存各种类型的HTTP响应数据,如JSON、XML等。
- Glide:更适合图片加载场景,能自动处理图片的解码、缩放等操作。
14.2 与OkHttp缓存的对比
OkHttp是另一个流行的HTTP客户端库,也提供了缓存功能。
缓存策略:
- Volley:依赖HTTP标准头信息(如Cache-Control、ETag),也支持自定义缓存策略。
- OkHttp:同样依赖HTTP标准头信息,但提供了更灵活的缓存拦截器机制。
源码实现:
- Volley:通过
DiskBasedCache实现磁盘缓存,缓存逻辑集成在NetworkDispatcher中。 - OkHttp:通过
Cache类和CacheInterceptor实现缓存,缓存逻辑更独立。
适用场景:
- Volley:适合与Android UI组件紧密结合的场景,提供了简单易用的API。
- OkHttp:更适合需要底层控制和高级配置的场景,如自定义缓存拦截器。
14.3 与Room数据库缓存的对比
Room是Android官方的ORM库,也可用于本地数据缓存。
缓存策略:
- Volley:基于HTTP响应的缓存,主要用于临时存储网络数据。
- Room:基于数据库的缓存,适合长期存储结构化数据。
源码实现:
- Volley:通过文件系统实现缓存,数据以二进制形式存储。
- Room:基于SQLite数据库,提供了类型安全的DAO接口。
适用场景:
- Volley:适合缓存临时的、时效性较强的网络数据。
- Room:适合缓存需要长期存储、复杂查询的数据,如用户信息、应用配置等。
上述内容涵盖了Volley缓存配置的性能优化、安全隐私考虑以及与其他缓存库的对比分析。如果你需要进一步深入探讨某个方面,或有其他需求,欢迎继续提问。