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

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值