DiskLurCache
使用
打开缓存
-
打开缓存函数
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。
-
实际调用
public File getDiskCacheDir(Context context, String uniqueName) { String cachePath; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) { cachePath = context.getExternalCacheDir().getPath(); } else { cachePath = context.getCacheDir().getPath(); } return new File(cachePath + File.separator + uniqueName); } public int getAppVersion(Context context) { try { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return info.versionCode; } catch (NameNotFoundException e) { e.printStackTrace(); } return 1; } //调用open 函数, 这里的 open 函数, 当版本号改变后, //appVersionString, valueCountString 改变的时候, 会直接 报 IO异常 DiskLruCache mDiskLruCache = null; try { File cacheDir = getDiskCacheDir(context, "bitmap"); if (!cacheDir.exists()) { cacheDir.mkdirs(); } mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); } catch (IOException e) { e.printStackTrace(); }
//private void readJournal() throws IOException 函数.
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 + "]");
}
写入缓存
-
下载一张图片
//调用 URL 系在一张图片, 并写入 outputStream 中. private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); out = new BufferedOutputStream(outputStream, 8 * 1024); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { e.printStackTrace(); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) { e.printStackTrace(); } } return false; }
-
根据 URL 生成 MD5值, 唯一标识, 当作内部的 LURCache List的键
public String hashKeyForDisk(String key) { String cacheKey; try { final MessageDigest mDigest = MessageDigest.getInstance("MD5"); mDigest.update(key.getBytes()); cacheKey = bytesToHexString(mDigest.digest()); } catch (NoSuchAlgorithmException e) { cacheKey = String.valueOf(key.hashCode()); } return cacheKey; } private String bytesToHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(0xFF & bytes[i]); if (hex.length() == 1) { sb.append('0'); } sb.append(hex); } return sb.toString(); }
-
调用 edit(key) 获取 Editor 对象
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); //这里的 editor 里面可以生成一个 关于 dirty 文件的 output 对象, 并重写了 //输出流的 write()函数, 一旦写操作报 IO 异常, 会将 Entry 中的 hasError 置为 true, //在 commit 的时候会调用 commitEdit(false), 将 dirty 删除掉, //理想情况下是成功的, 那么会将文件保存为 clean 文件, 并记录一行 DIRTY操作, //下载图片使用的 outputStream 是editor 的, 也就是 指向 dirty 的 outputStream. //在调用 commit/abort 函数时,会将dirty文件转换为clean文件,或者删除掉. new Thread(new Runnable() { @Override public void run() { try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); if (downloadUrlToStream(imageUrl, outputStream)) { editor.commit(); } else { editor.abort(); } } //检查当前 存储的size 是否大于设置的maxSize, //大于, 将删除 LUR 中原本不常用的 文件, 直到 size < maxSize. mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start();
读取缓存
-
调用函数
public synchronized Snapshot get(String key) throws IOException
-
根据 URL 生成的KEY 去获取对应的文件, 获取 SnapShot 对象.
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
-
调用 SnapShot 对象里面输入流
输入流 指向的是 KEY 对应的 CLEAN 文件 的 InputStrean, 并且在 SnapSnot 中保存的是一个 inputStream 数组, 数组的长度是 valueCount(一个KEY 对应几个文件) 的大小,
每个 inputStream[] 保存的是对应的下标 文件, 具体的 获取文件的函数在 Entry 中, 调用 getCleanFile(index) 函数获取.
这里调用getInputStream(0), 将 inputStream 转换为 Bitmap 并显示出来.
try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
移除缓存
mDiskLruCache.remove(key); 调用 remove 函数, 关键判断为 key 值来删除
try {
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
String key = hashKeyForDisk(imageUrl);
mDiskLruCache.remove(key);
} catch (IOException e) {
e.printStackTrace();
}
将会在 journal 文件中写入一行 REMOVE 操作, 并将 redundantOpCount++
源码解析
概述
DiskLurCache 涉及到一个 journal 的文件, 这个文件保存 CLEAN, DIRTY, REMOVE, READ 操作
-
初始化一个 DiskLurCache 对象, 需要调用 open 函数
DiskLruCache.open(directory, appVersion, valueCount, maxSize) ;
-
关于写操作
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); os.write(...) os.falsh(); //提交写操作, 将之前写的 temp 文件保存为 clean 文件, //当之前写操作出现错误的时候, 会将文件删除, 并将 KEY 从 lur 中删除掉. editor.commit();
-
关于读操作
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); } Bitmap bitmap = BitmapFactory.decodeStream(is); imageView.setBitmap(bitmap); //关闭所有的 inputStream. snapShot.close();
journal 文件
journal文件你打开以后呢,是这个格式;
libcore.io.DiskLruCache
1
1
1
DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY be8bdac81c12a08e15988555d85dfd2b
CLEAN be8bdac81c12a08e15988555d85dfd2b 99
READ be8bdac81c12a08e15988555d85dfd2b
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 536788f4dbdffeecfbb8f350a941eea3
首先看前五行:ok,以上5行可以称为该文件的文件头,DiskLruCache初始化的时候,如果该文件存在需要校验该文件头。
DiskLruCache初始化的时候,如果该文件存在需要校验该文件头。
- 第一行固定字符串
libcore.io.DiskLruCache
- 第二行DiskLruCache的版本号,源码中为常量1
- 第三行为你的app的版本号,当然这个是你自己传入指定的
- 第四行指每个key对应几个文件,一般为1
- 第五行,空行
操作记录:
- DIRTY 表示一个entry正在被写入(其实就是把文件的OutputStream交给你了)。那么写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。
- REMOVE除了上述的情况呢,当你自己手动调用remove(key)方法的时候也会写入一条REMOVE记录。
- READ就是说明有一次读取的记录。
- 每个CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字(参照文件头第四行)。
DiskLruCache#open
-
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. 每个缓存条目的值数量. 每个 KEY 对应的文件 * @param maxSize the maximum number of bytes this cache should use to store 用于存储的最大字节数. * @throws IOException if reading or writing the cache directory fails 当写文件和度文件失败, 会抛出异常. */ //创建 DiskLruCache 对象, 并初始化文件存放的地址. 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"); } //查找 bkp 文件是否存在, 不存在 // 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. //即存在 bkp 文件又存在 journal 文件, 删除backup 文件, //存在 bkp 文件,但是不存在 journal文件, 将文件重命名. 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); //根据文件夹信息, 创建对应的源信息, 和backup , 和临时操作的文件. 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; }
-
重新创建 journal 文件 (1. 文件不存在, 2. 文件存在, 但是多余的操作超过 2000, 为了保证 journal 文件的大小, 会重新生成文件.)
/** * 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(); } //新将数据写到 tmp 文件中. 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"); //重新写文件, REMOVE, READ 操作会被干光, 重新写文件. for (Entry entry : lruEntries.values()) { //判断entry 是否是脏数据的存在. 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()) { //在清除 REMOVE, READ 操作的时候, 如果 之前存在 journalFile, 那么将文件保存为 Backup 文件, 并将源文件删除. renameTo(journalFile, journalFileBackup, true); } //将 tmp 文件重新保存为 journalFile 文件, 但是不删除 tmp 文件. renameTo(journalFileTmp, journalFile, false); //在转换成功后, 将 备份文件也删除掉, 在 DiskLurCache 的每关于文件的操作都会将 IO 异常抛出去, //这里就是当 renameTo() 这个函数被抛出了 IO 异常的时候备份文件不会被删除掉. journalFileBackup.delete(); journalWriter = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); }
-
初始化的时候, 文件存在, 读取文件行, 并保证 lur 中保存的记录是只有 CLEAN, 而没有 REMOVE/ DIRTY 操作的 KEY - ENTRY
//只会在初始化的时候被调用 open() 函数的时候才会被调用. /** * 1. 通过文件头来检测是否是 journal 文件, 如果不是, 直接报IO 异常, * 2. 对文件进行while 循环, 一直跑到捕获文件尾异常, * 2.1. while 循环中会将 标签为 CLEAN 标志的标签的KEY 添加到 LUR 数组中去, * 但是当在轮询中碰到 Remove 的操作标签, 会将 对应的 KEY 从原本的 LUR 数组中移除, * * 2.3. 判断3个状态, REMOVE, CLEAN, DIRTY, * REMOVE: 会删除在 LUR 中的 KEY 值. * CLEAN: 生成一个 Entry(不管是不是空的), * 设置 currentEditor = null, * 设置 readable = true (可读) * 设置 lengths, 也是通过空格来区分的. * * DIRTY: 脏数据, 如果文件是脏数据(正在操作)时. 分配一个新的 Editor(关于 entry的 Editor(文件流)) * REMOVE, CLEAR, DIRTY, READ, 和KEY 之间都有空格, 他们之间的判断第一个是名称, 第二个是空格的数量. * * 2.4. 统计当前文件中多余的操作次数: * 文本的行数 - LUR.size() = 多余操作次数. * * 2.5. 判断文件的读写是否是异常停止, 文件未读到末尾, 则调用 reBuildJournal() 函数. 重写生成 journal 文件. * * 2.6. journalWriter 初始化 journalWriter, 写字段到 journal文件中的 输出流. * * @throws IOException */ 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) { //捕获 crash 来跳出循环. break; } } //多余的操作次数. redundantOpCount = lineCount - lruEntries.size(); // If we ended on a truncated line, rebuild the journal before appending to it. //函数执行错误, 未结束, 但是报了 EOFException 错误. if (reader.hasUnterminatedLine()) { rebuildJournal(); } else { //初始化 write. journalWriter = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(journalFile, true), Util.US_ASCII)); } } finally { //关闭 reader 流. Util.closeQuietly(reader); } } private void readJournalLine(String line) throws IOException { //切割字符串, 准备判断关于 KEY 值的状态, 是否需要被删除掉. 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); //判断状态是否被标志位 REMOVE. 是的话, 将会被移除. if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { //lruEntries 是没有数据的. 删除对应的标识, 也就是 一个 key(一个文件) 的可能被多次操作. lruEntries.remove(key); return; } } else { key = line.substring(keyBegin, secondSpace); } //将key 值和 Entry() 保存在 lruEntries 中. 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)) { //当前的状态为 Clean , 即将数据保存起来了, 或者洗衣个将会删除数据. //获取key 之后的string, 使用空格分割, 分割出来的 lengths 即时对应的 entry lengths 的值. 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); //如果文件是脏数据(正在操作)时. 分配一个新的 Editor(关于 entry的 Editer(文件流)) } 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. * * 1. 计算已经保存文件的长度,(CLEAN 标记的) * 2. 删除 DIRTY 标记的条目对应的 文本文件和 设置 entry 为null. * 并将自己从原本的LUR数列中删除掉, 擦除记录. */ private void processJournal() throws IOException { deleteIfExists(journalFileTmp);//删除 临时文件. for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { Entry entry = i.next(); //文件的操作点是 CLEAR. 也就是干净的, if (entry.currentEditor == null) { //valueCount 是针对一个 key 能存多上个 value., 数据存在 Entry 里面. for (int t = 0; t < valueCount; t++) { //增加文件的长度, size. size += entry.lengths[t]; } } else { entry.currentEditor = null; //删除对应的 cleanFIle 和脏数据, 只要key 值被标记了 REMOVE / 脏数据操作的标记, 那么之前就会有 CLEAN 操作 // 这个地方会将原本的操作也删除掉. for (int t = 0; t < valueCount; t++) { deleteIfExists(entry.getCleanFile(t)); deleteIfExists(entry.getDirtyFile(t)); } //从数组中删除自己. i.remove(); } } }
-
open 总结
经过open以后,journal文件肯定存在了;lruEntries里面肯定有值了;size存储了当前所有的实体占据的容量;。
存入缓存
-
示例
String key = generateKey(url); DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); //...after op editor.commit();
-
调用对外部提供的 edit 函数, 获取 Editor 对象
/** * Returns an editor for the entry named {@code key}, or null if another * edit is in progress. * * 对外开发 获取 Editor 对象的函数, 根据 KEY获取 Editor 对象 */ public Editor edit(String key) throws IOException { return edit(key, ANY_SEQUENCE_NUMBER); } /** * 创建一个新的 Editor 对象, 将从 LUR 里面获取的 Entry / 重新创建的 Entry 对象赋值到 Editor 上面去. * 并给 Entry 赋值 entry.currentEditor = editor. * * 在日志文件中写入 DIRTY 操作日志. * * 注: 在初始化 Editor 之前,会先判断 entry.currentEditor != null , * 如果 * * 1. 检查当前的 write 是否被close, 检查key值的正常 * 2. 创建一个新的 KEY / LUR 中获取, 并将 editor.currentEntry 指向当前的 entry. * 3. 记录一行脏数据操作, 并返回 Editor对象, */ 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. } //当 entry 为 null 时, 创建一个 entry 对象并保存到 LUR 里面去. if (entry == null) { entry = new Entry(key); lruEntries.put(key, entry); //当前的 editor 正在被操作, 直接返回 null. } else if (entry.currentEditor != null) { return null; // Another edit is in progress. } //创建一个新的 Editor对象, 并设置 currentEditor 对象为之前的 Entry, 或者 lruEntries.get(key) 获取的editor Editor editor = new Editor(entry); entry.currentEditor = editor; //设置脏当前的KEY 为脏数据标记. // Flush the journal before creating files to prevent file leaks. journalWriter.write(DIRTY + ' ' + key + '\n'); journalWriter.flush(); return editor; }
-
Editor/ Entry 对象
//空的 output , 对write 做的操作都不做任何事情. private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { @Override public void write(int b) throws IOException { // Eat all writes silently. Nom nom. } }; /** Edits the values for an entry. */ public final class Editor { private final Entry entry; //对应的 写入者. private final boolean[] written; //FilterOutputStream 的任何流操作,出了异常,都会被标记为 error true. private boolean hasErrors; //是否提交完成标记 private boolean committed; private Editor(Entry entry) { this.entry = entry; this.written = (entry.readable) ? null : new boolean[valueCount]; } /** * Returns an unbuffered input stream to read the last committed value, * or null if no value has been committed. * inputStream 需要外部自己close. */ public 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)); } catch (FileNotFoundException e) { return null; } } } /** * Returns the last committed value as a string, or null if no value * has been committed. */ public String getString(int index) throws IOException { InputStream in = newInputStream(index); return in != null ? inputStreamToString(in) : null; } /** * Returns a new unbuffered output stream to write the value at * {@code index}. If the underlying output stream encounters errors * when writing to the filesystem, this edit will be aborted when * {@link #commit} is called. The returned output stream does not throw * IOExceptions. */ /** * index 指的是在用户传入的 一个key 对应几个 文件的下标, */ public OutputStream newOutputStream(int index) throws IOException { if (index < 0 || index >= valueCount) { throw new IllegalArgumentException("Expected index " + index + " to " + "be greater than 0 and less than the maximum value count " + "of " + valueCount); } synchronized (DiskLruCache.this) { if (entry.currentEditor != this) { throw new IllegalStateException(); } //当对应的 entry 对应的 KEY 之前没有被写入, 那么 WRITTEN[i] 会被置为 true. if (!entry.readable) { written[index] = true; } //获取 outPut 写入文件的存放位置在 脏文件中,(.tmp), 其中是根据 index 来命名的. File dirtyFile = entry.getDirtyFile(index); FileOutputStream outputStream; //两次打开文件, 第一次可以判断文件夹不存在的时候, 会创建文件夹, //然后, 再次打开文件, //如果还是报错误了, 会返回一个对 Write 无处理的的 output, 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; } } //返回正常情况下的 OutPutStream., //数据操作中的 OutputWriter 操作的数据会被直接写入到脏文件中保存. return new FaultHidingOutputStream(outputStream); } } /** Sets the value at {@code index} to {@code value}. */ public void set(int index, String value) throws IOException { //单独创建一个 Writer 对像, 将数据保存到指定文件中. Writer writer = null; try { writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); 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. * * 提交数据到硬盘上,当 hasErrors 不为 true 时, * 会调用 completeEdit(this, true) 函数将 数据写入到指定位置, * * 将tmp 文件写成 clean 文件. */ public void commit() throws IOException { if (hasErrors) { completeEdit(this, false); remove(entry.key); // The previous entry is stale. } else { completeEdit(this, true); } committed = true; } /** * Aborts this edit. This releases the edit lock so another edit may be * started on the same key. * * 将temp 文件删除掉. */ public void abort() throws IOException { completeEdit(this, false); } //在提交中, 想要中断提交. public void abortUnlessCommitted() { if (!committed) { try { abort(); } catch (IOException ignored) { } } } //输出流的写函数都被 try/catch, 一旦报错,就在commit 的时候, 将 DIRTY 文件删除掉 //并将对应的 KEY 从对应的 LUR 数组中移除. private class FaultHidingOutputStream extends FilterOutputStream { private FaultHidingOutputStream(OutputStream out) { super(out); } @Override public void write(int oneByte) { try { out.write(oneByte); } catch (IOException e) { hasErrors = true; } } @Override public void write(byte[] buffer, int offset, int length) { try { out.write(buffer, offset, length); } catch (IOException e) { hasErrors = true; } } @Override public void close() { try { out.close(); } catch (IOException e) { hasErrors = true; } } @Override public void flush() { try { out.flush(); } catch (IOException e) { hasErrors = true; } } } } private final class Entry { //entry 对应的KEY值, 唯一标识符 private final String key; /** Lengths of this entry's files. * 一个 KEY 对应的文件个数, 使用lengths 来表示. * */ private final long[] lengths; /** True if this entry has ever been published. * 设置当前文件是否 可读, 在被成功写入时 "CLEAR" 会伴随这对应的 entry.readable 被标记为true. * readJournalLine() 时, 当前的标志为 CLEAN时, 会被标记为可读. * */ private boolean readable; /** The ongoing edit or null if this entry is not being edited. * 当前编辑, 当状态为 Clear 是, 这个对象为 null, * 当为脏数据时, currentEditor 不为空. * */ private Editor currentEditor; /** The sequence number of the most recently committed edit to this entry. */ //最近提交的序列号, 主要的用处是 Snapshot 对象获取快照, 但是原本的文件被改动了, 就直接return null. private long sequenceNumber; //输入一个新 KEY, 并创建一个 长度为 valueCount 的 int 数组 lengths private Entry(String key) { this.key = key; this.lengths = new long[valueCount]; } //根据 lengths 的个数,返回 String, 使用分隔符 ' ' 分割. 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". * 这里的 valueCount 是指的起初 open() 设置进来的一个 Key 对应几个文件, * 其中每个文件以 0,1,2,3,... 来区分. * * 将对应文件的长度设置进来. * */ private void setLengths(String[] strings) throws IOException { if (strings.length != valueCount) { throw invalidLengths(strings); } //保存 lengths 到 Entry 的 length 上面, 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)); } //对应的文件使用 test.0, test.1 ... 来存放 public File getCleanFile(int i) { return new File(directory, key + "." + i); } //对应的文件使用 test.0, test.1 ... 来存放 public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); } }
-
对 outPutSrream 操作的关键操作, 调用 edit的commit 函数才会被调用, 在editor 参数被调用的 时候, 返回的 OutputStream 指向的是 DIRTY 文件, 不存在,创建.
/** * 1. 保存/删除 缓存文件 --> success. success = true 时, 将脏数据文件 写成对应的 clean 文件, 并删除原本的脏数据文件 * success = false时, 将脏数据直接删除. 不保存成 clean文件, * 2. 多余的操作++ (DIRTY). * * 3. 检查是否关于 valueCount 长度的 output 都同事被操作聊(第一次的时候) , 不然直接报错了. readable(只有已经被写入的文件才会有这个标记,) * * 4. * */ 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. //第一次写入文件,必须每个对应的index 都有值, 不然这个地方会 报错, // 当 valueCount = 1 的时候, 这个比较好理解, //当 valueCount = 3 的时候, 每次提交新的 KEY 的时候, 对应的 index 必须在 commit() 函数之前先调用, //不然在这个位置会报错误, if (success && !entry.readable) { for (int i = 0; i < valueCount; i++) { //written 这个数组是在 readable 为 false 的时候才会被置为 true. //也就是第一次使用对应KEY 时才会被置为TRUE. if (!editor.written[i]) { editor.abort(); //新创建的条目没有为索引创建价值 throw new IllegalStateException("Newly created entry didn't create value for index " + i); } //获取使用 newOutPutStream 操作的那个脏文件是否存在, //不存在时, 直接 return , 并记那个 success 置为false. if (!entry.getDirtyFile(i).exists()) { //删除对应的脏文件, 并重新写入对应的 CLEAN / REMOVE 的记录. editor.abort(); return; } } } //将对应的脏文件修改为CLEAN 文件. for (int i = 0; i < valueCount; i++) { File dirty = entry.getDirtyFile(i); if (success) { //对应的 index 文件存在的时候,才会将文件 rename. if (dirty.exists()) { File clean = entry.getCleanFile(i); dirty.renameTo(clean); //重新设置 size 大小, 相对脏文件. long oldLength = entry.lengths[i]; long newLength = clean.length(); entry.lengths[i] = newLength; size = size - oldLength + newLength; } } else { //success = false 时, 将删除掉 脏数据文件. deleteIfExists(dirty); } } redundantOpCount++; entry.currentEditor = null; //当 readable 为true (之前被写入过), 或者 success时, 将重新写入 SIZE. if (entry.readable | success) { entry.readable = true; //重新写入 文件lengths 长度, 当对应的文件没有数据时, 长度为0. journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); //当重新写入了 CLEAN 时, entry 会被重新写入 sequenceNumber. if (success) { entry.sequenceNumber = nextSequenceNumber++; } } else { //删除 LUR 里面的 KEY , 并写入 REMOVE 操作. lruEntries.remove(entry.key); journalWriter.write(REMOVE + ' ' + entry.key + '\n'); } //关闭写入操作 journalWriter.flush(); //重新计算大小. if (size > maxSize || journalRebuildRequired()) { executorService.submit(cleanupCallable); } }
-
保证文件大小限制大小操作
//执行的时机: //1. 在使用 get() 函数获取 Snapshot 对象的时候, 可能会触发一次 if (journalRebuildRequired()) //2. 重新设置 MaxSize setMaxSize(maxSize) if( journalRebuildRequired() ) //3. 调用数据提交时: completeEdit(); if (size > maxSize || journalRebuildRequired()) //4. 调用 remove(key) 函数时. if( journalRebuildRequired() ) // journalRebuildRequired() 函数, 判断的是 : // redundantOpCount 参数: 多余的操作次数: 1. 在调用 readJournal() 的时候会只有 clean() 并没有被移除的条目会被添加到 LUR 中, // 然后其他的多余的操作还有 remove() 会写入 REMOVE 字段, // completeEdit() 的时候会写入 DIRTY 字段, // get() 的时候会显示 READ 参数. private final Callable<Void> cleanupCallable = new Callable<Void>() { public Void call() throws Exception { synchronized (DiskLruCache.this) { if (journalWriter == null) { return null; // Closed. } //当 当前文件 SIZE 大于设置进来的 maxSize 值, 会移除不常使用的文件, //保证保存的文件是 最常使用的, 并全部长度加起来 < maxSize. //LUR 算法的优势. trimToSize(); if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0; } } return null; } };
-
检测文件大小操作
//只有当 多余操作大于 2000 并且多余操作大于等于 缓存数据的长度. private boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold // && redundantOpCount >= lruEntries.size(); }
-
检查 SIZE 是否大于 maxSize, 大于的时候,会删除不常用的 文件, 调用 remove 函数, 删除 KEY对应的文件.
调用时机:
- cleanupCallable() 被调用
- close() 被调用时
- flash() 被调用时
/** * 借助 LUR 算法的帮助, 函数 trimToSize() 会删除最近不常使用的 key. * http://blog.csdn.net/justloveyou_/article/details/71713781 */ private void trimToSize() throws IOException { while (size > maxSize) { Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); remove(toEvict.getKey()); } }
读操作
-
示例
try { String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg"; String key = hashKeyForDisk(imageUrl); DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key); if (snapShot != null) { InputStream is = snapShot.getInputStream(0); Bitmap bitmap = BitmapFactory.decodeStream(is); mImage.setImageBitmap(bitmap); } } catch (IOException e) { e.printStackTrace(); }
get() 函数
/**
* 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.
*
* 1. 通过 KEY 拿到 readable 的 entry 对象, 可以被读写的 CLEAN 标记的 文件
* 2. 申请一个 长度为 valueCount 的 InputStream 数据, 并将对应 entry 的 clean 文件赋值给 inputStream
* 3. 为 KEY 写入 READ 标记, 多余的操作++
* 4. 返回一个 Snapshot对象.
*/
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);
}
其他操作
-
remove(key)
/** * 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. * * 1. 判断当前entry 的 currentEditor 是否为null, 为null 证明没有在操作, * 在被操作, 直接返回 false. * 2. 根据 valueCount 长度, 循环减去 将要被删除的文件长度大小, 更新 size 大小, * 重置 entry lengths 全部为0. * * 3. 多余的操作++ , 从 LUR 中删除 key, 和 检查当前多余操作是否过多, * 如果过多会被指向重写生成 journal文件,删除文件中 REMORE, READ 操作, 和已经被删除文件的 * KEY 操作 */ public synchronized boolean remove(String key) throws IOException { checkNotClosed(); //判断 写入流 是否为空, validateKey(key); Entry entry = lruEntries.get(key); //当当前文件还在被编辑, 或者当前的 entry 不被保存在 LUR 里面, 会直接返回 false. if (entry == null || entry.currentEditor != null) { return false; } //删除 对应的cleanFile 文件, 一个 key 对应对个文件. 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 大小 size -= entry.lengths[i]; //修改entry 的长度 length 大小 entry.lengths[i] = 0; } redundantOpCount++; //写入 remove 操作条例到文件中. journalWriter.append(REMOVE + ' ' + key + '\n'); //删除key. lruEntries.remove(key); //检测多余的操作数 > 2000 && > lur.size(). if (journalRebuildRequired()) { //重新 rebuilder. executorService.submit(cleanupCallable); } return true; }
public File getDirectory() : 返回当前缓存数据的目录
public synchronized long getMaxSize() : 获取设置的缓存的最大大小
public synchronized long size() : //获取当前存储的 占用 硬盘空间, byte.
public synchronized boolean isClosed() : 判断写 日志文件的 journalWriter是否被 close() 掉了, 置为null
public synchronized void flush() : 调用 trimToSize() 函数, 和关闭 调用 journalWriter 的 flush() 函数
public synchronized void close() : 关闭当前写 LOG 文件的 journalWriter , 并停止所有的 写操作, 中断
public void delete() : 删除传入的 缓存目录, 递归删除全部的文件 .
Done.