DiskLruCache概述
简单来说DiskLruCache是一款数据缓存框架。此处的Cache特指应用常需要保存的数据。那么什么数据可以进行存储呢?简单来说,就是可以存储一切由二进制组成的数据,常见的包括JSON数据,文件,图片,音视频等等。特别强调一下,DiskLruCache是多线程安全的,支持多个线程同时操作。因为内部巧妙的使用了synchronized关键字(同步监视器),而且内部维护了一个单线程模式的线程池,用来处理核心任务。这款框架可以植入到任何使用java开发的软件上,不仅仅是安卓,因为没有使用安卓SDK提供的api,而是一款纯Java输出的框架。
想要深入学习DiskLruCache框架需要储备的知识
LRU概念?如何使用LinkedHashMap实现Lru策略功能?最好把实现的原理了解清楚(拓展:如何使用栈,数组,队列实现LRU策略,各方式的优劣势)
清楚基本操作文件的Api,重要的需要知道如何实现:一行一行的输出一个文件的内容?提醒(可以使用BufferReader实现,不过最好还是基于InputStream来实现,多了解几种实现方案)
需稍微知道正则的匹配规则
DiskLruCahe缓存设计逻辑
框架设计图如上图在使用框架之前,框架需要提取日志文件来初始化框架需要的数据,比如提取所有已保存数据到内存中。
框架基本操作规则或需要注意地方
-
框架保存的数据以key_value形式。为了增加Value的丰富性,支持一对多的保存方式。简单来说,一个key可以保存多个数据
-
Key作为缓存数据的ID,在框架中具有唯一性,就像Java集合中的Set一样,不能保存相同Key的数据。在命名上Key的规则需要匹配”[a-z0-9_-]{1,64}“规范,特别注意的是不能保存除这以外的符号,否则将会报错。
-
在框架中每一个Value将会以一个文件的新式存在。文件的命名规范为:Key+"."+"index"。此处的index的作用为:标识数据保存的位置。比如要保存一对多形式的数据:wnp => 1 wnp => "abc" , 那么保存数据1的文件名为wnp.0 , 保存的数据”abc“的文件名为wnp.1。这里可以看到允许同一个key保存不一样类型的数据,那么如何区别呢?比如在查询的时候,查找wnp的数据,此时输入的index为0,那么将返回1。如果此时输入的index为1,那么将返回”abc“。
-
日志操作文件用于记录、管理保存的数据。比如进行查询,插入,删除的时候都得记录在案。在数据管理上框架采用LRU的缓存策略,通过LRU算法对缓存进行管理,以最近最少使用作为管理的依据,删除最近最少使用的数据,保留最近最常用的数据。日志文件除了保存数据的日志外,还会在文件的开头填入关键信息。比如:文件的魔数“libcore.io.DiskLruCache” ,文件的固定值“1” , 版本号,一个Key对应的保存数据数量的最大值。
下图为日志文件结构介绍
libcore.io.DiskLruCache//魔数
1//固定值
1//版本号
2//key可保存的数据数量,此处为一个Key保存两个数据
DIRTY wnp5 //处于正在编辑状态的wnp5
CLEAN wnp5 6 6 //wnp5的数据保存成功 后面的数字为保存的数据大小,单位为字节。第一个6表示第一个数据保存的大小,第二个6表示第二个数据保存的大小
READ wnp5//外部程序访问了wnp5的数据
REMOVE wnp5 //外部程序删除了wnp5的数据
DisLruCache内部代码逻辑
- 初始化
//进行初始化
DiskLruCache diskLruCache = DiskLruCache.open(new File("D:\\User\\project\\javaProject\\GennericParadigm\\src\\cache"),1, 2, 50 * 1024 * 1024);
//源码解析
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
//非法参数判断
if (maxSize <= 0L) {
throw new IllegalArgumentException("maxSize <= 0");
} else if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
} else {
//检查是否存在备份文件,如果存在则进行替换正式文件,或者删除备份文件
File backupFile = new File(directory, "journal.bkp");
if (backupFile.exists()) {
File journalFile = new File(directory, "journal");
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
//构造DiskLruCache对象,里面进行一些重要数据的初始化,也就是初始化环境数据
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 var8) {
System.out.println("DiskLruCache " + directory + " is corrupt: " + var8.getMessage() + ", removing");
cache.delete();
}
}
directory.mkdirs();
//重新创建对象,初始化数据
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();//重新创建日志文件
return cache;
}
}
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
//重点研究一下此线程池的创建输入的参数。核心线程池数为0,最大线程数为1,存活时间为60s,使用链表式的阻塞队列
this.executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
//进行清除多余数据或者实行LRU策略时执行的任务
this.cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized(DiskLruCache.this) {
if (DiskLruCache.this.journalWriter == null) {
return null;
} else {
DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
DiskLruCache.this.rebuildJournal();//重构日志文件
DiskLruCache.this.redundantOpCount = 0;
}
return null;
}
}
}
};
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, "journal");//真正的Journal日志记录文件
this.journalFileTmp = new File(directory, "journal.tmp");//Journal的临时文件
this.journalFileBackup = new File(directory, "journal.bkp");//Journal的备份文件
this.valueCount = valueCount;
this.maxSize = maxSize;
}
//提取日志文件的关键信息,初始化框架运行环境
private void readJournalLine(String line) throws IOException {
int firstSpace = line.indexOf(32);//32为空格的ASIIC码
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
} else {
int keyBegin = firstSpace + 1;
int secondSpace = line.indexOf(32, keyBegin);
String key;
if (secondSpace == -1) {
key = line.substring(keyBegin);
if (firstSpace == "REMOVE".length() && line.startsWith("REMOVE")) {
this.lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//可以代表提取到存储的数据的ID
if (entry == null) {
entry = new DiskLruCache.Entry(key);
this.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 DiskLruCache.Editor(entry);
} else if (secondSpace != -1 || firstSpace != "READ".length() || !line.startsWith("READ")) {
throw new IOException("unexpected journal line: " + line);
}
}
}
//提取完日志文件之后,需要对提取的信息做第二次优化,也就是删除一部分无效的数据
private void processJournal() throws IOException {
deleteIfExists(this.journalFileTmp);
Iterator i = this.lruEntries.values().iterator();
while(true) {
while(i.hasNext()) {
DiskLruCache.Entry entry = (DiskLruCache.Entry)i.next();
int t;
if (entry.currentEditor == null) {
for(t = 0; t < this.valueCount; ++t) {
this.size += entry.lengths[t];
}
} else {
entry.currentEditor = null;
for(t = 0; t < this.valueCount; ++t) {
deleteIfExists(entry.getCleanFile(t));
deleteIfExists(entry.getDirtyFile(t));
}
i.remove();
}
}
return;
}
}
- 读取
//使用例子:
DiskLruCache.Snapshot snapshot2 = diskLruCache.get("wnp5");
if (snapshot != null) {
String str = snapshot2.getString(0);
snapshot.close();
System.out.println(str);
} else {
System.out.println("不存在此条缓存");
}
//源码:
public synchronized DiskLruCache.Snapshot get(String key) throws IOException {
//检查是否为非法输入和环境状态
this.checkNotClosed();
this.validateKey(key);
DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中获取对应的key的数据
if (entry == null) {
return null;
} else if (!entry.readable) {//当前数据标志位不可读
return null;
} else {
InputStream[] ins = new InputStream[this.valueCount];//数组用来保存数据的输入流
try {
for(int i = 0; i < this.valueCount; ++i) {
ins[i] = new FileInputStream(entry.getCleanFile(i));//创建保存文件的输入流
}
} catch (FileNotFoundException var6) {
for(int i = 0; i < this.valueCount && ins[i] != null; ++i) {
Util.closeQuietly(ins[i]);
}
return null;
}
++this.redundantOpCount;
this.journalWriter.append("READ " + key + '\n');//追加记录到日志文件中
if (this.journalRebuildRequired()) {
this.executorService.submit(this.cleanupCallable);//清除日志文件无用的数据
}else{
this.journalWriter.flush();//完成对日志文件的记录追加。
}
return new DiskLruCache.Snapshot(key, entry.sequenceNumber, ins, entry.lengths);//可以对保存的数据进行访问
}
}
- 写
//使用例子:
DiskLruCache diskLruCache = DiskLruCache.open(new File("D:\\User\\project\\javaProject\\GennericParadigm\\src\\cache"),
1, 2, 50 * 1024 * 1024);
DiskLruCache.Editor editor = diskLruCache.edit("wnp5");//开启对wnp5的编辑
editor.set(0, "微信");//对位置0,进行数据设置
editor.set(1, "微信");//对位置1,进行数据设置
editor.commit();//完成编辑
//源码解析:
private synchronized DiskLruCache.Editor edit(String key, long expectedSequenceNumber) throws IOException {
// 检查是否为非法输入和环境状态
this.checkNotClosed();
this.validateKey(key);
DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中获取对应的数据
if (expectedSequenceNumber == -1L || entry != null && entry.sequenceNumber == expectedSequenceNumber) {
//进入此方法体内,可能为修改旧数据操作,可能为新插入的新数据两种操作
if (entry == null) {//新数据
entry = new DiskLruCache.Entry(key);
this.lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {//如果当前数据已经在编辑了,那么不可以在进行编辑
return null;
}
DiskLruCache.Editor editor = new DiskLruCache.Editor(entry);
entry.currentEditor = editor;//标志着此数据正在编辑状态
this.journalWriter.write("DIRTY " + key + '\n');//写入正在编辑状态的记录
this.journalWriter.flush();
return editor;//返回可编辑的数据实例
} else {
return null;
}
}
public void set(int index, String value) throws IOException {
// 创建对应要保存数据的文件输出流。文件名的规则为:this.key + "." + index + ".tmp"
// 用于临时保存
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(this.newOutputStream(index), Util.UTF_8);
writer.write(value);//写入到临时文件中去
} finally {
Util.closeQuietly(writer);
}
}
public void commit() throws IOException {
if (this.hasErrors) {//如果在操作临时文件的过程中发生错误,那么当此次保存失效,需要进行清除
DiskLruCache.this.completeEdit(this, false);
DiskLruCache.this.remove(this.entry.key);
} else {
DiskLruCache.this.completeEdit(this, true);//b保存成功
}
this.committed = true;
}
private synchronized void completeEdit(DiskLruCache.Editor editor, boolean success) throws IOException {
DiskLruCache.Entry entry = editor.entry;
if (entry.currentEditor != editor) {//非法操作
throw new IllegalStateException();
} else {
int i;
if (success && !entry.readable) {//保存成功,但是文件不可读,那么需要将数据进行删除
for(i = 0; i < this.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(i = 0; i < this.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;
this.size = this.size - oldLength + newLength;//改变占用的物理内存大小记录
}
} else {//保存失败
deleteIfExists(dirty);//删除临时文件
}
}
++this.redundantOpCount;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
this.journalWriter.write("CLEAN " + entry.key + entry.getLengths() + '\n');//添加记录,保存完成
if (success) {
entry.sequenceNumber = (long)(this.nextSequenceNumber++);
}
} else {
this.lruEntries.remove(entry.key);
this.journalWriter.write("REMOVE " + entry.key + '\n');//添加记录,删除成功
}
this.journalWriter.flush();
if (this.size > this.maxSize || this.journalRebuildRequired()) {//进行Lru缓存策略管理,可能进行清除无用的文件
this.executorService.submit(this.cleanupCallable);
}
}
}
- 删除
public synchronized boolean remove(String key) throws IOException {
this.checkNotClosed();//检查是否为正常的环境状态
this.validateKey(key);//检查Key是否符合命名规则
DiskLruCache.Entry entry = (DiskLruCache.Entry)this.lruEntries.get(key);//从内存中读取对应的key
if (entry != null && entry.currentEditor == null) {//不为空,entry.currentEditor == null标识没有在编辑状态
for(int i = 0; i < this.valueCount; ++i) {
File file = entry.getCleanFile(i);//根据规则获取对应key保存的数据文件
if (file.exists() && !file.delete()) {//进行数据文件的删除
throw new IOException("failed to delete " + file);
}
this.size -= entry.lengths[i];//this.size标志着当前缓存数据的大小
entry.lengths[i] = 0L;
}
++this.redundantOpCount;
this.journalWriter.append("REMOVE " + key + '\n');//往日志文件中追加REMOVE key记录
this.lruEntries.remove(key);//移除key对应的内存记录,此处的LruEntries保存的数据类型为LinkedHashMap
if (this.journalRebuildRequired()) {
this.executorService.submit(this.cleanupCallable);//清除日志文件无用的数据
}else{
this.journalWriter.flush();//完成对日志文件的记录追加。
}
return true;//处理成功返回true
} else {
return false;//处理失败返回false
}
}
答疑
进行什么操作的时候DiskLruCache会对已经保存的数据进行淘汰?需要满足什么条件?
DiskLruCache有个成员变量叫:cleanupCallable,这个变量的类型为Callable,用来在线程池中被执行。此变量执行的任务就是将已保存的数据,采用Lru策略进行删除(淘汰)。
源码分析:
Callable<Void> cleanupCallable = new Callable<Void>() {
public Void call() throws Exception {
synchronized(DiskLruCache.this) {//同步监视器
if (DiskLruCache.this.journalWriter == null) {
return null;
} else {
DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
DiskLruCache.this.rebuildJournal();//重构日志文件
DiskLruCache.this.redundantOpCount = 0;
}
return null;
}
}
}
};
private void trimToSize() throws IOException {
while(this.size > this.maxSize) {//这里的maxSize为初始化时,外部传入的参数。作用:保存数据最大值
//获取符合Lru策略的数据,也就是要淘汰的数据
java.util.Map.Entry<String, DiskLruCache.Entry> toEvict = (java.util.Map.Entry)this.lruEntries.entrySet().iterator().next();
this.remove((String)toEvict.getKey());//此处调用remove方法,具体的逻辑可以参考删除操作
}
}
在什么时候调用会引发我们的回收机制呢?
有如下几个情况:
- 调用查询数据的时候。---get(keyName)
- 插入数据或者修改数据的时候。----completeEdit(DiskLruCache.Editor editor, boolean success)
- 删除数据的时候.----remove(keyName)
DiskLruCahe如何支持多线程编程
1.使用线程池技术
//线程池单线程模式,核心线程为0,为了减少资源占用,挺好的思维。
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
new Callable<Void>() {
public Void call() throws Exception {
synchronized(DiskLruCache.this) {//同步监视器
if (DiskLruCache.this.journalWriter == null) {
return null;
} else {
DiskLruCache.this.trimToSize();//执行Lru策略模式,进行无效无效数据删除
if (DiskLruCache.this.journalRebuildRequired()) {//判断是否需要重构日志文件
DiskLruCache.this.rebuildJournal();//重构日志文件
DiskLruCache.this.redundantOpCount = 0;
}
return null;
}
}
}
};
//上面两个对象配合使用,解决了下面Callable执行的顺序问题。
-
同步监视器机制
内部方法.png
上面的代码方法都有加入synchronized关键字,用来支持多线程编程。
网友评论