服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android高手进阶之彻底了解DiskLruCache磁盘缓存机制原理

Android高手进阶之彻底了解DiskLruCache磁盘缓存机制原理

2021-09-14 23:16Android开发编程 Android

DiskLruCache是一种管理数据存储的技术,单从Cache的字面意思也可以理解到,"Cache","高速缓存"。今天我们来从源码上分析下DiskLruCache;Android进阶之彻底理解LruCache缓存机制原理

Android高手进阶之彻底了解DiskLruCache磁盘缓存机制原理

前言

DiskLruCache是一种管理数据存储的技术,单从Cache的字面意思也可以理解到,"Cache","高速缓存";

之前我们介绍过lrucache,没有看过老铁,可以从历史记录看;

今天我们来从源码上分析下DiskLruCache;

Android进阶之彻底理解LruCache缓存机制原理

一、为什么用DiskLruCache

1、LruCache和DiskLruCache

LruCache和DiskLruCache两者都是利用到LRU算法,通过LRU算法对缓存进行管理,以最近最少使用作为管理的依据,删除最近最少使用的数据,保留最近最常用的数据;

LruCache运用于内存缓存,而DiskLruCache是存储设备缓存;

2、为何使用DiskLruCache

离线数据存在的意义,当无网络或者是网络状况不好时,APP依然具备部分功能是一种很好的用户体验;

假设网易新闻这类新闻客户端,数据完全存储在缓存中而不使用DiskLruCache技术存储,那么当客户端被销毁,缓存被释放,意味着再次打开APP将是一片空白;

另外DiskLruCache技术也可为app“离线阅读”这一功能做技术支持;

DiskLruCache的存储路径是可以自定义的,不过也可以是默认的存储路径,而默认的存储路径一般是这样的:/sdcard/Android/data/包名/cache,包名是指APP的包名。我们可以在手机上打开,浏览这一路径;

二、DiskLruCache使用

1、添加依赖

  1. // add dependence 
  2. implementation 'com.jakewharton:disklrucache:2.0.2' 

2、创建DiskLruCache对象

  1. /* 
  2.  * directory – 缓存目录 
  3.  * appVersion - 缓存版本 
  4.  * valueCount – 每个key对应value的个数 
  5.  * maxSize – 缓存大小的上限 
  6.  */ 
  7. DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10); 

3、添加 / 获取 缓存(一对一)

  1. /** 
  2.  * 添加一条缓存,一个key对应一个value 
  3.  */ 
  4. public void addDiskCache(String key, String value) throws IOException { 
  5.     File cacheDir = context.getCacheDir(); 
  6.     DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, 1, 1, 1024 * 1024 * 10); 
  7.     DiskLruCache.Editor editor = diskLruCache.edit(key); 
  8.     // index与valueCount对应,分别为0,1,2...valueCount-1 
  9.     editor.newOutputStream(0).write(value.getBytes());  
  10.     editor.commit(); 
  11.     diskLruCache.close(); 
  12. /** 
  13.  * 获取一条缓存,一个key对应一个value 
  14.  */ 
  15. public void getDiskCache(String key) throws IOException { 
  16.     File directory = context.getCacheDir(); 
  17.     DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10); 
  18.     String value = diskLruCache.get(key).getString(0); 
  19.     diskLruCache.close(); 

4、添加 / 获取 缓存(一对多)

  1. /** 
  2.  * 添加一条缓存,1个key对应2个value 
  3.  */ 
  4. public void addDiskCache(String key, String value1, String value2) throws IOException { 
  5.     File directory = context.getCacheDir(); 
  6.     DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024 * 1024 * 10); 
  7.     DiskLruCache.Editor editor = diskLruCache.edit(key); 
  8.     editor.newOutputStream(0).write(value1.getBytes()); 
  9.     editor.newOutputStream(1).write(value2.getBytes()); 
  10.     editor.commit(); 
  11.     diskLruCache.close(); 
  12. /** 
  13.  * 添加一条缓存,1个key对应2个value 
  14.  */ 
  15. public void getDiskCache(String key) throws IOException { 
  16.     File directory = context.getCacheDir(); 
  17.     DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024); 
  18.     DiskLruCache.Snapshot snapshot = diskLruCache.get(key); 
  19.     String value1 = snapshot.getString(0); 
  20.     String value2 = snapshot.getString(1); 
  21.     diskLruCache.close(); 

