android菜鸟笔记

本地缓存DiskLruCache源码分析

2019-09-26  本文已影响0人  李发糕

本文详细分析Glide及OKHttp所用的文件缓存类DiskLruCache~~ 分析顺序及笔者看代码的顺序

首先我们看一下注释

/*
     * This cache uses a journal file named "journal". A typical journal file
     * looks like this:
     * 此缓存使用一个叫journal的日志文件,格式如下
     * 
     *     libcore.io.DiskLruCache //头部标示
     *     1 //缓存版本
     *     100 //应用版本
     *     2 //valuecount 及缓存中一个缓存包含几个文件 比如Glide只有一个图片文件,而OKHttp有两个 一个存header,一个存body
     *          //空行
     *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
     *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
     *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
     *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
     *     DIRTY 1ab96a171faeeee38496d8b330771a7a
     *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
     *     READ 335c4c6028171cfddfbaae1a9c313c52
     *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
     *
     * 之后的每一行代表对一个缓存entry的状态记录 状态 key 可选的详细状态值
     * 如下状态:
     *   o DIRTY 状态 代表一个entry正在被创建或者更新 每一个成功的DIRTY操作会紧跟着一个CLEAN或者是REMOVE
     *       操作。如果没有跟着匹配的CLEAN或者REMOVE操作,代表这是个临时文件,可能需要删除
     *   o CLEAN 状态 代表一个缓存entry被成功发布了并且可读,后面会拼接该缓存每个文件的长度
     *   o READ 读LRU
     *   o REMOVE entry被移除了
     *
     * 日志文件附加到缓存操作发生时。有时可能会因删除冗余行而受到压缩。 压缩期间将使用名为“journal.tmp”的临时            * 文件; 如果在打开缓存时该文件存在,则应删除该文件。
     */

注释中多次提到了entry,实际上这也是我们外部调用时存取的入口,我们看一下

private final class Entry {
  private final String key;//key

  /** Lengths of this entry's files. */
  private final long[] lengths;//缓存文件长度数组

  /** Memoized File objects for this entry to avoid char[] allocations. */
  File[] cleanFiles;//完成写入的文件
  File[] dirtyFiles;//正在写入的文件

  /** True if this entry has ever been published. */
  private boolean readable;//是否可读

  /** The ongoing edit or null if this entry is not being edited. */
  private Editor currentEditor;//当前的editor,先不管

  /** The sequence number of the most recently committed edit to this entry. */
  private long sequenceNumber;//最近提交编辑给这个entry的序列号

  //下面看一下构造方法
  private Entry(String key) {
    this.key = key;
    this.lengths = new long[valueCount];
    cleanFiles = new File[valueCount];
    dirtyFiles = new File[valueCount];

    // The names are repetitive so re-use the same builder to avoid allocations.
    StringBuilder fileBuilder = new StringBuilder(key).append('.');
    int truncateTo = fileBuilder.length();
    for (int i = 0; i < valueCount; i++) {
        fileBuilder.append(i);
        cleanFiles[i] = new File(directory, fileBuilder.toString());//获取对应的文件引用
        fileBuilder.append(".tmp");
        dirtyFiles[i] = new File(directory, fileBuilder.toString());//获取临时文件引用
        fileBuilder.setLength(truncateTo);//这里可以一个builder在循环中多次利用,相当于删除了之前拼接的内容
    }
  }

  public String getLengths() throws IOException {//获取缓存中每个文件长度的字符转
    StringBuilder result = new StringBuilder();
    for (long size : lengths) {
      result.append(' ').append(size);
    }
    return result.toString();
  }

  /** Set lengths using decimal numbers like "10123". */
  private void setLengths(String[] strings) throws IOException {//设置长度给lengths数组
    if (strings.length != valueCount) {
      throw invalidLengths(strings);
    }

    try {
      for (int i = 0; i < strings.length; i++) {
        lengths[i] = Long.parseLong(strings[i]);
      }
    } catch (NumberFormatException e) {
      throw invalidLengths(strings);
    }
  }

  private IOException invalidLengths(String[] strings) throws IOException {
    throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
  }

  public File getCleanFile(int i) {//获取cleanFile的引用
    return cleanFiles[i];
  }

  public File getDirtyFile(int i) {//获取dirtyFile的引用
    return dirtyFiles[i];
  }
}

可以看到Entry中持有了我们缓存文件的引用,但是并没有get put 方法,却有一个Editor对象,顾名思义,编辑器。那么我们看一下这个Eidtor是干啥的

