DiskLruCache源码分析

来源:互联网 发布:安卓7.0 java模拟器 编辑:程序博客网 时间:2024/05/21 07:50

简介

上篇文章介绍了LRUCache它的思想是把一部分常用的对象存在内存里,以便下次使用的时候快速提取。
但是内存容量也就三G两G的,早期的或者低端一点的也就几百M,能分给自己的APP用来缓存数据的空间实在不多。
但是别忘了,我们还有Disk这个后花园。磁盘缓存的速度虽然不然不及内存缓存,但是容量很大,是典型的用时间换空间思想。

简单使用

DiskLruCache mDiskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);String key = generateKey(url);DiskLruCache.Editor editor = mDiskLruCache.edit(key);OuputStream os = editor.newOutputStream(0);DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);if (snapShot != null) {    InputStream is = snapShot.getInputStream(0);}

valueCount 是每一个key对应的value文件有几个

Journal文件

Journal是一个日志文件,一个典型的Journal文件如下:

libcore.io.DiskLruCache
1
100
2

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

前五行组成了日志文件的文件头

第一行 固定字符串libcore.io.DiskLruCache,也就是文件的魔数。
第二行 是DiskLruCache的版本号,源码中为常量1
第三行 是app的版本号,自己在open方法里传入
第四行 valueCount,每个key对应几个缓存文件
第五行 空行

接下来看起来乱乱的就是一条条操作记录了,
每一行由状态、key、如果key对应多个缓存文件且它是CLEAN的会依次列出缓存文件的大小。

DIRTY表示这个entry正在被写入,如果写入成功后边会跟一条CLEAN记录。
如果写入失败,后边会跟一条REMOVE记录。
CLEAN 表示缓存写好了,后边还会跟多个缓存文件的长度
READ get方法get一次,也就是读取一次,就写一个READ
REMOVE 删除之后写

数据结构

lruEntries里存储了key和Entry,Entry里存储了关于文件的信息。
key和文件的状态存储在日志文件里,和日志文件同级的的真正缓存的文件。
这样lruEntries放在内存里,存储所有关于缓存的信息,同LRUCache一样,它也是LinkHashMap
所以也实现了LRU算法,在达到内存上限之后,删除掉近期最少使用的文件。

