Android缓存源码分析(DiskLruCache,LruCache)

绝地灬酷狼 2022-06-18 09:42 236阅读 0赞
  1. Android的常见的缓存有三种:网络缓存、文件缓存、内存缓存。这里网络缓存我不考虑,我们看下文件缓存(DiskLruCache)、内存缓存(LruCache)的源码是咋实现的。

一,LRU算法

  1. 文件缓存(DiskLruCache)和内存缓存(LruCache)用到的都是LRU缓存算法,LRU算法(Least Recently Used 最近最少使用)简单来说就是最近使用的会被留下来,最少使用的会被淘汰出去。
  2. 为了能够更好的理解LRU算法举个例子:比如我们缓存里面只能保留三个数据,缓存队列为: 43423 142
  3. 第一次数据进入: 4 进入,缓存:4
  4. 第二次数据进入: 3 进入,缓存:34
  5. 第三次数据进入: 4 进入,缓存:43
  6. 第四次数据进入: 2 进入,缓存:243
  7. 第五次数据进入: 3 进入,缓存:324
  8. 第六次数据进入: 1 进入,缓存:132 4被淘汰了)
  9. 第七次数据进入: 4 进入,缓存:413 2被淘汰了)
  10. 第八次数据进入: 2 进入,缓存:241 3被淘汰了)

二,Android内部存储、外部存储

内部存储:/data 文件夹下的所有内容就是我们常说的内部存储。在内部存储文件中我们关注 /data/data/包名 这个文件夹下的内容,这个文件对每个应用来说是私有的别的应用访问不到。

/data/data/包名 常规文件结构如下:
/data/data/包名 常规文件结构

  1. /data/data/包名 下有四个主要的文件cachefilesdatabasesshared\_prefs
  • /data/data/包名/cache:context.getCacheDir()来获取该路径,用来保存缓存信息,但是我们自己开发应用的时候,一般也是尽量避免把缓存数据放这个目录下,优先考虑放到外部存储的缓存目录下,因为内部存储空间有限,要是内存存储耗尽了装不了其他的应用。
  • /data/data/包名/files:context.getFilesDir()来获取路径,用来保存一些额外的信息到内部存储。
  • /data/data/包名/databases:存放的是应用中用到的一些数据库。
  • /data/data/包名/shared_prefs:存放的是应用中SharedPreferences保存的一些数据。

PS:当应用卸载的同时内部文件 /data/data/包名 这个文件会被删除掉。

外部存储:storage文件夹所有的内容,外部存储里面有一个文件我们经常接触到 /storage/Android/data/包名 文件。
这里写图片描述

  • /storage/Android/data/包名/cache:context.getExternalCacheDir()来获取该路径,用来保存缓存信息。
  • /storage/Android/data/包名/files:context.getExternalFilesDir()来获取路径,用来保存一些额外的信息到外部存储。

PS:当应用卸载的同时内部文件 /storage/Android/data/包名 这个文件会被删除掉

Android中每个应用都有清除数据和清除缓存两个功能。这两个功能呢的作用分别是;
清除数据:清除的是 /data/data/包名 下面除lib文件夹之外的所有文件。
清除缓存:清除的是 /sdcard/Android/data/包名/cache 文件会被删除。

三,DiskLruCache源码分析

  1. 在分析DiskLruCache代码之前,先了解下LinkedHashMap双向循环链表,这是LRU算法的关键点,LinkedHashMap有一个三个函数的构造函数(DiskLruCacheLruCache里面用的都是这个)这里LinkedHashMap构造函数的第三个参数还有意思啊;true:基于访问顺序,false:基于插入顺序。基于访问顺序就是当访问了LinkedHashMap某个结点的时候内部都会将这个结点移动到链表的尾部,没有访问的就一直在链表的头部放着。稍微做一个简单的处理在特定饿情况下把表头节点删除正好体现了LRU算法。很有意思的一个点。
  1. DiskLruCache源码下载地址
  2. DiskLruCache简单介绍
    1. DiskLruCache是市面上文件缓存使用最多的一个类,可以把想缓存的数据缓存都文件中去。DiskLruCache缓存又两部分组成一个是日志文件journal,一个是具体的缓存内容(所有的缓存内容都是以文件的形式保存)如下图所示。

