LruDiskCache要点--不可不用的磁盘缓存工具类

来源:互联网 发布:淘宝批量上架软件 编辑:程序博客网 时间:2024/06/03 20:46

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,它显然更加完善。)



0 0
原创粉丝点击