三、源码分析

Android高手进阶之彻底了解DiskLruCache磁盘缓存机制原理

1、open()

DiskLruCache的构造方法是private修饰,这也就是告诉我们,不能通过new DiskLruCache来获取实例,构造方法如下:

  1. private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { 
  2.     this.directory = directory; 
  3.     this.appVersion = appVersion; 
  4.     this.journalFile = new File(directory, JOURNAL_FILE); 
  5.     this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); 
  6.     this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); 
  7.     this.valueCount = valueCount; 
  8.     this.maxSize = maxSize; 

但是提供了open()方法,供我们获取DiskLruCache的实例,open方法如下:

  1. /** 
  2.    * Opens the cache in {@code directory}, creating a cache if none exists 
  3.    * there. 
  4.    * 
  5.    * @param directory a writable directory 
  6.    * @param valueCount the number of values per cache entry. Must be positive. 
  7.    * @param maxSize the maximum number of bytes this cache should use to store 
  8.    * @throws IOException if reading or writing the cache directory fails 
  9.    */ 
  10.   public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) 
  11.       throws IOException { 
  12.     if (maxSize <= 0) { 
  13.       throw new IllegalArgumentException("maxSize <= 0"); 
  14.     } 
  15.     if (valueCount <= 0) { 
  16.       throw new IllegalArgumentException("valueCount <= 0"); 
  17.     } 
  18.     // If a bkp file exists, use it instead
  19.     //看备份文件是否存在 
  20.     File backupFile = new File(directory, JOURNAL_FILE_BACKUP); 
  21.    //如果备份文件存在,并且日志文件也存在,就把备份文件删除 
  22.     //如果备份文件存在,日志文件不存在,就把备份文件重命名为日志文件 
  23.      if (backupFile.exists()) { 
  24.       File journalFile = new File(directory, JOURNAL_FILE); 
  25.       // If journal file also exists just delete backup file. 
  26.         // 
  27.       if (journalFile.exists()) { 
  28.         backupFile.delete(); 
  29.       } else { 
  30.         renameTo(backupFile, journalFile, false); 
  31.       } 
  32.     } 
  33.     // Prefer to pick up where we left off
  34.     //初始化DiskLruCache,包括,大小,版本,路径,key对应多少value 
  35.     DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 
  36.     //如果日志文件存在,就开始赌文件信息,并返回 
  37.     //主要就是构建entry列表 
  38.     if (cache.journalFile.exists()) { 
  39.       try { 
  40.         cache.readJournal(); 
  41.         cache.processJournal(); 
  42.         return cache; 
  43.       } catch (IOException journalIsCorrupt) { 
  44.         System.out 
  45.             .println("DiskLruCache " 
  46.                 + directory 
  47.                 + " is corrupt: " 
  48.                 + journalIsCorrupt.getMessage() 
  49.                 + ", removing"); 
  50.         cache.delete(); 
  51.       } 
  52.     } 
  53.     //不存在就新建一个 
  54.     // Create a new empty cache. 
  55.     directory.mkdirs(); 
  56.     cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); 
  57.     cache.rebuildJournal(); 
  58.     return cache; 
  59.   } 

open函数:如果日志文件存在,直接去构建entry列表;如果不存在,就构建日志文件;

2、rebuildJournal()

  1. 构建文件: 
  2.   //这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作 
  3.  private final File journalFile; 
  4.  //journal文件的temp 缓存文件,一般都是先构建这个缓存文件,等待构建完成以后将这个缓存文件重新命名为journal 
  5.  private final File journalFileTmp; 
  6. /** 
  7.    * Creates a new journal that omits redundant information. This replaces the 
  8.    * current journal if it exists. 
  9.    */ 
  10.   private synchronized void rebuildJournal() throws IOException { 
  11.     if (journalWriter != null) { 
  12.       journalWriter.close(); 
  13.     } 
  14.     //指向journalFileTmp这个日志文件的缓存 
  15.     Writer writer = new BufferedWriter( 
  16.         new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); 
  17.     try { 
  18.       writer.write(MAGIC); 
  19.       writer.write("\n"); 
  20.       writer.write(VERSION_1); 
  21.       writer.write("\n"); 
  22.       writer.write(Integer.toString(appVersion)); 
  23.       writer.write("\n"); 
  24.       writer.write(Integer.toString(valueCount)); 
  25.       writer.write("\n"); 
  26.       writer.write("\n"); 
  27.       for (Entry entry : lruEntries.values()) { 
  28.         if (entry.currentEditor != null) { 
  29.           writer.write(DIRTY + ' ' + entry.key + '\n'); 
  30.         } else { 
  31.           writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 
  32.         } 
  33.       } 
  34.     } finally { 
  35.       writer.close(); 
  36.     } 
  37.     if (journalFile.exists()) { 
  38.       renameTo(journalFile, journalFileBackup, true); 
  39.     } 
  40.      //所以这个地方 构建日志文件的流程主要就是先构建出日志文件的缓存文件,如果缓存构建成功 那就直接重命名这个缓存文件,这样做好处在哪里? 
  41.     renameTo(journalFileTmp, journalFile, false); 
  42.     journalFileBackup.delete(); 
  43.     //这里也是把写入日志文件的writer初始化 
  44.     journalWriter = new BufferedWriter( 
  45.         new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); 
  46.   } 