public final class Editor {
  private final Entry entry;//这里可以看到,编辑器也持有Entry的对象,这个就是它正在编辑的对象
  private final boolean[] written;//缓存中文件是否可写
  private boolean committed;

  private Editor(Entry entry) {
    this.entry = entry;
    this.written = (entry.readable) ? null : new boolean[valueCount];
  }

  //获取读文件的输入流 防止CLEAN文件在这里被写,所以只能返回输入流
  private 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));//如果存在并且可读,返回clean文件的输入流
      } catch (FileNotFoundException e) {
        return null;
      }
    }
  }

  。。。
    
    //获取可写的File
  public File getFile(int index) throws IOException {
    synchronized (DiskLruCache.this) {
      if (entry.currentEditor != this) {
          throw new IllegalStateException();
      }
      if (!entry.readable) {//之前有没有发布过
          written[index] = true;
      }
      File dirtyFile = entry.getDirtyFile(index);//返回写入中的dirtyFile
      if (!directory.exists()) {
          directory.mkdirs();
      }
      return dirtyFile;
    }
  }

  /** Sets the value at {@code index} to {@code value}. */
  public void set(int index, String value) throws IOException {
    Writer writer = null;
    try {
      OutputStream os = new FileOutputStream(getFile(index));
      writer = new OutputStreamWriter(os, Util.UTF_8);//写入dirty文件
      writer.write(value);
    } finally {
      Util.closeQuietly(writer);
    }
  }

  /**
   * Commits this edit so it is visible to readers.  This releases the
   * edit lock so another edit may be started on the same key.
   */
  public void commit() throws IOException {//提交
    // The object using this Editor must catch and handle any errors
    // during the write. If there is an error and they call commit
    // anyway, we will assume whatever they managed to write was valid.
    // Normally they should call abort.
    completeEdit(this, true);//提交编辑
    committed = true;//已经提交
  }

  /**
   * Aborts this edit. This releases the edit lock so another edit may be
   * started on the same key.
   */
  public void abort() throws IOException {//中断
    completeEdit(this, false);
  }

  public void abortUnlessCommitted() {
    if (!committed) {
      try {
        abort();
      } catch (IOException ignored) {
      }
    }
  }
}

下面我们看一下上面提及的方法

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()) {//不存在dirty文件,那就没必要修改了
        editor.abort();//终止这次修改
        return;
      }
    }
  }

  for (int i = 0; i < valueCount; i++) {//循环这个缓存的文件
    File dirty = entry.getDirtyFile(i);
    if (success) {//成功
      if (dirty.exists()) {//存在dirty
        File clean = entry.getCleanFile(i);
        dirty.renameTo(clean);//使用dirty替换旧的clean文件
        long oldLength = entry.lengths[i];
        long newLength = clean.length();
        entry.lengths[i] = newLength;
        size = size - oldLength + newLength;//修改长度
      }
    } else {
      deleteIfExists(dirty);//失败,删除dirty
    }
  }

  redundantOpCount++;//计数器++ ,暂时不知道他是啥没关系
  entry.currentEditor = null; //置空编辑器 提交过了就不能再次编辑了
  if (entry.readable | success) {//如果是提交并成功
    entry.readable = true;
    journalWriter.append(CLEAN);
    journalWriter.append(' ');
    journalWriter.append(entry.key);
    journalWriter.append(entry.getLengths());
    journalWriter.append('\n');

    if (success) {
      entry.sequenceNumber = nextSequenceNumber++;//操作序列号++
    }
  } else {//否则说明提交失败了,记录删除dirty了
    lruEntries.remove(entry.key);
    journalWriter.append(REMOVE);
    journalWriter.append(' ');
    journalWriter.append(entry.key);
    journalWriter.append('\n');
  }
  journalWriter.flush();//flushwriter

  if (size > maxSize || journalRebuildRequired()) {
    executorService.submit(cleanupCallable);//整理重构日志记录
  }
}

那么我们在外部如何获取Editor来操作呢

首先LruCache提供了一个get方法

public Editor edit(String key) throws IOException {
  return edit(key, ANY_SEQUENCE_NUMBER);//entry序列号,这个方法不需要
}

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
  checkNotClosed();
  Entry entry = lruEntries.get(key);//这就是一个entry的lru内存缓存
  if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
      || entry.sequenceNumber != expectedSequenceNumber)) {
    return null; // Value is stale.
  }
  if (entry == null) {//新的key
    entry = new Entry(key);//新建一个entry
    lruEntries.put(key, entry);//放入缓存
  } else if (entry.currentEditor != null) {//这个entry正在被编辑
    return null; // return
  }

  Editor editor = new Editor(entry);//实例用于操作该entry的editor
  entry.currentEditor = editor;//绑定

  journalWriter.append(DIRTY);//写入dirty日志
  journalWriter.append(' ');
  journalWriter.append(key);
  journalWriter.append('\n');
  journalWriter.flush();
  return editor;
}

