ImageLoader使用的DiskLruCache硬盘缓存算法

转载自:简书的排版
最近在研究ImageLoader的源码,发现一个硬盘缓存比较通用的类,这个类不属于谷歌官方却受官方亲睐,基本硬盘缓存都可以利用这个类来实现。
我们先来说一下缓存记录文件journal文件:

journal文件

作用:记录缓存的文件的行为:删除、读取、正在编辑等状态。
“`
libcore.io.DiskLruCache
1
1
1

DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY be8bdac81c12a08e15988555d85dfd2b
CLEAN be8bdac81c12a08e15988555d85dfd2b 99
READ be8bdac81c12a08e15988555d85dfd2b
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 536788f4dbdffeecfbb8f350a941eea3
“`

大家这个journal文件,
首先看前五行:

1.第一行固定常量libcore.io.DiskLruCache
2.第二行DiskLruCache的版本号,源码中为常量1
3.第三行app版本号
4.第四行标记一个key对应几个缓存文件,一般也为1.
5.第五行空行
以下为journal文件的一些规则:
6.REMOVE(删除) 、READ(读) 、DIRTY(脏)都是以 执行标记+空格+ key+空格 的规范写入
7.CLEAN (清理) 以 执行标记+空格+ key+空格+写入缓存字节 规范写入
8.REMOVE 是我们删除一条缓存文件(条目)时记录。
9.READ 是我们每读一个缓存文件(条目)时记录。
10.CLEAN 清理状态,缓存文件写入正确记录。
10.DIRTY 是缓存文件正在编辑写入时的状态,我们开始写入缓存文件时就记录为DIRTY 状态,写入完成后会紧跟着CLEAN 状态或者REMOVE状态。如果缓存的文件编辑完成记录CLEAN 状态,如果写入时出现IO异常则把缓存文件删除并且记录REMOVE状态。

以上就是所有关于journal文件的规则。

重要的全局变量

静态常量:
String JOURNAL_FILE:日志文件名
String JOURNAL_FILE_TEMP:临时日志文件名
String JOURNAL_FILE_BACKUP:备份日志文件名
Pattern LEGAL_KEY_PATTERN:key需要配置的正则表达式
全局变量:
Writer journalWriter:日志文件的操作流
LinkedHashMap lruEntries:缓存条目的链式列表
int redundantOpCount:冗余的操作数
long nextSequenceNumber:用来标识被成功提交的序号
long size :已经保存的字节大小
int fileCount:记录已经保存的文件数

初始化

构造方法


private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP);
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
this.maxFileCount = maxFileCount;
}

构造方法是私有的,说明我们不能直接new得出DiskLrucache对象。构造方法就是初始化一些传入的文件夹路径,app版本、日志临时、备份、原文件等。其中valueCount 是相同key相对应保存的文件数,maxSize是我们维护的最大字节数,maxFileCount 是我们维护的最大文件数。

open方法


public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
throws IOException

是我们唯一创建DiskLruCache的方法,抛出异常,传入我们构造方法需要的参数。

判断可能出现的错误