再来看当日志文件存在的时候,做了什么

3、readJournal()

  1. private void readJournal() throws IOException { 
  2. StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); 
  3. try { 
  4. //读日志文件的头信息 
  5.   String magic = reader.readLine(); 
  6.   String version = reader.readLine(); 
  7.   String appVersionString = reader.readLine(); 
  8.   String valueCountString = reader.readLine(); 
  9.   String blank = reader.readLine(); 
  10.   if (!MAGIC.equals(magic) 
  11.       || !VERSION_1.equals(version) 
  12.       || !Integer.toString(appVersion).equals(appVersionString) 
  13.       || !Integer.toString(valueCount).equals(valueCountString) 
  14.       || !"".equals(blank)) { 
  15.     throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " 
  16.         + valueCountString + ", " + blank + "]"); 
  17.   } 
  18. //这里开始,就开始读取日志信息 
  19.   int lineCount = 0; 
  20.   while (true) { 
  21.     try { 
  22.     //构建LruEntries entry列表 
  23.       readJournalLine(reader.readLine()); 
  24.       lineCount++; 
  25.     } catch (EOFException endOfJournal) { 
  26.       break; 
  27.     } 
  28.   } 
  29.   redundantOpCount = lineCount - lruEntries.size(); 
  30.   // If we ended on a truncated line, rebuild the journal before appending to it. 
  31.   if (reader.hasUnterminatedLine()) { 
  32.     rebuildJournal(); 
  33.   } else { 
  34.     //初始化写入文件的writer 
  35.     journalWriter = new BufferedWriter(new OutputStreamWriter( 
  36.         new FileOutputStream(journalFile, true), Util.US_ASCII)); 
  37.   } 
  38. } finally { 
  39.   Util.closeQuietly(reader); 

然后看下这个函数里面的几个主要变量:

  1. //每个entry对应的缓存文件的格式 一般为1,也就是一个key,对应几个缓存,一般设为1,key-value一一对应的关系 
  2. private final int valueCount; 
  3. private long size = 0; 
  4. //这个是专门用于写入日志文件的writer 
  5. private Writer journalWriter; 
  6. //这个集合应该不陌生了, 
  7. private final LinkedHashMap<String, Entry> lruEntries = 
  8.         new LinkedHashMap<String, Entry>(0, 0.75f, true); 
  9. //这个值大于一定数目时 就会触发对journal文件的清理了 
  10. private int redundantOpCount; 

下面就看下entry这个实体类的内部结构

  1. private final class Entry { 
  2.         private final String key
  3.         /** 
  4.          * Lengths of this entry's files. 
  5.          * 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1 
  6.          */ 
  7.         private final long[] lengths; 
  8.         /** 
  9.          * True if this entry has ever been published. 
  10.          * 曾经被发布过 那他的值就是true 
  11.          */ 
  12.         private boolean readable; 
  13.         /** 
  14.          * The ongoing edit or null if this entry is not being edited. 
  15.          * 这个entry对应的editor 
  16.          */ 
  17.         private Editor currentEditor; 
  18.         @Override 
  19.         public String toString() { 
  20.             return "Entry{" + 
  21.                     "key='" + key + '\'' + 
  22.                     ", lengths=" + Arrays.toString(lengths) + 
  23.                     ", readable=" + readable + 
  24.                     ", currentEditor=" + currentEditor + 
  25.                     ", sequenceNumber=" + sequenceNumber + 
  26.                     '}'
  27.         } 
  28.         /** 
  29.          * The sequence number of the most recently committed edit to this entry. 
  30.          * 最近编辑他的序列号 
  31.          */ 
  32.         private long sequenceNumber; 
  33.         private Entry(String key) { 
  34.             this.key = key
  35.             this.lengths = new long[valueCount]; 
  36.         } 
  37.         public String getLengths() throws IOException { 
  38.             StringBuilder result = new StringBuilder(); 
  39.             for (long size : lengths) { 
  40.                 result.append(' ').append(size); 
  41.             } 
  42.             return result.toString(); 
  43.         } 
  44.         /** 
  45.          * Set lengths using decimal numbers like "10123"
  46.          */ 
  47.         private void setLengths(String[] strings) throws IOException { 
  48.             if (strings.length != valueCount) { 
  49.                 throw invalidLengths(strings); 
  50.             } 
  51.             try { 
  52.                 for (int i = 0; i < strings.length; i++) { 
  53.                     lengths[i] = Long.parseLong(strings[i]); 
  54.                 } 
  55.             } catch (NumberFormatException e) { 
  56.                 throw invalidLengths(strings); 
  57.             } 
  58.         } 
  59.         private IOException invalidLengths(String[] strings) throws IOException { 
  60.             throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); 
  61.         } 
  62.         //臨時文件創建成功了以後 就會重命名為正式文件了 
  63.         public File getCleanFile(int i) { 
  64.             Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath()); 
  65.             return new File(directory, key + "." + i); 
  66.         } 
  67.         //tmp开头的都是临时文件 
  68.         public File getDirtyFile(int i) { 
  69.             Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath()); 
  70.             return new File(directory, key + "." + i + ".tmp"); 
  71.         } 