这里写图片描述

  1. journalDiskLruCache缓存的日志文件。journal文件包含两部分内容;前五行是文件头信息、之后记录了每个缓存内容读取存储的情况。

这里写图片描述

journal文件头第一行;标记固定为libcore.io.DiskLruCache、第二行DiskLruCache版本、第三行应用版本、第四行每个key对应缓存文件个数、第五行预留。
journal缓存记录信息,每一行都是一次记录信息,有四种情况:
a. CLEAN(空格)【 key】(空格) 【缓存文件大小】(空格)【缓存文件大小】….:表示指定key的缓存记录写入成功。(缓存文件大小循环次数和文件头第四行对应,如果是1就是CLEAN(空格)【 key】(空格) 【缓存文件大小】形式)
b. DIRTY(空格)【 key】:表示指定key的缓存正在被写入。(但是还没有Editor.commit()函数)
c. REMOVE(空格)【 key】:表示指定key的缓存写入失败。
d. READ(空格)【 key】:表示读取了指定key缓存信息。

  1. DiskLruCache代码分析

    1. 使用DiskLruCache来实现文件缓存,用的最多的几个函数就是DiskLruCache对象初始化(open()),保存缓存记录(edit(),Editor.commit(),flush()),读取缓存记录(get())。代码的分析就从这三个方面入手。
    • DiskLruCache对象初始化:
      这里写图片描述
      入口在open()函数。DiskLruCache对象初始化初始化的时候做的事情就两件事:第一通过日志文件头信息去判断之前缓存是否可用、第二解析之前缓存信息到LinkedHashMap

      /* 初始化DiskLruCache对象 @param directory 缓存日志文件,和缓存文件保存的目录。 @param appVersion 应用版本号,如果版本号升级了,之前的缓存信息不在使用。 @param valueCount 每个key可以对应多少个缓存文件,一般都是1 @param maxSize 所有缓存文件的限制大小。 @throws IOException 失败 */
      public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {

      1. if (maxSize <= 0) {
      2. throw new IllegalArgumentException("maxSize <= 0");
      3. }
      4. if (valueCount <= 0) {
      5. throw new IllegalArgumentException("valueCount <= 0");
      6. }
      7. // prefer to pick up where we left off
      8. DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
      9. /** * 判断日志文件是否存在 */
      10. if (cache.journalFile.exists()) {
      11. /** * 日志文件存在 */
      12. try {
      13. /** * 读缓存日志文件,同时会去判断标记,版本,应用版本信息如果不一致,会抛出异常,并且会把上次的缓存信息读到之前缓存信息到LinkedHashMap<String, Entry> lruEntries中去 */
      14. cache.readJournal();
      15. /** * 对上一次的缓存信息做处理(根据lruEntries),计算所有缓存文件的大小,把正在被编辑的缓存删除掉。 */
      16. cache.processJournal();
      17. cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE);
      18. return cache;
      19. } catch (IOException journalIsCorrupt) {
      20. // System.logW("DiskLruCache " + directory + " is corrupt: "
      21. // + journalIsCorrupt.getMessage() + ", removing");
      22. /** * 操作失败,删除文件(这里会删除缓存目录下面所有的文件包括日志文件和日志临时文件和缓存文件) */
      23. cache.delete();
      24. }
      25. }
      26. /** * 日志文件不存在,或者之前的缓存信息不可用了 */
      27. // create a new empty cache
      28. directory.mkdirs();
      29. cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
      30. /** * 重新构建一个日志文件 */
      31. cache.rebuildJournal();
      32. return cache;

      }