if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (maxFileCount <= 0) {
throw new IllegalArgumentException("maxFileCount <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}

说明一下规则:
1.maxSize最大字节数不能少于等于0.
2.maxFileCount 最大文件数不能少于等于0
3.valueCount相同key维护的文件数不能少于等于0

日志备份文件处理


File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
//如果journal文件也存在,仅需要删除备份文件 否则备份文件重命名。
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}

代码流程如下:
1.取出日志备份文件判断,如果没有日志备份文件直接下一步
2.存在备份文件,如果也存在原日志文件,删除备份文件
3.存在备份文件,如果不存在原日志文件,日志备份文件重命名为原文件

如果日志文件已经存在,对日志文件进行处理


DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
if (cache.journalFile.exists()) {
//如果日志文件存在 直接返回读取后返回当前新建的DiskLruCache对象
try {
//读日志文件
cache.readJournal();
//处理日志文件
cache.processJournal();
//创建写文件的流
cache.journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}

代码说明:
1.调用构造方法获取DiskLruCache对象。
2.判断如果存在日志文件对日志进行如下操作
3.读日志文件内容
4.处理日志文件
5.创建日志文件的写入流
6.返回DiskLruCache对象或者报异常删除文件夹

如果不存在日志文件则新建一个新的日志文件


directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
//重建日志文件
cache.rebuildJournal();

1.如果目录不存在新建目录
2.新建持有的对象
3.重建日志文件

我先给大家说下一下流程,然后再深入详细的解析日志文件的产生以及重建。

日志文件的创建管理流程

读取日志文件 readJournal()和readJournalLine(String line)

readJournal()
作用:初始化缓存条目和redundantOpCount、校验版本信息。
“`
private void readJournal() throws IOException {
//日志文件的输入流 一行行读取数据
StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
try {
//校验文件头是否异常
String magic = reader.readLine();
String version = reader.readLine();
String appVersionString = reader.readLine();
String valueCountString = reader.readLine();
String blank = reader.readLine();
if (!MAGIC.equals(magic)
|| !VERSION_1.equals(version)
|| !Integer.toString(appVersion).equals(appVersionString)
|| !Integer.toString(valueCount).equals(valueCountString)
|| !”“.equals(blank)) {
throw new IOException(“unexpected journal header: [” + magic + “, ” + version + “, ”
+ valueCountString + “, ” + blank + “]”);
}

        int lineCount = 0;
        while (true) {
            try {
                //读入日志文件的每一行进行处理
                readJournalLine(reader.readLine());
                lineCount++;
            } catch (EOFException endOfJournal) {
                break;
            }
        }
        //操作数
        redundantOpCount = lineCount - lruEntries.size();
    } finally {
        Util.closeQuietly(reader);
    }
}

“`
1.先创建StrictLineReader 对象,StrictLineReader 对象是一个封装输入流的类,调用reader.readLine()会一行行的读取数据。
2.校验日志文件前五行的正确性,如果检验不通过会抛出异常,抛出异常后会删除文件夹重建。所以每个版本的APP传入的值如果不一样,会导致日志文件删除,然后重建建立缓存文件夹,缓存文件夹的直接删除也说明,我们的文件夹必须的缓存该类文件所专属的,不能放置其他文件,以防误删。
3.读取每一行数据进行解析处理
4.记录redundantOpCount=所有操作行-有效操作行。redundantOpCount 会在执行删除、读、添加文件时自增。
5.关闭文件流

readJournalLine(String line)
“`
// 读每一行,根据每行的字符串构建Entry
private void readJournalLine(String line) throws IOException {
//找到第一个空格的位置
int firstSpace = line.indexOf(’ ‘);
//如果为-1肯定为异常
if (firstSpace == -1) {
throw new IOException(“unexpected journal line: ” + line);
}

    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    //取出 key
    if (secondSpace == -1) {
        //如果第二个空格为-1 是这样的形势
        //DIRTY 335c4c6028171cfddfbaae1a9c313c52
        //REMOVE 335c4c6028171cfddfbaae1a9c313c52
        //READ 335c4c6028171cfddfbaae1a9c313c52
        key = line.substring(keyBegin);
        if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
            //删除
            lruEntries.remove(key);
            return;
        }
    } else {
        key = line.substring(keyBegin, secondSpace);
    }
    //根据 key 取出 Entry
    Entry entry = lruEntries.get(key);
    //如果为null 就新建
    if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    }

    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
        String[] parts = line.substring(secondSpace + 1).split(" ");
        entry.readable = true;
        entry.currentEditor = null;
        entry.setLengths(parts);
    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
        entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
        // This work was already done by calling lruEntries.get().
        // 如果为READ则什么都不需要做。上面这句翻译一下就是说这里要做的工作已经在调用lruEntries.get()时做过了
        // 遇到READ其实就是再次访问该key,因此上面调用get的时候已经将其移动到最近使用的位置了
    } else {
        throw new IOException("unexpected journal line: " + line);
    }
}


下面我们逐步解析,日志文件是如何转化成缓存条目的呢?因为这个方法很重要,着重讲解:
1.取出key值

//找到第一个空格的位置
int firstSpace = line.indexOf(’ ‘);
//如果为-1肯定为异常
if (firstSpace == -1) {
throw new IOException(“unexpected journal line: ” + line);
}

    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    //取出 key
    if (secondSpace == -1) {
        //如果第二个空格为-1 是这样的形势
        //DIRTY 335c4c6028171cfddfbaae1a9c313c52
        //REMOVE 335c4c6028171cfddfbaae1a9c313c52
        //READ 335c4c6028171cfddfbaae1a9c313c52
        key = line.substring(keyBegin);
        if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
            //删除
            lruEntries.remove(key);
            return;
        }
    } else {
        key = line.substring(keyBegin, secondSpace);
    }


上面代码就是取出key的过程代码,我们知道四种状态,只有CLEAN状态存在第二个空格键,所以我们取出第一个空格键的位置firstSpace,再取出第二个空格键的位置secondSpace,如果secondSpace为-1说明是DIRTY 、REMOVE 、READ 三种状态,直接使用line.substring(keyBegin)第一个空格键到结束就能截取出key,同时如果是REMOVE就使用key索引删除缓存条目。CLEAN需要使用第一个空格键和第二个空格键完成key的截取。
2.如果不存在缓存条目就创建新的:

//根据 key 取出 Entry
Entry entry = lruEntries.get(key);
//如果为null 就新建
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}

我们知道只有REMOVE状态不会存在缓存条目,所以REMOVE状态删除之后直接reture,其他三个状态都存在缓存条目,所以,无论那种状态,我们都初始化新建一个key缓存条目。
3.对相应状态值进行处理:

if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(” “);
entry.readable = true;
entry.currentEditor = null;
entry.setLengths(parts);
} else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
entry.currentEditor = new Editor(entry);
} else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
// 如果为READ则什么都不需要做。上面这句翻译一下就是说这里要做的工作已经在调用lruEntries.get()时做过了
// 遇到READ其实就是再次访问该key,因此上面调用get的时候已经将其移动到最近使用的位置了
} else {
throw new IOException(“unexpected journal line: ” + line);
}
“`
如果是CLEAN状态,我们把缓存条目设置为已读,这说明文件完整,可以进行访问,设置为currentEditor =null,说明已经写入数据完毕,然后就是读取出文件的字节进行设置setLengths(parts)。
如果是DIRTY状态,是一种脏的状态,也可以理解为是一种正在写入数据流的编辑状态,设置当前.currentEditor = new Editor(entry)标记该缓存条目正在被编辑,其他线程不能再编辑,其后必须紧跟相同key的CLEAN或者REMOVE状态。
如果是READ,我们什么也不做。
最后是读完所有行数据后抛出异常中断循环。

处理日志文件processJournal()

作用:计算size和filecount的值。假设正在编辑状态的写入不一致,直接删除。

private void processJournal() throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor == null) {
for (int t = 0; t < valueCount; t++) {
size += entry.lengths[t];
fileCount++;
}
} else {
// 当前条目正在被编辑,删除正在编辑的文件并将currentEditor赋值为null
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
}

valueCount一般为1.
1.删除临时文件
2.编立lruEntries缓存条目,如果entry.currentEditor == null说明不在编辑状态,计算遍历相同key的所有文件大小合并到size和fileCount.
3.如果是正在编辑的状态,先设置当前编辑为null,然后删除CleanFile和DirtyFile,最后删除缓存条目。

重建日志文件

“`
private synchronized void rebuildJournal() throws IOException {
//先关闭之前的写的流
if (journalWriter != null) {
journalWriter.close();
}
//创建一个临时的写入流
Writer writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
try {
//写入一个常量
writer.write(MAGIC);
writer.write(“\n”);
//写入一个缓存版本号 默认为1
writer.write(VERSION_1);
writer.write(“\n”);
//写入APP的版本号
writer.write(Integer.toString(appVersion));
writer.write(“\n”);
//写入值计数
writer.write(Integer.toString(valueCount));
writer.write(“\n”);
//写入一个空行
writer.write(“\n”);
// 遍历Map写入日志文件
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
writer.write(DIRTY + ’ ’ + entry.key + ‘\n’);
} else {
writer.write(CLEAN + ’ ’ + entry.key + entry.getLengths() + ‘\n’);
}
}
} finally {
writer.close();
}

    if (journalFile.exists()) {
        //如果日志文件存在 就重命名为临备份日志文件 并且先把之前的日志文件删除掉
        renameTo(journalFile, journalFileBackup, true);
    }
    //备份文件重命名为日志文件
    renameTo(journalFileTmp, journalFile, false);
    //删除备份文件
    journalFileBackup.delete();
    //新建日志文件的写入流
    journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
}


重建日志文件步骤如下:
1.新建一个临时文件流。
2.把五行固定格式写入,然后遍历现有的缓存lruEntries列表。
3.关闭流
4.存在日志文件,就把之前的日志文件重命名为备份文件。
5.临时文件再改名为正式文件
6.不出现异常就删除备份文件
7.创建流
处罚重建日志文件有两个地方:
1.初始化的时候检验出现异常等。
2.符合如下条件,因为日志文件大庞大,进行删减一些无用记录

private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
“`

ImageLoader中重要的几个内部类

Entry缓存条目

Paste_Image.png
上面就是Entry类的构造以及函数方法,因为常量比较简单 ,这里就不说了,这个类,就是把缓存条目记录起来,进行快速检索。因为相同key是支持多个文件的,所以这里的文件数量是数组,而且文件想以key.index等方式命名存储的,以完全写入的清洁文件为列子:

public File getCleanFile(int i) {
return new File(directory, key + "." + i);
}

就是通过名字key和index来进行检索文件的。

Editor编辑对象

说到Editor对象,我们先来看看以下使用的代码:
“`
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}

    OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
    boolean copied = false;
    try {
        copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
    } finally {
        IoUtils.closeSilently(os);
        if (copied) {
            editor.commit();
        } else {
            editor.abort();
        }
    }
    return copied;
}

“`
通过代码我们知道,Editor其实就是用来封装,记录写入文件流的编辑过程,文件流正常写入,就提交Clean,失败就强制删除,其实就是一个事务处理机制。

Paste_Image.png
常量:
boolean committed:是否提交完成
boolean hasErrors:是否存在异常
boolean[] written:记录是否需要写
Entry entry编辑的缓存条目
主要方法有:

public InputStream newInputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
return null;
}
try {
return new FileInputStream(entry.getCleanFile(index));
} catch (FileNotFoundException e) {
return null;
}
}
}