DiskLruCache的open函数的主要流程就基本走完了;

4、get()

  1. /** 
  2.    * Returns a snapshot of the entry named {@code key}, or null if it doesn't 
  3.    * exist is not currently readable. If a value is returned, it is moved to 
  4.    * the head of the LRU queue. 
  5.    * 通过key获取对应的snapshot 
  6.    */ 
  7.   public synchronized Snapshot get(String key) throws IOException { 
  8.     checkNotClosed(); 
  9.     validateKey(key); 
  10.     Entry entry = lruEntries.get(key); 
  11.     if (entry == null) { 
  12.       return null
  13.     } 
  14.     if (!entry.readable) { 
  15.       return null
  16.     } 
  17.     // Open all streams eagerly to guarantee that we see a single published 
  18.     // snapshot. If we opened streams lazily then the streams could come 
  19.     // from different edits. 
  20.     InputStream[] ins = new InputStream[valueCount]; 
  21.     try { 
  22.       for (int i = 0; i < valueCount; i++) { 
  23.         ins[i] = new FileInputStream(entry.getCleanFile(i)); 
  24.       } 
  25.     } catch (FileNotFoundException e) { 
  26.       // A file must have been deleted manually! 
  27.       for (int i = 0; i < valueCount; i++) { 
  28.         if (ins[i] != null) { 
  29.           Util.closeQuietly(ins[i]); 
  30.         } else { 
  31.           break; 
  32.         } 
  33.       } 
  34.       return null
  35.     } 
  36.     redundantOpCount++; 
  37.     //在取得需要的文件以后 记得在日志文件里增加一条记录 并检查是否需要重新构建日志文件 
  38.     journalWriter.append(READ + ' ' + key + '\n'); 
  39.     if (journalRebuildRequired()) { 
  40.       executorService.submit(cleanupCallable); 
  41.     } 
  42.     return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); 
  43.   } 

再看一下,validateKey

5、validateKey

  1. private void validateKey(String key) { 
  2.         Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); 
  3.         if (!matcher.matches()) { 
  4.           throw new IllegalArgumentException("keys must match regex " 
  5.                   + STRING_KEY_PATTERN + ": \"" + key + "\""); 
  6.         } 
  7.   } 

这里是对存储entry的map的key做了正则验证,所以key一定要用md5加密,因为有些特殊字符验证不能通过;

然后看这句代码对应的:

  1. if (journalRebuildRequired()) { 
  2.       executorService.submit(cleanupCallable); 
  3.     } 