另外还有一个内部类Value我们看一下

public final class Value {//当前entry的一个快照
  private final String key;
  private final long sequenceNumber;
  private final long[] lengths;
  private final File[] files;

    private Value(String key, long sequenceNumber, File[] files, long[] lengths) {
    this.key = key;
    this.sequenceNumber = sequenceNumber;
    this.files = files;
    this.lengths = lengths;
  }

  /**
   * Returns an editor for this snapshot's entry, or null if either the
   * entry has changed since this snapshot was created or if another edit
   * is in progress.
   */
  public Editor edit() throws IOException {
    return DiskLruCache.this.edit(key, sequenceNumber);//直接编辑sequenceNumber对应的entry
  }

  public File getFile(int index) {
      return files[index];
  }

  /** Returns the string value for {@code index}. */
  public String getString(int index) throws IOException {
    InputStream is = new FileInputStream(files[index]);
    return inputStreamToString(is);
  }

  /** Returns the byte length of the value for {@code index}. */
  public long getLength(int index) {
    return lengths[index];
  }
}

最后我们看一下整体Lrucache的流程

public final class DiskLruCache implements Closeable {//首先看一下全局变量
  static final String JOURNAL_FILE = "journal"; //日志文件名
  static final String JOURNAL_FILE_TEMP = "journal.tmp"; //日志临时文件名
  static final String JOURNAL_FILE_BACKUP = "journal.bkp"; //日志备份文件名
  static final String MAGIC = "libcore.io.DiskLruCache"; //日志文件头
  static final String VERSION_1 = "1"; //版本号
  static final long ANY_SEQUENCE_NUMBER = -1; //空序列号
  private static final String CLEAN = "CLEAN"; 
  private static final String DIRTY = "DIRTY";
  private static final String REMOVE = "REMOVE";
  private static final String READ = "READ";
  private final File directory; //缓存所在文件夹
  private final File journalFile; //日志文件
  private final File journalFileTmp; 
  private final File journalFileBackup;
  private final int appVersion;
  private long maxSize;//日志文件最大值
  private final int valueCount;//当前缓存中一个缓存包含几个文件
  private long size = 0;//日志文件当前长度
  private Writer journalWriter;//日志文件writer
  private final LinkedHashMap<String, Entry> lruEntries =
      new LinkedHashMap<String, Entry>(0, 0.75f, true);//缓存文件的引用在内存中的缓存 使用lru
  private int redundantOpCount;//多余的操作的数量

  /**
   * To differentiate between old and current snapshots, each entry is given
   * a sequence number each time an edit is committed. A snapshot is stale if
   * its sequence number is not equal to its entry's sequence number.
   */
  private long nextSequenceNumber = 0;//序列号,每次提交成功会自增,用于判断当前快照是否过期

  /** This cache uses a single background thread to evict entries. */
  final ThreadPoolExecutor executorService =
      new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
  //线程池,执行一些操作用
  private final Callable<Void> cleanupCallable = new Callable<Void>() {//cleanup的操作
    public Void call() throws Exception {
      synchronized (DiskLruCache.this) {
        if (journalWriter == null) {
          return null; // Closed.
        }
        trimToSize();//压缩尺寸
        if (journalRebuildRequired()) {//还需要rebuild
          rebuildJournal(); //rebuild 
          redundantOpCount = 0; //rebuild完了,多余的操作记录数置空
        }
      }
      return null;
    }
  };

  。。。

    // 根据文件夹及版本号,maxsize等打开一个缓存
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {

    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);//实例一个DiskLruCache
    if (cache.journalFile.exists()) {//存在日志文件
      try {
        cache.readJournal();//读取日志
        cache.processJournal();//加工日志
        return cache;//返回
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();//删除
      }
    }