获取一个输入流,输入流文件以缓存条目的entry.getCleanFile(index) 完整文件命名。

public OutputStream newOutputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
//如果还没有被提交过
if (!entry.readable) {
//设置编辑类的 写入初始值为true
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);
}
}

获取一个输出流,!entry.readable这个条件说明之前这个文件从来没有被写入完整过,把写入权限设置为true,
然后先取脏文件的输入出流,封装成FaultHidingOutputStream这个对象,这个对象比较简单,就是出现IO异常不抛出,设置hasErrors为true.

public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // The previous entry is stale.
} else {
completeEdit(this, true);
}
committed = true;
}

提交事务的方法,没有错误,直接调用completeEdit(this, true),有出现IO异常就completeEdit(this, false),并且删除这个缓存条目。
而abort()事务回掉,其实就是调用DiskLruCache方法的completeEdit(this, false)。

我们先放下completeEdit(this, false)这个方法,我们来聊聊DiskLruCache中的edit(String key)方法,也就是我们获取到Editor事务的方法。
“`
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
            || entry.sequenceNumber != expectedSequenceNumber)) {
        return null; // Snapshot is stale.
    }
    if (entry == null) {
        entry = new Entry(key);
        lruEntries.put(key, 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(DIRTY + ' ' + key + '\n');
    journalWriter.flush();
    return editor;
}

“`
上面两个方法,最终都会调用edit(String key, long expectedSequenceNumber),默认序列号为-1,我们可以看到,第一步是先检验日志文件的流是否关闭,第二部是检验Key是否匹配Pattern.compile(“[a-z0-9_-]{1,64}”)的正则表达式,第三部检验序列号,当我们只调用单个key不传入序列号是检验序列号是恒成立的,我们需要关注的是(entry == null|| entry.sequenceNumber != expectedSequenceNumber)),这个方法主要是确保,我们的Snapshot 是最新的。
然后下面就是创建Entry,Editor ,并且保证相同key,是不会同时写入数据的,也就是说entry.currentEditor代表我们的文件流正在写入,其中一个线程正在写入,另一个线程是无法获取到Editor的,最后写入日志文件。

