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](http://upload-images.jianshu.io/upload_images/3161886-1503360c8da2a13a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容