转载请标明出处:
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
2DIRTY 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进行了判断,如果超出,就通过线程池开启线程重建。

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

Android之DiskLruCache源码解析相关推荐

  1. BAT高级架构师合力熬夜15天,肝出了这份PDF版《Android百大框架源码解析》,还不快快码住。。。

    前言 为什么要阅读源码? 现在中高级Android岗位面试中,对于各种框架的源码都会刨根问底,从而来判断应试者的业务能力边际所在.但是很多开发者习惯直接搬运,对各种框架的源码都没有过深入研究,在面试时 ...

  2. http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

    http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

  3. Android通知系统源码解析

    Android通知系统源码解析 1. 概述 2. 流程图 2.1. 发送通知流程图 3. 源码解析 3.1. 使用通知--APP进程 3.1.1. 创建通知: 3.1.2. 发送(更新)通知: 3.1 ...

  4. Android Gradle Plugin 源码解析(上)

    一.源码依赖 本文基于: android gradle plugin版本: com.android.tools.build:gradle:2.3.0 gradle 版本:4.1 Gradle源码总共3 ...

  5. Android之EasyPermissions源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 我们知道在Android中想要申请权限就需要在AndroidManifest ...

  6. Android之AsyncTask源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 AsyncTask是一种轻量级的异步任务类,内部封装了Thread和Ha ...

  7. Android Hawk的源码解析,一款基于SharedPreferences的存储框架

    转载请标注:http://blog.csdn.net/friendlychen/article/details/76218033 一.概念 SharedPreferences的使用大家应该非常熟悉啦. ...

  8. Android之LocalBroadcastManager源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 广播想必大家都不陌生,日常开发中同一个APP中的多个进程之间需要进行传输信息 ...

  9. android debug database 源码解析

    我们今天分析下android debug database 的源码: 项目地址: https://github.com/amitshekhariitbhu/Android-Debug-Database ...

最新文章

  1. 汇编中的REPZ CMPSB
  2. python基本随机生成函数_Python学习笔记(三):随机生成函数方法
  3. 机器学习硕士、博士如何自救?
  4. XBMC源代码分析 3:核心部分(core)-综述
  5. [渝粤教育] 西南科技大学 公共事业管理概论 在线考试复习资料
  6. 两个小故事告诉你静下来的力量
  7. VMware Workstation Pro 12 安装黑苹果问题
  8. POC会成为下一个POW吗?
  9. centos漏洞系列(三):Google Android libnl权限提升漏洞
  10. 6轴串联关节机器人的奇异点
  11. 提升工作效率五步走之前两步 2016-09-18 思佳真探
  12. 原码、反码、补码和真值
  13. 二极管选型-二极管参数介绍
  14. 解决安卓11崩溃率高的问题
  15. 软件测试 | 常见代理工具
  16. 重温经典 15年IE浏览器大盘点
  17. 六:分布式架构存储设计
  18. Can not perform this action after onSaveInstanceState和重建Activity时恢复缓存的Fragmen的问题
  19. OKLink行业观察:投资数字资产的机构版图(二)微策略
  20. mysql sql语句生成日历表

热门文章

  1. 设计模式之强大的接口适配器模式,继承Thread or 实现Runnable?
  2. ThreeJS的特效合成器和后期处理通道
  3. Log4J xml配置
  4. Logstash inputs配置
  5. RouterOS和艾泰路由建立ipsec ×××连接
  6. 资源盗链困扰站长 安全狗内置盗链保护功能
  7. Playing Video on iPhone Cocos2D-X
  8. MySQL优化—磁盘事宜
  9. DB2 V8,V9并存在同一 server 的处理
  10. Spring AOP Capability and Goal