    // Create a new empty cache.
    directory.mkdirs();//不存在之前的日志文件 先创建文件夹
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);//生成实例
    cache.rebuildJournal();//重构日志
    return cache;//返回
  }

  //下面先看看读取日志的方法
  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();//rebuild
      } else {
        journalWriter = new BufferedWriter(new OutputStreamWriter(
            new FileOutputStream(journalFile, true), Util.US_ASCII));//获取对日志文件的写入writer
      }
    } 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)) {//如果调用了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);
    }
  }

  /**
   * Computes the initial size and collects garbage as a part of opening the
   * cache. Dirty entries are assumed to be inconsistent and will be deleted.
   */
  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];
        }
      } else {
        entry.currentEditor = null;
        for (int t = 0; t < valueCount; t++) {
          deleteIfExists(entry.getCleanFile(t));
          deleteIfExists(entry.getDirtyFile(t));
        }
        i.remove();
      }
    }
  }

  /**
   * Creates a new journal that omits redundant information. This replaces the
   * current journal if it exists.
   */
  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));
  }

  private static void deleteIfExists(File file) throws IOException {
    if (file.exists() && !file.delete()) {
      throw new IOException();
    }
  }

  private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
    if (deleteDestination) {
      deleteIfExists(to);
    }
    if (!from.renameTo(to)) {
      throw new IOException();
    }
  }

  /**
   * Returns a snapshot of the entry named {@code key}, or null if it doesn't
   * exist is not currently readable. If a value is returned, it is moved to
   * the head of the LRU queue.
   */
  public synchronized Value get(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }

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

    for (File file : entry.cleanFiles) {
        // A file must have been deleted manually!
        if (!file.exists()) {
            return null;
        }
    }

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

    return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
  }

  /**
   * Returns an editor for the entry named {@code key}, or null if another
   * edit is in progress.
   */
  public Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

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

  /** Returns the directory where this cache stores its data. */
  public File getDirectory() {
    return directory;
  }

  /**
   * Returns the maximum number of bytes that this cache should use to store
   * its data.
   */
  public synchronized long getMaxSize() {
    return maxSize;
  }

  /**
   * Changes the maximum number of bytes the cache can store and queues a job
   * to trim the existing store, if necessary.
   */
  public synchronized void setMaxSize(long maxSize) {
    this.maxSize = maxSize;
    executorService.submit(cleanupCallable);
  }

  /**
   * Returns the number of bytes currently being used to store the values in
   * this cache. This may be greater than the max size if a background
   * deletion is pending.
   */
  public synchronized long size() {
    return size;
  }

  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.append(CLEAN);
      journalWriter.append(' ');
      journalWriter.append(entry.key);
      journalWriter.append(entry.getLengths());
      journalWriter.append('\n');

      if (success) {
        entry.sequenceNumber = nextSequenceNumber++;
      }
    } else {
      lruEntries.remove(entry.key);
      journalWriter.append(REMOVE);
      journalWriter.append(' ');
      journalWriter.append(entry.key);
      journalWriter.append('\n');
    }
    journalWriter.flush();

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

  /**
   * We only rebuild the journal when it will halve the size of the journal
   * and eliminate at least 2000 ops.
   */
  private boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold //
        && redundantOpCount >= lruEntries.size();
  }

  /**
   * Drops the entry for {@code key} if it exists and can be removed. Entries
   * actively being edited cannot be removed.
   *
   * @return true if an entry was removed.
   */
  public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    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(REMOVE);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');

    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

  /** Returns true if this cache has been closed. */
  public synchronized boolean isClosed() {
    return journalWriter == null;
  }

  private void checkNotClosed() {
    if (journalWriter == null) {
      throw new IllegalStateException("cache is closed");
    }
  }

  /** Force buffered operations to the filesystem. */
  public synchronized void flush() throws IOException {
    checkNotClosed();
    trimToSize();
    journalWriter.flush();
  }

  /** Closes this cache. Stored values will remain on the filesystem. */
  public synchronized void close() throws IOException {
    if (journalWriter == null) {
      return; // Already closed.
    }
    for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
      if (entry.currentEditor != null) {
        entry.currentEditor.abort();
      }
    }
    trimToSize();
    journalWriter.close();
    journalWriter = null;
  }

  private void trimToSize() throws IOException {
    while (size > maxSize) {
      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
      remove(toEvict.getKey());
    }
  }

  /**
   * Closes the cache and deletes all of its stored values. This will delete
   * all files in the cache directory including files that weren't created by
   * the cache.
   */
  public void delete() throws IOException {
    close();
    Util.deleteContents(directory);
  }

  private static String inputStreamToString(InputStream in) throws IOException {
    return Util.readFully(new InputStreamReader(in, Util.UTF_8));
  }
}
上一篇下一篇

猜你喜欢

热点阅读