对应的回调函数是:

  1. /** This cache uses a single background thread to evict entries. */ 
  2.   final ThreadPoolExecutor executorService = 
  3.       new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); 
  4.   private final Callable<Void> cleanupCallable = new Callable<Void>() { 
  5.     public Void call() throws Exception { 
  6.       synchronized (DiskLruCache.this) { 
  7.         if (journalWriter == null) { 
  8.           return null; // Closed. 
  9.         } 
  10.         trimToSize(); 
  11.         if (journalRebuildRequired()) { 
  12.           rebuildJournal(); 
  13.           redundantOpCount = 0; 
  14.         } 
  15.       } 
  16.       return null
  17.     } 
  18.   }; 

其中的trimTOSize():

6、trimTOSize()

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

就是检测总缓存是否超过了限制数量,

再来看journalRebuildRequired函数

7、journalRebuildRequired()

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

就是校验redundantOpCount是否超出了范围,如果是,就重构日志文件;

最后看get函数的返回值 new Snapshot()

  1. /** A snapshot of the values for an entry. */ 
  2. //这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容 
  3.   public final class Snapshot implements Closeable { 
  4.     private final String key
  5.     private final long sequenceNumber; 
  6.     private final InputStream[] ins; 
  7.     private final long[] lengths; 
  8.     private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { 
  9.       this.key = key
  10.       this.sequenceNumber = sequenceNumber; 
  11.       this.ins = ins; 
  12.       this.lengths = lengths; 
  13.     } 
  14.     /** 
  15.      * Returns an editor for this snapshot's entry, or null if either the 
  16.      * entry has changed since this snapshot was created or if another edit 
  17.      * is in progress. 
  18.      */ 
  19.     public Editor edit() throws IOException { 
  20.       return DiskLruCache.this.edit(key, sequenceNumber); 
  21.     } 
  22.     /** Returns the unbuffered stream with the value for {@code index}. */ 
  23.     public InputStream getInputStream(int index) { 
  24.       return ins[index]; 
  25.     } 
  26.     /** Returns the string value for {@code index}. */ 
  27.     public String getString(int index) throws IOException { 
  28.       return inputStreamToString(getInputStream(index)); 
  29.     } 
  30.     /** Returns the byte length of the value for {@code index}. */ 
  31.     public long getLength(int index) { 
  32.       return lengths[index]; 
  33.     } 
  34.     public void close() { 
  35.       for (InputStream in : ins) { 
  36.         Util.closeQuietly(in); 
  37.       } 
  38.     } 
  39.   } 

到这里就明白了get最终返回的其实就是entry根据key 来取的snapshot对象,这个对象直接把inputStream暴露给外面;

8、save的过程

  1. public Editor edit(String key) throws IOException { 
  2.     return edit(key, ANY_SEQUENCE_NUMBER); 
  3. //根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor 
  4. //同时在日志文件里加入一条对该key的dirty记录 
  5. private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { 
  6.     //因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化 
  7.     checkNotClosed(); 
  8.     //这个地方是校验 我们的key的,通常来说 假设我们要用这个缓存来存一张图片的话,我们的key 通常是用这个图片的 
  9.     //网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范 
  10.     validateKey(key); 
  11.     Entry entry = lruEntries.get(key); 
  12.     if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null 
  13.             || entry.sequenceNumber != expectedSequenceNumber)) { 
  14.         return null; // Snapshot is stale. 
  15.     } 
  16.     if (entry == null) { 
  17.         entry = new Entry(key); 
  18.         lruEntries.put(key, entry); 
  19.     } else if (entry.currentEditor != null) { 
  20.         return null; // Another edit is in progress. 
  21.     } 
  22.     Editor editor = new Editor(entry); 
  23.     entry.currentEditor = editor; 
  24.     // Flush the journal before creating files to prevent file leaks. 
  25.     journalWriter.write(DIRTY + ' ' + key + '\n'); 
  26.     journalWriter.flush(); 
  27.     return editor; 