下面我们来说说DiskLruCache中的 completeEdit(Editor editor, boolean success)方法,这个方法是Editor做提交事务后进行事务回滚和完成事务调用的。

Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}

判断正在编辑的Editor是否是正操作的。

if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}

entry.readable为true说明不是首次提交,entry.readable为false说明是首次提交,也即是满足,写入成功,但是文件标记为不能写,或者dirty文件不存在,就强制回滚事务,但是一般不会触发。


for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
//提交成功
if (success) {
if (dirty.exists()) {
//如果是成功的 就把临时文件转成
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
fileCount++;
}
} else {
//提交失败直接删除掉
deleteIfExists(dirty);
}
}

遍历获取dirty文件,一般情况valueCount为1,就是说提交失败删除dirty文件,提交成功就重命名文件,并把size,fileCount值做统计。
然后:

redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
journalWriter.flush();

这一段代码就是把事务置为null,并且判断是写入日志文件Remove状态还是clean状态。
最后

if (size > maxSize || fileCount > maxFileCount || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}

判断文件大小 ,文件数量,以及日志文件是否超标,超标就启动任务重构日志文件。

关于Editor,既可以理解为事务的方法就说完了。

Snapshot快照对象

我们先来看看,ImageLoader对快照的使用:

public File get(String imageUri) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = cache.get(getKey(imageUri));
return snapshot == null ? null : snapshot.getFile(0);
} catch (IOException e) {
L.e(e);
return null;
} finally {
if (snapshot != null) {
snapshot.close();
}
}
}

