LruDiskCache是使用Lru算法的磁盘缓存类,它的功能是将LruCache中缓存位置由内存改为磁盘,一般两者结合使用,用于对处理小文件,图片的缓存。
下面记录下阅读过程中几个比较重要的点:
Get
获取缓存数据时,LruDiskCache会使用LinkedHashmap的算法,也就是最常使用的放在尾部,最少使用的首先被遍历到.
当你需要获取缓存数据时,首先会得到是一个Snapshot对象(如果数据正常的话:写入成功、在有效内等等),Snapshot其实就是持有缓存文件的输入流,无其它逻辑操作。
private synchronized Snapshot getByDiskKey(String diskKey) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(diskKey);
if (entry == null) {
return null;
}
if (!entry.readable) {//数据是否被写入成功
return null;
}
// 判断时间有效性
if (entry.expiryTimestamp < System.currentTimeMillis()) {
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
if (file.exists() && !file.delete()) {
throw new IOException("failed to delete " + file);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(DELETE + " " + diskKey + '\n');
lruEntries.remove(diskKey);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return null;
}
//同一个key可能对应多个缓存
FileInputStream[] ins = new FileInputStream[valueCount];
try {
for (int i = 0; i < valueCount; i++) {
ins[i] = new FileInputStream(entry.getCleanFile(i));
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
IOUtils.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + " " + diskKey + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(diskKey, entry.sequenceNumber, ins, entry.lengths);
}
Set
增加缓存数据时,需先调用edit方法,获得Editor对象,或者null(已经是edit状态时),并写入一条update日志,该条日志并不是写入缓存成功的标识。
注:diskKey为原key经过md5加密后的值。
private synchronized Editor editByDiskKey(String diskKey, long expectedSequenceNumber) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(diskKey);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER &&
(entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(diskKey);
lruEntries.put(diskKey, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(UPDATE + " " + diskKey + '\n');
journalWriter.flush();
return editor;
}
然后通过Editor对象获得文件的操作流FaultHidingOutputStream对象,该对象在操作文件出错的时候会将hasErrors变量赋值为false,该变量对最终插入数据成功与否起关键性的作用。
代码中entry.getDirtyFile(index),可能有同学有疑问,为什么是Dirty。其实这里只是作为一个临时文件,在数据写入成功后会将改文件重命名后做为正式文件。
readable表示当前Entry的数据是否被写入过了,如果是,则不能再重复写入。
public OutputStream newOutputStream(int index) throws IOException {
synchronized (LruDiskCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
FileOutputStream outputStream;
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
outputStream = new FileOutputStream(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
return NULL_OUTPUT_STREAM;
}
}
return new FaultHidingOutputStream(outputStream);
}
}
最终新增加缓存,需调用Editor对象的commit方法;
在commit时,会进行判断,如果写入成功,刚加一条clean日志(clean才表示数据插入成功了)
1
2
|
entry.readable =
true
;
journalWriter.write(CLEAN +
" "
+ entry.diskKey +
" "
+ EXPIRY_PREFIX + entry.expiryTimestamp + entry.getLengths() +
'\n'
);
|
否则做为脏数据处理,删除文件并写入删除日志。
1
|
journalWriter.write(DELETE +
" "
+ entry.diskKey +
'\n'
);
|
Delete
删除缓存时,先判断是不是edit状态,不是才能执行删除操作,并将删除记录写入日志文件。
如果同一key对应多个缓存文件,则全删。
private synchronized boolean removeByDiskKey(String diskKey) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(diskKey);
if (entry == null || entry.currentEditor != null) {
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);
}
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
journalWriter.append(DELETE + " " + diskKey + '\n');
lruEntries.remove(diskKey);
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return true;
}
其它分析:
许多操作的结尾处都会添加一个整理数据的任务,保证数据在可控范围:
private final Callable<Void> cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized (LruDiskCache.this) {
if (journalWriter == null) {
return null; // Closed.
}
trimToSize();//超过大小时删除不常用数据
if (journalRebuildRequired()) {//超过2000条日志后需要重建日志
rebuildJournal();//根据现在的数据生成新的日志文件,重命名原有的日志文件
redundantOpCount = 0;//初始化
}
}
return null;
}
};
日志文件是有限制长度的,不能随意增长:
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
有些操作会遍历valueCount,这个值表示同一个key下的多个缓存,比如一张图片可分为大中小,这时候就以valueCount来做区分.
(上述代码来源于xutils中的LruDiskCache,相对于Android官网内的DiskLruCache,它显然更加完善。)