23行,如果有日志文件说明之前有缓存过信息,这里对上次的缓存信息做处理,关键的东西在journal文件里面,从journal文件解析到之前的缓存信息。
31行,去读journal文件里面之前的缓存信息。判断缓存是否过期,同时把之前的缓存信息记录保存到lruEntries,Map里面去。
35行,对读到的上次缓存信息做处理,计算size,把没有调用Edit.commit()的缓存剔除掉。
readJournal()函数具体实现:

  1. /** * 读取缓存日志文件信息,主要做两件事 * 1. 读日志文件的头部信息,标记,缓存版本,应用版本,进而判断日志文件是否过期 * 2. 把日志文件的缓存记录读取到lruEntries,Map里面去。 */
  2. private void readJournal() throws IOException {
  3. InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
  4. try {
  5. /** * 缓存日志文件第一行,标记 */
  6. String magic = readAsciiLine(in);
  7. /** * 缓存日志文件第二行,版本 */
  8. String version = readAsciiLine(in);
  9. /** * 缓存日志文件第三行,应用版本 */
  10. String appVersionString = readAsciiLine(in);
  11. /** * 缓存日志文件第四行,表示同一个key可以对应多少个缓存文件 */
  12. String valueCountString = readAsciiLine(in);
  13. /** * 缓存日志文件第五行,预留 */
  14. String blank = readAsciiLine(in);
  15. /** * 对日志文件头做判断 */
  16. if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion).equals(appVersionString) ||
  17. !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)) {
  18. throw new IOException(
  19. "unexpected journal header: [" + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
  20. }
  21. while (true) {
  22. try {
  23. /** * 读取日志信息(从第六行开始才是真正的日志信息) */
  24. readJournalLine(readAsciiLine(in));
  25. } catch (EOFException endOfJournal) {
  26. break;
  27. }
  28. }
  29. } finally {
  30. /** * 关闭InputStream */
  31. closeQuietly(in);
  32. }
  33. }

processJournal()函数具体实现:

  1. /** * 处理日志文件, * 1. 会去计算整个缓存文件的大小size * 2. 把正在被编辑的key(上次保存缓存的时候没有调用Edit.commit()),可以认为是没有写成功的缓存,重置掉(相应的缓存文件删除, 并且从lruEntries中删除掉) */
  2. private void processJournal() throws IOException {
  3. /** * 删除日志临时文件 */
  4. deleteIfExists(journalFileTmp);
  5. /** * 遍历lruEntries(缓存map) */
  6. for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
  7. Entry entry = i.next();
  8. if (entry.currentEditor == null) {
  9. /** * 有缓存文件 */
  10. for (int t = 0; t < valueCount; t++) {
  11. size += entry.lengths[t];
  12. }
  13. } else {
  14. /** * 保存缓存的时候没有调用Edit.commit() */
  15. entry.currentEditor = null;
  16. for (int t = 0; t < valueCount; t++) {
  17. deleteIfExists(entry.getCleanFile(t));
  18. deleteIfExists(entry.getDirtyFile(t));
  19. }
  20. i.remove();
  21. }
  22. }
  23. }
  • 保存缓存记录
    这里写图片描述
    要保存缓存的时候,要做两件事一时保存缓存文件,二是写缓存日志文件。为了保存缓存文件我们写的得到Edit对象,然后通过Edit对象得到OutputStream对象然后才可以写入文件,最后commit()提交保存。
    总得来说五个步骤;调用edit()得到Edit对象,调用Editt.newOutputStream()得到OutputStream,调用OutputStream.write()把缓存写入文件,Edit.commit()确定缓存写入【commit不用每次都调用,可以挑个合适的时间调用】,最后调用flush()。
    edit()函数得到Edit对象,拿到Edit对象之后就可以做一系列操作了。

    /* 编辑key对应的缓存,获取到Editor然后可以做一些列的操作 */

    1. private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    2. checkNotClosed();
    3. validateKey(key);
    4. Entry entry = lruEntries.get(key);
    5. if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
    6. return null; // snapshot is stale
    7. }
    8. if (entry == null) {
    9. /** * 新加入的缓存 */
    10. entry = new Entry(key);
    11. lruEntries.put(key, entry);
    12. } else if (entry.currentEditor != null) {
    13. /** * 之前加入过的缓存,并且这个缓存正在被编辑(没有调用Edit.commit()) */
    14. return null; // another edit is in progress
    15. }
    16. Editor editor = new Editor(entry);
    17. entry.currentEditor = editor;
    18. // flush the journal before creating files to prevent file leaks
    19. /** * key对应的缓存正在被写入,正在被编辑 */
    20. journalWriter.write(DIRTY + ' ' + key + '\n');
    21. journalWriter.flush();
    22. return editor;
    23. }

