Android之DiskLruCache源码解析

转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258
本文出自:【顾林海的博客】

个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
在这里插入图片描述

前言

Andrpod的DiskLruCache是用于磁盘缓存的一套解决框架,虽然比较老,但也是一款比较经典的框架,阅读它的源码可以学习到关于缓存方面(磁盘)的一些知识,这套框架是由大神jakeWharton编写,相信大家对jakeWharton大神一点都不陌生吧,除了DiskLruCache,他还编写了Retrofit、ButterKnife、Okhttp等一些非常出名的开源框架。当然网上基于DiskLruCache讲解的文章也是很多的,比如 鸿洋的《Android DiskLruCache 源码解析 硬盘缓存的绝佳方案》和郭霖的《Android DiskLruCache完全解析,硬盘缓存的最佳方案》 。承接大神的文章,并结合自己对DiskLruCache理解,对它的源码进行学习和讲解,讲解不到之处还往各路看管多多保函。

获取DiskLruCache实例

阅读源码最忌讳的就是直接冲进源码中,漫无目的的看,这样的话效果微乎其微,按照使用习惯,分析先从实例的获取,由于DiskLruCache类的构造器是私有的,因此,在外部我们不能通过new获取DiskLruCache的实例。

private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize)

只能通过以下方式获取DiskLruCache的实例:

DiskLruCache diskLruCache = DiskLruCache.open(directory, appVersion, valueCount, maxSize);

源码如下:

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

open方法的四个参数说明如下:

  • directory:指定数据的缓存地址。
  • appVersion:当前应用的版本号。
  • valueCount:指定一个key对应缓存的文件数。
  • maxSize:最多缓存的字节数。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
        throw new IllegalArgumentException("valueCount <= 0");
    }

    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
        File journalFile = new File(directory, JOURNAL_FILE);
        if (journalFile.exists()) {
            backupFile.delete();
        } else {
            renameTo(backupFile, journalFile, false);
        }
    }

    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
        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();
        }
    }
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
}

在获取DiskLruCache实例的方法中,一开始对maxSize和valueCount进行判断,如果小于等于0抛出异常,往下走,JOURNAL_FILE_BACKUP是一个字符串为“journal.bkp”的字符串常量,JOURNAL_FILE是一个字符串为"journal"的字符串常量,先是获取缓存目录下的journal.bkp的文件,如果这个文件存在,再获取journal文件,如果journal文件存在,就将journal.bkp文件删除掉,反之如果journal文件不存在就将journal.bkp文件重命名为journal。接着就是通过new获取DiskLruCache的实例,实例化时对相关的参数进行初始化。
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
  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;
}


初始化中的directory、appVersion、valueCount和maxSize参数的含义上面已经提过了,剩余三个参数分别是获取journal、journal.tmp和journal.bkp文件,这里暂且不管,继续回到上面的open方法中,DiskLruCache的实例获取完毕后,判断journal文件是否存在,执行cache.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();
  } finally {
    Util.closeQuietly(reader);
  }
}

在readJournal方法中,读取journal文件中的内容,期初读取journal文件的前5行内容,分别是magic、version、appVersionString、valueCountString和blank,MAGIC是一个字符串为"libcore.io.DiskLruCache"的字符串常量,通过判断journal文件第一行magic与MAGIC是否相同,如果不相同,抛出异常,也就是说在journal文件中第一行是固定的字符串为"libcore.io.DiskLruCache";VERSION_1是一个字符串为"1"的字符串常量,通过与journal文件第二行version判断,如果journal文件第二行version不为1抛出异常,也就是说在journal文件第二行DiskLruCache的版本固定为1; 第三行appVersionString代表我们的应用程序的版本,传入的appVersion如果与journal文件中的版本不一致,就会抛出异常;第四行valueCountString对应我们传入的valueCount(每个key对应几个文件),如果传入的valueCount与journal文件中的第四行不一致就会抛出异常;第五行blank 如果不为空抛出异常。总结说journal文件前五行内容如下:
  • 第一行固定字符串libcore.io.DiskLruCache
  • 第二行是DiskLruCache的版本号,固定为1
  • 第三行是app的版本号
  • 第四行是每个key对应几个文件
  • 第五行为空


    journal文件的前五行内容确定后,通过while循环读取journal剩下的内容,当读到文件尾时退出循环,每次循环通过readJournalLine方法对读取到的journal文件中每行内容进行处理,readJournalLine方法实现如下:

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方法前,我们先将journal文件的内容贴出来,按照journal文件内容来讲解:
libcore.io.DiskLruCache
1
100
2

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