然后取得输出流

  1. public OutputStream newOutputStream(int index) throws IOException { 
  2.         if (index < 0 || index >= valueCount) { 
  3.             throw new IllegalArgumentException("Expected index " + index + " to " 
  4.                     + "be greater than 0 and less than the maximum value count " 
  5.                     + "of " + valueCount); 
  6.         } 
  7.         synchronized (DiskLruCache.this) { 
  8.             if (entry.currentEditor != this) { 
  9.                 throw new IllegalStateException(); 
  10.             } 
  11.             if (!entry.readable) { 
  12.                 written[index] = true
  13.             } 
  14.             File dirtyFile = entry.getDirtyFile(index); 
  15.             FileOutputStream outputStream; 
  16.             try { 
  17.                 outputStream = new FileOutputStream(dirtyFile); 
  18.             } catch (FileNotFoundException e) { 
  19.                 // Attempt to recreate the cache directory. 
  20.                 directory.mkdirs(); 
  21.                 try { 
  22.                     outputStream = new FileOutputStream(dirtyFile); 
  23.                 } catch (FileNotFoundException e2) { 
  24.                     // We are unable to recover. Silently eat the writes. 
  25.                     return NULL_OUTPUT_STREAM; 
  26.                 } 
  27.             } 
  28.             return new FaultHidingOutputStream(outputStream); 
  29.         } 
  30.     } 

注意这个index 其实一般传0 就可以了,DiskLruCache 认为 一个key 下面可以对应多个文件,这些文件 用一个数组来存储,所以正常情况下,我们都是

一个key 对应一个缓存文件 所以传0

  1. //tmp开头的都是临时文件 
  2.      public File getDirtyFile(int i) { 
  3.          return new File(directory, key + "." + i + ".tmp"); 
  4.      } 

然后你这边就能看到,这个输出流,实际上是tmp 也就是缓存文件的 .tmp 也就是缓存文件的 缓存文件 输出流;

这个流 我们写完毕以后 就要commit;

  1. public void commit() throws IOException { 
  2.         if (hasErrors) { 
  3.             completeEdit(this, false); 
  4.             remove(entry.key); // The previous entry is stale. 
  5.         } else { 
  6.             completeEdit(this, true); 
  7.         } 
  8.         committed = true
  9.     } 

这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log

  1. //最后判断是否超过最大的maxSize 以便对缓存进行清理 
  2. private synchronized void completeEdit(Editor editor, boolean success) throws IOException { 
  3.     Entry entry = editor.entry; 
  4.     if (entry.currentEditor != editor) { 
  5.         throw new IllegalStateException(); 
  6.     } 
  7.     // If this edit is creating the entry for the first time, every index must have a value. 
  8.     if (success && !entry.readable) { 
  9.         for (int i = 0; i < valueCount; i++) { 
  10.             if (!editor.written[i]) { 
  11.                 editor.abort(); 
  12.                 throw new IllegalStateException("Newly created entry didn't create value for index " + i); 
  13.             } 
  14.             if (!entry.getDirtyFile(i).exists()) { 
  15.                 editor.abort(); 
  16.                 return
  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.                 File clean = entry.getCleanFile(i); 
  25.                 dirty.renameTo(clean); 
  26.                 long oldLength = entry.lengths[i]; 
  27.                 long newLength = clean.length(); 
  28.                 entry.lengths[i] = newLength; 
  29.                 size = size - oldLength + newLength; 
  30.             } 
  31.         } else { 
  32.             deleteIfExists(dirty); 
  33.         } 
  34.     } 
  35.     redundantOpCount++; 
  36.     entry.currentEditor = null
  37.     if (entry.readable | success) { 
  38.         entry.readable = true
  39.         journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 
  40.         if (success) { 
  41.             entry.sequenceNumber = nextSequenceNumber++; 
  42.         } 
  43.     } else { 
  44.         lruEntries.remove(entry.key); 
  45.         journalWriter.write(REMOVE + ' ' + entry.key + '\n'); 
  46.     } 
  47.     journalWriter.flush(); 
  48.     if (size > maxSize || journalRebuildRequired()) { 
  49.         executorService.submit(cleanupCallable); 
  50.     } 

commit以后 就会把tmp文件转正 ,重命名为 真正的缓存文件了;

这个里面的流程和日志文件的rebuild 是差不多的,都是为了防止写文件的出问题。所以做了这样的冗余处理;

总结

DiskLruCache,利用一个journal文件,保证了保证了cache实体的可用性(只有CLEAN的可用),且获取文件的长度的时候可以通过在该文件的记录中读取。

利用FaultHidingOutputStream对FileOutPutStream很好的对写入文件过程中是否发生错误进行捕获,而不是让用户手动去调用出错后的处理方法;

原文链接:https://mp.weixin.qq.com/s/9dU7lxKbItLfq046zztpgg

延伸 · 阅读

精彩推荐