Edit.newOutputStream()得到OutputStream文件输出流。

  1. /** * 创建key对应的缓存文件的输出流,用来写入缓存到缓存文件 */
  2. public OutputStream newOutputStream(int index) throws IOException {
  3. synchronized (DiskLruCache.this) {
  4. if (entry.currentEditor != this) {
  5. throw new IllegalStateException();
  6. }
  7. return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
  8. }
  9. }

Edit.commit()调用到completeEdit()函数,提交保存缓存完成。

  1. /** * 完成key对应缓存文件的写入之后,调用该函数(调用了newOutputStream(),set() 函数之后调用该函数) */
  2. private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
  3. Entry entry = editor.entry;
  4. /** * 一一对应判断 */
  5. if (entry.currentEditor != editor) {
  6. throw new IllegalStateException();
  7. }
  8. /** * entry.readable = true 说明我们之前保存了该key对应的缓存文件 * entry.readable = false 说明我们之前没有保存该key对应的缓存文件(那肯定是准备些入缓存文件了) */
  9. // if this edit is creating the entry for the first time, every index must have a value
  10. if (success && !entry.readable) {
  11. /** * 之前没有保存key对应的缓存文件 */
  12. for (int i = 0; i < valueCount; i++) {
  13. /** * 调用了newOutputStream()之后entry.getDirtyFile(i).exists就是真的了 */
  14. if (!entry.getDirtyFile(i).exists()) {
  15. editor.abort();
  16. throw new IllegalStateException("edit didn't create file " + i);
  17. }
  18. }
  19. }
  20. for (int i = 0; i < valueCount; i++) {
  21. File dirty = entry.getDirtyFile(i);
  22. if (success) {
  23. if (dirty.exists()) {
  24. /** * 缓存临时文件,重命名为缓存文件 */
  25. File clean = entry.getCleanFile(i);
  26. dirty.renameTo(clean);
  27. long oldLength = entry.lengths[i];
  28. long newLength = clean.length();
  29. entry.lengths[i] = newLength;
  30. /** * 缓存大小 */
  31. size = size - oldLength + newLength;
  32. }
  33. } else {
  34. deleteIfExists(dirty);
  35. }
  36. }
  37. redundantOpCount++;
  38. /** * 一次缓存文件的操作结束了,置为null */
  39. entry.currentEditor = null;
  40. if (entry.readable | success) {
  41. entry.readable = true;
  42. /** * 标记一条entry写入成功 */
  43. journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
  44. if (success) {
  45. entry.sequenceNumber = nextSequenceNumber++;
  46. }
  47. } else {
  48. /** * 如果是失败的情况,则移除 */
  49. lruEntries.remove(entry.key);
  50. journalWriter.write(REMOVE + ' ' + entry.key + '\n');
  51. }
  52. /** * 又来一次缓存大小的限制 */
  53. if (size > maxSize || journalRebuildRequired()) {
  54. executorService.submit(cleanupCallable);
  55. }
  56. }
  • 读取缓存记录
    这里写图片描述
    读缓存记录这个就简单了得到Snapshot,然后通过Snapshot去得到InputStream或者直接得到具体的缓存内容。都会从缓存文件中去读取信息。
    通过get()函数去得到Snapshot

    /* 根据key取出缓存得到缓存key对应的Snapshot */

    1. public synchronized Snapshot get(String key) throws IOException {
    2. /** * 日志文件是否关闭 */
    3. checkNotClosed();
    4. /** * key是否有效 */
    5. validateKey(key);
    6. /** * 每次open的时候会把日志文件里面的信息读到lruEntries里面去 */
    7. Entry entry = lruEntries.get(key);
    8. /** * 是否有该key对应的日志文件 */
    9. if (entry == null) {
    10. return null;
    11. }
    12. /** * 是否可读(是否有缓存文件) */
    13. if (!entry.readable) {
    14. return null;
    15. }
    16. /** * 打开该key对应的所有的缓存文件,用来读取缓存文件 */
    17. InputStream[] ins = new InputStream[valueCount];
    18. try {
    19. for (int i = 0; i < valueCount; i++) {
    20. ins[i] = new FileInputStream(entry.getCleanFile(i));
    21. }
    22. } catch (FileNotFoundException e) {
    23. /** * 文件被手动删除了 */
    24. // a file must have been deleted manually!
    25. return null;
    26. }
    27. redundantOpCount++;
    28. /** * 日志文件记录一天read的记录 */
    29. journalWriter.append(READ + ' ' + key + '\n');
    30. if (journalRebuildRequired()) {
    31. /** * 重新构建日志文件 */
    32. executorService.submit(cleanupCallable);
    33. }
    34. return new Snapshot(key, entry.sequenceNumber, ins);
    35. }