前面5行内容已经讲解过了,接下来重点在除前5行外的内容,这些内容是记录着我们的操作信息,DIRTY代表的是正在写入,写入成功后会再写入一行CLEAN,CLEAN记录后面的两个数字(这些数字的个数是与journal文件第四行的key一致,代表一个key对应多个文件),这些数字代表文件的长度;如果写入失败会增加一行REMOVE记录,收到remove(key)也会增加一条REMOVE记录;READ记录的是读取的记录。 现在我们来讲讲readJournalLine方法的实现,先是从每行的内容中获取key(诸如335c4c6028171cfddfbaae1a9c313c52 ),如果是标记为REMOVE的话从lruEntries中移除key相关的Entry信息,其余的话将key相关的Entry放入lruEntries中,其中针对CLEAN和DIRIY的entry进行相应的设置,具体设置的作用,我们后面讲。到这里readJournalLine方法讲解结束,回到readJournal方法中,while循环遍历结束,redundantOpCount记录下没用的记录条数。readJournal方法也讲解完毕,继续回到open方法,调用了cache.processJournal(),processJournal方法实现如下:
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();
    }
  }
}


先是删除journal.tmp文件,接着通过遍历lruEntries,遍历过程中对操作记录为CLEAN后面的数字(key指定多少个文件 ,数字代表文件长度)进行统计。回到open方法,获取journal文件的BufferedWriter,以上是基于journal文件存在的前提下,对journal文件进行处理,journal文件在一开始是不存在的,因此我们创建一个新的缓存目录,实例化DiskLruCache,执行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));
}


rebuildJournal方法中,往journal.tmp文件写入五行内容如下:
libcore.io.DiskLruCache
1
100
2



最后把journal.tmp文件重命名为journal,并获取journal文件的BufferedWriter,到此DiskLruCache的实例已经获取完毕。

存入缓存

DiskLruCache.Editor editor = diskLruCache.edit("image_url");
OutputStream os = editor.newOutputStream(0);
editor.commit();

相关源码:
public Editor edit(String key) throws IOException {
  return edit(key, ANY_SEQUENCE_NUMBER);
}

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


checkNotClosed方法检查journalWriter是否为null,为null抛出异常,validateKey方法通过正则表达式 ,验证key,可以必须是字母、数字、下划线、横线(-)组成,且长度在1-120之间,接下来获取entry(不存在创建),并添加到lruEntries中,接着实例化Editor,赋值给entry的currentEditor,前面说过,写入时会往journal文件写入DIRTY相关操作。获取到editor对象后,调用它的newOutputStream方法获取输入流,newOutputStream方法实现如下:
public OutputStream newOutputStream(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);
    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);
  }
}


newOutputStream方法中通过Entry的getDirtyFile方法拿到一个key.index.tmp的文件,并把这个文件的FileOutputStream通过FaultHidingOutputStream封装后传递给我们,最后通过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;
}

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


在completeEdit方法中,如果之前记录有值该editor的entry属性readable为true,否则为false,在前面的editor.written已经被赋值为true,因此里面的流程我们不需要去看,进入第二个循环通过getDirtyFile方法拿到key.index.tmp 文件将它重命名为key.index,并刷新size,接下来满足readable或 success成功后,写入CLEAN标记,如果失败写入标记REMOVE,接下来判断size是否大于我们设置的缓存最大值,journalRebuildRequired方法判断 redundantOpCount是否到达2000,无论是超过缓存最大值还是redundantOpCount到达2000,都会进行重建,重建通过线程池来执行。
final ThreadPoolExecutor executorService =
    new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
private final Callable<Void> cleanupCallable = new Callable<Void>() {
  public Void call() throws Exception {
    synchronized (DiskLruCache.this) {
      if (journalWriter == null) {
        return null; // Closed.
      }
      trimToSize();
      if (journalRebuildRequired()) {
        rebuildJournal();
        redundantOpCount = 0;
      }
    }
    return null;
  }
};

trimToSize方法对lruEntries进行清空:
private void trimToSize() throws IOException {
  while (size > maxSize) {
    Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
    remove(toEvict.getKey());
  }
}

redundantOpCount到达2000进行重建journal文件,rebuildJournal方法前期已经讲过。

取出缓存数据

DiskLruCache.Snapshot snapShot = diskLruCache.get("image_url");
if (snapShot != null) {
    InputStream is = snapShot.getInputStream(0);
}


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

get方法会将key.index文件 的FileInputStream进行封装返回Snapshot,并写入READ操作记录,get方法中也对redundantOpCount是否到达2000进行了判断,如果超出,就通过线程池开启线程重建。

至此源码大体分析结束,如有不足或遗漏的请浏览前言的两篇文章,谢谢!

©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师:上身试试 返回首页