open

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)      throws IOException {    // If a bkp file exists, use it instead.    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);    if (backupFile.exists()) {      File journalFile = new File(directory, JOURNAL_FILE);      // If journal file also exists just delete backup file.      if (journalFile.exists()) {        backupFile.delete();      } else {        renameTo(backupFile, journalFile, false);      }    }    // Prefer to pick up where we left off.    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);    if (cache.journalFile.exists()) {      try {        cache.readJournal();        cache.processJournal();        return cache;      } catch (IOException journalIsCorrupt) {        cache.delete();      }    }    // Create a new empty cache.    directory.mkdirs();    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);    cache.rebuildJournal();    return cache;  }

如果journal的备份文件已经存在,就去new一个journal文件,如果journal文件也已经存在,那么就删除备份,
如果journal文件不存在,那么就将备份文件变成journal文件,也就是重命名。

如果journal的备份文件不存在,那么说明该路径没有任何缓存,那么就创建一个空的cache,并调用rebuildJournal方法

rebuildJournal方法

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");      writer.write(VERSION_1);      writer.write("\n");      writer.write(Integer.toString(appVersion));      writer.write("\n");      writer.write(Integer.toString(valueCount));      writer.write("\n");      writer.write("\n");      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));  }

创建journalFileTmp文件,并将其前5行写好。遍历lruEntries并将Entry当前的状态写入日志文件。
如果journalFile已经存在,那么就做个备份。
并将刚才的临时日志文件journalFileTmp重命名为日志文件journalFile。
然后删除备份日志文件。
为什么这么做呢?

readJournal

如果journalFile存在,那么首先调用了readJournal方法

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();      // If we ended on a truncated line, rebuild the journal before appending to it.      if (reader.hasUnterminatedLine()) {        rebuildJournal();      } else {        journalWriter = new BufferedWriter(new OutputStreamWriter(            new FileOutputStream(journalFile, true), Util.US_ASCII));      }    } finally {      Util.closeQuietly(reader);    }  }  private void readJournalLine(String line) throws IOException {    int firstSpace = line.indexOf(' ');    if (firstSpace == -1) {      throw new IOException("unexpected journal line: " + line);    }    int keyBegin = firstSpace + 1;    int secondSpace = line.indexOf(' ', keyBegin);    final String key;    if (secondSpace == -1) {      key = line.substring(keyBegin);      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {        lruEntries.remove(key);        return;      }    } else {      key = line.substring(keyBegin, secondSpace);    }    Entry entry = lruEntries.get(key);    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().    } else {      throw new IOException("unexpected journal line: " + line);    }  }

进来之后首先校验文件格式对不对,也就是验证是不是标准的日志文件。
然后调用了readJournalLine方法,我们分析过日志文件它确实是每个记录单独一行。
首先从日志文件中把key读出来。
如果当前key对应的记录是REMOVE,那么就从lruEntries中remove掉。
如果lruEntries中还没有此条记录,那么就new一个Entry放进去。
如果是CLEAN状态,说明没有对象在操作这条记录。
如果是DIRTY状态,说明正在被编辑,那么给它赋值一个编辑器。
如果是READ状态,那么什么都不操作。
这样内存中的lruEntries就整体把控了当前缓存的情况。

最后,读取过程中如果发现journal文件有问题,则重建journal文件。
没有问题的话,初始化journalWriter,并关闭reader。

然后在open方法中又调用了processJournal方法

经过以上调用,之后journal文件、lruEntries、以及size就都初始化好了。

写入缓存

一开始的时候写缓存是这样调用的

String key = generateKey(url);DiskLruCache.Editor editor = mDiskLruCache.edit(key);OuputStream os = editor.newOutputStream(0);

也就是先调用了edit方法

edit

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;  }

首先检查journalWriter有没有关闭以及key字符的合法性
然后去lruEntries里查找key对应的Entry,如果有就直接使用,如果没有就new一个。
然后new一个Editor给这个Entry
并在日志文件中写入DIRTY标志,标识此Entry正在被编辑。
最后返回这个Editor

然后调用了Editor的newOutputStream方法,拿到一个输出流。

newOutputStream

public OutputStream newOutputStream(int index) throws IOException {     if (index < 0 || index >= valueCount) {       throw new IllegalArgumentException("Expected index " + index + " to "               + "be greater than 0 and less than the maximum value count "               + "of " + valueCount);     }     synchronized (DiskLruCache.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);     }   }

创建一个输出流,并将流写入dirtyFile这个临时文件里。
dirtyFile通过Entry的getDirtyFile创建,它的命名规则是key.index.tmp
通过输出流写完文件之后,调用commit方法。

commit

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

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {    Entry entry = editor.entry;    if (entry.currentEditor != editor) {      throw new IllegalStateException();    }    // If this edit is creating the entry for the first time, every index must have a value.    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;        }      }    }    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;        }      } else {        deleteIfExists(dirty);      }    }    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();    if (size > maxSize || journalRebuildRequired()) {      executorService.submit(cleanupCallable);    }  }

如果成功
把dirtyFile变成cleanFile,并重新计算size。
然后把这个entry设置为可读,并写入一条CLEAN的操作日志。
如果失败
就把dirtyFile删除,然后把它从lruEntries中移除,并写入一条REMOVE的操作日志。
这样entry文件写好了,日志也写好了,这次缓存也就做好了。

读取

DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);if (snapShot != null) {  InputStream is = snapShot.getInputStream(0);}

那么首先看get方法

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.    InputStream[] ins = new InputStream[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) {          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, ins, entry.lengths);  }

同样是检查journalWriter有没有关闭,以及key的合法性。
然后new出valueCount个输入流。
向日志文件中写入一条READ日志。
最后封装一个Snapshot返回
拿到输入流,就可以读入文件了。

0 0