得到Snapshot之后通过Snapshot去读取缓存文件信息,这个就很容易了。

  1. 总结DiskLruCacheDiskLruCache的实现两个部分:日志文件和具体的缓存文件。每次对缓存存储的时候除了对缓存文件做相应的操作,还会在日志文件做相应的记录。每条日志文件有四种情况:CLEAN(调用了edit()之后,保存了缓存,并且调用了Edit.commit()了)、DIRTY(缓存正在编辑,调用edit()函数)、REMOVE(缓存写入失败)、READ(读缓存)。要想根据key从缓存文件中读取到具体的缓存信息,先得到Snapshot,然后根据Snapshot的一些方法做一些了的操作得到具体缓存信息。要保存一个缓存信息的时候写得到Editor,然后根据Editor对缓存文件做一些列的操作最后如果是保存了缓存信息记得commit下确认提交。

四,LruCache源码

  1. 有了上面DiskLruCache的分析,看LruCache就简单的多了,LruCache也是关键点在一个双向链表LinkedHashMap map上,同样这也是LRU算法的关键点。LruCache大部分都是在和这个LinkedHashMap打交道,不停的取出来,删除。当然在取出来或者存进去的时候得时时刻刻检查缓存的大小。LruCache里面每个缓存的大小都是要我们外部告诉LruCache的,因为LruCache不知道保存的是啥东西只能外部在构造LruCache的对象的时候通过重写sizeOf()函数来告诉LruCache每个缓存大小的计算方式。
  • LruCache对象初始化:
    在LruCache初始化的时候,主要做两件事情,一告诉LruCache总缓存限制大小,二告诉LruCache每个缓存的大小是怎么计算的。举个最简单的例子缓存string。

    private static final int DISK_MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 1024) / 10;

    1. private LruCache<String, String> mLruCache = null;
    2. public MemoryCacheImp() {
    3. mLruCache = new LruCache<String, String>(DISK_MAX_SIZE) {
    4. @Override
    5. protected int sizeOf(String key, String value) {
    6. return value == null ? 0 : value.length();
    7. }
    8. };
    9. }
  • 读取缓存:
    其实里面做的事情也蛮简单的,从LinkedHashMap里面根据key取出对应的value。

    /* 获取key对应的缓存 */

    1. public final V get(K key) {
    2. if (key == null) {
    3. throw new NullPointerException("key == null");
    4. }
    5. V mapValue;
    6. /** * 线程安全 */
    7. synchronized (this) {
    8. mapValue = map.get(key);
    9. if (mapValue != null) {
    10. /** * 命中,返回 */
    11. hitCount++;
    12. return mapValue;
    13. }
    14. /** * 没有命中 */
    15. missCount++;
    16. }
    17. /* * Attempt to create a value. This may take a long time, and the map * may be different when create() returns. If a conflicting value was * added to the map while create() was working, we leave that value in * the map and release the created value. * 下面感觉是对一些特殊的key做处理 */
    18. V createdValue = create(key);
    19. if (createdValue == null) {
    20. return null;
    21. }
    22. synchronized (this) {
    23. createCount++;
    24. mapValue = map.put(key, createdValue);
    25. if (mapValue != null) {
    26. // There was a conflict so undo that last put
    27. map.put(key, mapValue);
    28. } else {
    29. size += safeSizeOf(key, createdValue);
    30. }
    31. }
    32. if (mapValue != null) {
    33. entryRemoved(false, key, createdValue, mapValue);
    34. return mapValue;
    35. } else {
    36. trimToSize(maxSize);
    37. return createdValue;
    38. }
    39. }
  • 保存缓存:
    其实里面做的事情也是蛮简单的就是把key对应的value保存到LinkedHashMap里面去,同时对缓存的大小做限制,如果超过了最大大小从LinkedHashMap中剔除掉头部的节点。

    /* 加入缓存 Caches {@code value} for {@code key}. The value is moved to the head of the queue. @return the previous value mapped by {@code key}. */

    1. public final V put(K key, V value) {
    2. if (key == null || value == null) {
    3. throw new NullPointerException("key == null || value == null");
    4. }
    5. V previous;
    6. synchronized (this) {
    7. putCount++;
    8. /** * size大小增加 */
    9. size += safeSizeOf(key, value);
    10. /** * 放入缓存链表中 */
    11. previous = map.put(key, value);
    12. if (previous != null) {
    13. /** * 说明之前缓存链表里面有这个key对应的结点缓存(size大小减掉之前的) */
    14. size -= safeSizeOf(key, previous);
    15. }
    16. }
    17. if (previous != null) {
    18. /** * 有结点移除调用该函数 */
    19. entryRemoved(false, key, previous, value);
    20. }
    21. /** * 限制存储容量 */
    22. trimToSize(maxSize);
    23. return previous;
    24. }

    LruCache也就到这吧,也是一直在围绕LinkedHashMap这东西打转。

