与Volley缓存相关的文件主要为:
- Cache:定义缓存中的方法的接口类
- NoCache:实现了Cache接口,当并没有实现其方法,相当于一个空壳
- DiskBasedCache:实现了缓存中的所有方法,为核心类
1. Cache的实现
public interface Cache {
Entry get(String key);
void put(String key, Entry entry);
void initialize();
//True to fully expire the entry, false to soft expire
void invalidate(String key, boolean fullExpire);
void remove(String key);
void clear();
//用于表示缓存的内容
class Entry {
//缓存数据
public byte[] data;
//保证缓存一致性
public String eTag;
//从服务端获取到数据的时间
public long serverDate;
//最后被修改的时间
public long lastModified;
//保存该数据的最长时间
public long ttl;
//刷新数据的时间
public long softTtl;
//保存响应头的内容
public Map<String,String> responseHeaders = Collections.emptyMap();
//判断数据是否过期
boolean isExpired(){
return this.ttl < System.currentTimeMillis();
}
//判断数据是否需要刷新
boolean refreshNeeded(){
return this.softTtl < System.currentTimeMillis();
}
}
}
这里要注意的是除了提供常见的缓存方法,还提供了一个用于表示缓存内容的内部类。
2. DiskBasedCache的实现
- 相关的定义和构造方法:
//定义一个linkedHashMap作为内存缓存,缓存的是信息头
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<>(16, .75f, true);
//当前缓存的最大值
private long mTotalSize = 0;
//缓存目录
private final File mRootDirectory;
//定义最大的缓存空间
private final int mMaxCacheSizeInBytes;
//默认的缓存空间
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
//当超过最大缓存时,对缓存的删除的负载因子
private static final float HYSTERESIS_FACTORY = 0.9f;
//缓存的标志-魔数
private static final int CACHE_MAGIC = 0x20150306;
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes){
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
值得注意的是其内部采用了LinkedHashMap,而我们知道LinkedHashMap内部实现是LRU算法。
- 对信息头CacheHeader(个人说法)的定义
//定义缓存的信息头
public static class CacheHeader {
//缓存数据的大小
public long size;
public String key;
public String eTags;
public long serverDate;
public long lastModified;
public long ttl;
public long softTtl;
public Map<String,String> responseHeaders;
//定义一个空的CacheHeader,便于后面的操作
private CacheHeader(){
}
public CacheHeader(String key, Entry entry){
this.key = key;
this.size = entry.data.length;
this.eTags = entry.eTag;
this.serverDate = entry.serverDate;
this.lastModified = entry.lastModified;
this.ttl = entry.ttl;
this.softTtl = entry.softTtl;
this.responseHeaders = entry.responseHeaders;
}
//将CacheHeader对象转换为Entry对象
public Entry toCacheEntry(byte[] data) {
Entry e = new Entry();
e.data = data;
e.eTag = eTags;
e.serverDate = serverDate;
e.lastModified = lastModified;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = responseHeaders;
return e;
}
}
这里可能会产生一个疑问:
为什么还要重新创建一个CacheHeader对象呢?我们不是已经用Entry对象表示缓存内容吗?——实际上,我们对比下两者,发现Entry中是包含真正缓存数据data的,而CacheHeader中只是包含了数据的大小。
为什么要这样设计呢?
我们可以这么想,如果LinkedHashMap中的value对应的Entry对象,那么试想这对内存的需求要多大,因为这相当于将数据完全保存在内存中!!!
因此,为了解决这个问题,我们可以将数据的相关信息保存在内存中,然后真正的数据放在文件中,当需要获取的时候,找到对应的文件,再将文件中的数据加载到内存即可。
- 定义一个可统计已读取长度的输入流
//定义一个可统计已读取长度的输入流
private static class CountingInputStream extends FilterInputStream {
//记录已读取的长度
private int bytesRead = 0;
protected CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int result = super.read();
if (result != -1){
bytesRead ++;
}
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
if (result != -1){
//针对缓存池的读取
bytesRead += result;
}
return result;
}
}
为什么需要这么一个统计已读取的输入流设计呢?这是为后续的文件读取时,对信息头和数据的获取的便利。(后面会细说)
- 实现读写各种类型数据的操作
//一个个字节读取输入流
private static int read(InputStream is) throws IOException {
int b = is.read();
if (b == -1){
throw new EOFException();
}
return b;
}
//写入整型数据:write是按一个个byte地写
static void writeInt(OutputStream os, int n) throws IOException {
os.write((n >> 0) & 0xff);
os.write((n >> 8) & 0xff);
os.write((n >> 16) & 0xff);
os.write((n >> 24) & 0xff);
}
//读取整型数据
static int readInt(InputStream is) throws IOException {
int n = 0;
n |= (read(is) << 0);
n |= (read(is) << 8);
n |= (read(is) << 16);
n |= (read(is) << 24);
return n;
}
//写入Long数据
static void writeLong(OutputStream os, long n) throws IOException {
os.write((byte)(n >>> 0));
os.write((byte)(n >>> 8));
os.write((byte)(n >>> 16));
os.write((byte)(n >>> 24));
os.write((byte)(n >>> 32));
os.write((byte)(n >>> 40));
os.write((byte)(n >>> 48));
os.write((byte)(n >>> 56));
}
//读取Long数据
static long readLong(InputStream is) throws IOException {
long n = 0;
n |= ((read(is) & 0xFFL) << 0);
n |= ((read(is) & 0xFFL) << 8);
n |= ((read(is) & 0xFFL) << 16);
n |= ((read(is) & 0xFFL) << 24);
n |= ((read(is) & 0xFFL) << 32);
n |= ((read(is) & 0xFFL) << 40);
n |= ((read(is) & 0xFFL) << 48);
n |= ((read(is) & 0xFFL) << 56);
return n;
}
//写入String数据
static void writeString(OutputStream os, String s) throws IOException {
//将String类型转换为byte类型
byte[] b = s.getBytes("UTF-8");
//分别写入长度和数据
writeLong(os, b.length);
os.write(b, 0, b.length);
}
//读取String数据
static String readString(InputStream is) throws IOException {
int n = (int) readLong(is);
//将输入流转换为byte数组
byte[] b = streamToBytes(is, n);
//将byte数组转换为String数据
return new String(b, "UTF-8");
}
//写入信息头
static void writeStringMap(Map<String,String> map, OutputStream os) throws IOException {
if (map != null){
writeInt(os, map.size());
for (Map.Entry<String, String> entry : map.entrySet()){
writeString(os, entry.getKey());
writeString(os, entry.getValue());
}
} else {
writeInt(os, 0);
}
}
//读取信息头
static Map<String, String> readStringMap(InputStream is) throws IOException {
int size = readInt(is);
Map<String, String> result = (size == 0) ? Collections.<String, String>emptyMap()
: new HashMap<String, String>(size);
for (int i = 0; i < size; i ++){
String key = readString(is).intern();
String value = readString(is).intern();
result.put(key,value);
}
return result;
}
//将流转换为byte:注意这里是对固定长度的转换
private static byte[] streamToBytes(InputStream is, int length) throws IOException {
byte[] bytes = new byte[length];
int count;
//统计读取的字节数
int pos = 0;
//进行读取
while (pos < length && ((count = is.read(bytes, pos, length - pos)) != -1)){
pos += count;
}
//若读取的字节数和传入的长度不同,说明存在问题
if (pos != length){
throw new IOException("Expected " + length + " bytes, read" + pos + "bytes");
}
return bytes;
}
以上主要提供了对int、long和String类型的读写。要注意的是对int和long不同位数的处理,以及String和byte数组之间的转换。
- 对信息头的读写
在对进行读写操作分析前,我们先了解下文件格式,如下:
/*信息头*/(非文件数据)
魔数(用于标记缓存)
key
eTags
serverDate
lastModified
ttl
softTtl
responseHeaders
/*信息头*/
真实数据
对应的读写如下:
//从文件输入流中读取信息头,并返回CacheHeader对象
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = readInt(is);
if (magic != CACHE_MAGIC){
throw new IOException();
}
entry.key = readString(is);
entry.eTags = readString(is);
if (entry.eTags.equals("")){
entry.eTags = null;
}
entry.serverDate = readLong(is);
entry.lastModified = readLong(is);
entry.ttl = readLong(is);
entry.softTtl = readLong(is);
entry.responseHeaders = readStringMap(is);
return entry;
}
//将信息头内容写入到输出流中
public boolean writeHeader(OutputStream os){
try {
writeInt(os, CACHE_MAGIC);
writeString(os, key);
writeString(os, eTags == null ? "" : eTags);
writeLong(os, serverDate);
writeLong(os, lastModified);
writeLong(os, ttl);
writeLong(os, softTtl);
writeStringMap(responseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
VolleyLog.d("%s",e.toString());
return false;
}
}
- 初始化缓存操作
//初始化缓存:即将缓存目录中所有文件的信息头加入到内存缓存中
@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){
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(file));
CacheHeader entry = CacheHeader.readHeader(bis);
entry.size = file.length();
//将文件中的信息头添加到内存缓存中
putEntry(entry.key, entry);
} catch (IOException e){
if (file != null){
file.delete();
}
} finally {
try {
if (bis != null){
bis.close();
}
} catch (IOException ingored){}
}
}
}
实际上,所谓的初始化缓存就是:检查缓存目录是否存在,若存在则需要将缓存文件中信息头添加到内存缓存中。
- get操作获取缓存
检查内存缓存中是否存在——>若存在,则获取到key对应的文件——>读取文件中的数据——>包装为Entry对象返回
public Entry get(String key) {
//获取缓存头信息
CacheHeader entry = mEntries.get(key);
if (entry == null){
return null;
}
//获取到文件
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
//使用CountingInputStream来获取文件内容
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
//读取缓存内容中信息头
CacheHeader.readHeader(cis);
//读取缓存中的真正数据
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
//包装为Entry对象返回
return entry.toCacheEntry(data);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} catch (NegativeArraySizeException e){
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null){
try {
cis.close();
} catch (IOException e) {
return null;
}
}
}
}
我们可以看到这里使用了我们自定义的CountingInputStream来获取文件内容,文件中存在信息头和数据,我们需要对它们分别读取,问题是读取完信息头后,如何确定数据流中已读取的位置?实际上,这就是CountingInputStream的作用。我们看到这一句byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
,里面通过cis.bytesRead
获取到已读取的内容,那么文件剩下的内容自然就是数据了。
- put操作实现添加缓存
检查缓存空间是否足够——>若空间足够,则将信息头和数据写入到文件中——>最后将信息头添加到内存缓存中
public synchronized void put(String key, Entry entry) {
//检查缓存中的剩余空间是否足够容纳新添加的缓存对象
pruneIfNeed(entry.data.length);
//获取到文件
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
//将信息头写入到文件中
boolean success = e.writeHeader(fos);
if (!success){
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
//将真正数据写入到文件中
fos.write(entry.data);
fos.close();
//将信息头信息添加到内存缓存中
putEntry(key, e);
return;
} catch (IOException e){
}
//添加失败,则对文件进行清除
boolean deleted = file.delete();
if (!deleted){
VolleyLog.d("Couldn't clean up file %s", file.getAbsolutePath());
}
}
我们先看到对缓存空间的检查,若空间不足,应该如何处理?
//检查缓存中的剩余空间是否足够容纳新添加的缓存对象
private void pruneIfNeed(int neededSpace) {
//剩余空间满足新添加的缓存对象所需大小
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes){
return;
}
if (VolleyLog.DEBUG){
VolleyLog.v("Pruning old cache entries");
}
long before = mTotalSize;
int prunedFiles = 0;
//获取当前时间,为日志记录准备
long startTime = SystemClock.elapsedRealtime();
//获取到内存缓存的迭代器
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
//删除当前文件
boolean deleted = getFileForKey(e.key).delete();
//若删除成功,则将减少当前空间的大小
if (deleted){
mTotalSize -= e.size;
} else {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
e.key, getFileNameForKey(e.key));
}
//当然,内存缓存中也要删除对应的信息
iterator.remove();
prunedFiles ++;
//删除文件,直到满足达到负载因子的需求
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTORY){
break;
}
}
//打印日志信息
if (VolleyLog.DEBUG){
VolleyLog.v("pruned %d files, %d bytes, %d ms",
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
}
}
我们可以看到,当空间不足时,会采用删除文件的方法来保证足够的空间来添加新的缓存。删除的方式很简单,就是遍历LinkedHashMap的节点,逐个删除(我们之前提到过其内部实现了LRU算法,因此,实际上删除的是最近最少使用的节点)。
保证的方法也稍微有些特殊,并不是说刚好满足可以添加新的缓存就停止删除,而是保证小于最大缓存的0.9。为啥?因为如果只是刚好满足的话,下一次添加新的缓存可能又再次触发删除文件操作,这样势必会影响一些效率。
putEntry操作也需要注意:判断是否内存缓存中已含有该key对应的缓存,若存在,则更新为新的缓存
//将信息头添加到内存缓存中
private void putEntry(String key, CacheHeader entry) {
//若内存缓存中,不包含,则直接添加
if (!mEntries.containsKey(key)){
mTotalSize += entry.size;
}
//否则,将替换到之前的缓存,即更新缓存
else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
//最后添加信息头到内存缓存中
mEntries.put(key, entry);
}
- remove操作删除缓存
对应删除文件缓存和内存缓存即可。
//删除缓存
@Override
public synchronized void remove(String key) {
//删除对应的文件
boolean deleted = getFileForKey(key).delete();
//删除对应的信息头
removeEntry(key);
if (!deleted){
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
key, getFileNameForKey(key));
}
}
//删除内存缓存中的key对应的信息头
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null){
mTotalSize -= entry.size;
mEntries.remove(key);
}
}
- clear操作清除缓存
删除缓存目录下的所有缓存文件和清空内存缓存即可
public synchronized void clear() {
//删除缓存目录下的所有文件
File[] files = mRootDirectory.listFiles();
if (files != null){
for (File file : files){
file.delete();
}
}
//清空内存缓存
mEntries.clear();
mTotalSize = 0;
VolleyLog.d("Cache cleared");
}
- invalidate操作使得缓存失效
注意刷新缓存和使缓存过期两种情况
public synchronized void invalidate(String key, boolean fullExpire) {
Entry entry = get(key);
if (entry != null){
//表示需要刷新缓存
entry.softTtl = 0;
//表示缓存过期
if (fullExpire){
entry.softTtl = 0;
}
//注意:这里还是需要将其添加到内存缓存中
put(key, entry);
}
}
3.从中获取到的技能
- CacheHeader和Entry的定义,一个只是保留数据相关信息保存在内存缓存中,另一个是保存数据,保留在文件中。这样的定义,很好地将内存缓存和磁盘缓存联系在一起。
- 数据保存在文件的格式也是蛮值得学习的,读和写与文件格式密切相关。
- 对基本类型数据的读写操作,比如int和long的位操作,String和byte数组的转换。
DiskBasedCache完整代码