从这个方法中,我们知道,其实就是取出快照,然后返回文件流。那我们的快照做了什么处理呢。其实就在通过key,索引到一些数据,然后把数据封装到Snapshot 中,我们来看看取快照的方法cache.get(getKey(imageUri)):
“`
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}

    if (!entry.readable) {
        return null;
    }

    // Open all streams eagerly to guarantee that we see a single published
    // snapshot. If we opened streams lazily then the streams could come
    // from different edits.
    File[] files = new File[valueCount];
    InputStream[] ins = new InputStream[valueCount];
    try {
        File file;
        for (int i = 0; i < valueCount; i++) {
            file = entry.getCleanFile(i);
            files[i] = file;
            ins[i] = new FileInputStream(file);
        }
    } catch (FileNotFoundException e) {
        // A file must have been deleted manually!
        for (int i = 0; i < valueCount; i++) {
            if (ins[i] != null) {
                Util.closeQuietly(ins[i]);
            } else {
                break;
            }
        }
        return null;
    }

    redundantOpCount++;
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
        executorService.submit(cleanupCallable);
    }

    return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
}

“`
流程很简单:
1.检验
2.从缓存条目中取出文件和文件流数组,
3.把操作写入日志文件
4.达到某程序的冗余数后重建的日志文件
5.封装new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
也就是说,一切就是为了把我们需要的数据装到快照里面。
结构如下:

!
Paste_Image.png

也不复杂 这里我就不说了,其他还有的方法,各位可以参考源码。

猜你喜欢

转载自blog.csdn.net/u010782846/article/details/78040252