五,总结

  1. DiskLruCacheLruCache一个是把缓存保存到文件中去,一个是把缓存直接保存在内存里面。DiskLruCacheLruCache要稍微复杂一点每个缓存保存一个文件并且还要做相应的日志记录,根据日志的信息方便应用下次进来的时候还能利用上之前保存的缓存信息。LruCache呢就完完全全是和LinkedHashMap打交道就行了。这里不管是DiskLruCache还是LruCache里面都是用到了LinkedHashMap的三个函数的构造函数。LinkedHashMap的第三个参数给的是true:基于访问顺序,换句话就是说当访问了LinkedHashMap中的某个结点的时候LinkedHashMap内部都会将这个结点移动到链表的尾部,这样就出现了一个结果,经常访问的节点会徘徊在LinkedHashMap尾部,没有被访问到的节点会徘徊在LinkedHashMap的头部,这样当缓存超过了限制大小的时候从LinkedHashMap的头部把节点删除就好了(DiskLruCache在删除的时候除了日志文件做记录,还会把缓存文件给删除)。

最后给一个写的没啥大用处的demo例子吧 下载链接地址

发表评论

表情:
评论列表 (有 0 条评论,236人围观)

还没有评论,来说两句吧...

相关阅读

    相关 mybatis一级缓存分析

      MyBatis执行SQL语句之后,这条语句就是被缓存,以后再执行这条语句的时候,会直接从缓存中拿结果,而不是再次执行SQL 这也就是大家常说的MyBatis一级缓存,一