DiskLruCache是一种管理数据存储的技术,单从Cache的字面意思也可以理解到,"Cache","高速缓存"。
前言
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、添加依赖
// add dependence implementation 'com.jakewharton:disklrucache:2.0.2'
2、创建DiskLruCache对象
/* * directory – 缓存目录 * appVersion - 缓存版本 * valueCount – 每个key对应value的个数 * maxSize – 缓存大小的上限 */ DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10);
3、添加 / 获取 缓存(一对一)
/** * 添加一条缓存,一个key对应一个value */ public void addDiskCache(String key, String value) throws IOException { File cacheDir = context.getCacheDir(); DiskLruCache diskLruCache = DiskLruCache.open(cacheDir, 1, 1, 1024 * 1024 * 10); DiskLruCache.Editor editor = diskLruCache.edit(key); // index与valueCount对应,分别为0,1,2...valueCount-1 editor.newOutputStream(0).write(value.getBytes()); editor.commit(); diskLruCache.close(); } /** * 获取一条缓存,一个key对应一个value */ public void getDiskCache(String key) throws IOException { File directory = context.getCacheDir(); DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 1, 1024 * 1024 * 10); String value = diskLruCache.get(key).getString(0); diskLruCache.close(); }
4、添加 / 获取 缓存(一对多)
/** * 添加一条缓存,1个key对应2个value */ public void addDiskCache(String key, String value1, String value2) throws IOException { File directory = context.getCacheDir(); DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024 * 1024 * 10); DiskLruCache.Editor editor = diskLruCache.edit(key); editor.newOutputStream(0).write(value1.getBytes()); editor.newOutputStream(1).write(value2.getBytes()); editor.commit(); diskLruCache.close(); } /** * 添加一条缓存,1个key对应2个value */ public void getDiskCache(String key) throws IOException { File directory = context.getCacheDir(); DiskLruCache diskLruCache = DiskLruCache.open(directory, 1, 2, 1024); DiskLruCache.Snapshot snapshot = diskLruCache.get(key); String value1 = snapshot.getString(0); String value2 = snapshot.getString(1); diskLruCache.close(); }
三、源码分析
1、open()
DiskLruCache的构造方法是private修饰,这也就是告诉我们,不能通过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; }
但是提供了open()方法,供我们获取DiskLruCache的实例,open方法如下:
/** * Opens the cache in {@code directory}, creating a cache if none exists * there. * * @param directory a writable directory * @param valueCount the number of values per cache entry. Must be positive. * @param maxSize the maximum number of bytes this cache should use to store * @throws IOException if reading or writing the cache directory fails */ 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"); } // 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,包括,大小,版本,路径,key对应多少value DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); //如果日志文件存在,就开始赌文件信息,并返回 //主要就是构建entry列表 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; }
open函数:如果日志文件存在,直接去构建entry列表;如果不存在,就构建日志文件;
2、rebuildJournal()
构建文件: //这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作 private final File journalFile; //journal文件的temp 缓存文件,一般都是先构建这个缓存文件,等待构建完成以后将这个缓存文件重新命名为journal private final File journalFileTmp; /** * 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(); } //指向journalFileTmp这个日志文件的缓存 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(); //这里也是把写入日志文件的writer初始化 journalWriter = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); }
再来看当日志文件存在的时候,做了什么
3、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 { //构建LruEntries entry列表 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 { //初始化写入文件的writer journalWriter = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(journalFile, true), Util.US_ASCII)); } } finally { Util.closeQuietly(reader); } }
然后看下这个函数里面的几个主要变量:
//每个entry对应的缓存文件的格式 一般为1,也就是一个key,对应几个缓存,一般设为1,key-value一一对应的关系 private final int valueCount; private long size = 0; //这个是专门用于写入日志文件的writer private Writer journalWriter; //这个集合应该不陌生了, private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true); //这个值大于一定数目时 就会触发对journal文件的清理了 private int redundantOpCount;
下面就看下entry这个实体类的内部结构
private final class Entry { private final String key; /** * Lengths of this entry's files. * 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1 */ private final long[] lengths; /** * True if this entry has ever been published. * 曾经被发布过 那他的值就是true */ private boolean readable; /** * The ongoing edit or null if this entry is not being edited. * 这个entry对应的editor */ private Editor currentEditor; @Override public String toString() { return "Entry{" + "key='" + key + '\'' + ", lengths=" + Arrays.toString(lengths) + ", readable=" + readable + ", currentEditor=" + currentEditor + ", sequenceNumber=" + sequenceNumber + '}'; } /** * The sequence number of the most recently committed edit to this entry. * 最近编辑他的序列号 */ private long sequenceNumber; private Entry(String key) { this.key = key; this.lengths = new long[valueCount]; } 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 { if (strings.length != valueCount) { throw invalidLengths(strings); } try { for (int i = 0; i < strings.length; i++) { lengths[i] = Long.parseLong(s
发表评论 取消回复