在正式最近最久未使用缓存(LruDiscCache)之前,先介绍一个概念和重要的三个类:
key:是DiscCacheAware接口中save方法里面的imageUri参数通过调用FileNameGenerator的generate(imageUri)所生成的字符串,key必须满足[a-z0-9_-]{1,64};对应着Entry,Snapshot以及Editor的key字段。通过以如下方法来检测key的合法性
privatevoid validateKey(String key) {
Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
if (!matcher.matches()) {
thrownew IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
}
Entry:是DiskLruCache里面的内部类,每一个key对应多个缓存图片文件,保存文件的个数是由Entry中lengths数组的长度来决定的,如果追根就地的话是valueCount来决定Entry中封装的文件的个数。(不过在实际初始化缓存的过程中,该每一个Entry中只有一个图片缓存文件与之对应,也就是说一个key对应一个file)
该类只有一个构造函数用来初始化key和lengths数组
属性名称
说明
类型
key
每一个key对应多个file,该可以是图片文件
String
readable
当当前Entry对象被发布的时候设置为true
boolean
currentEditor
如果当前entry没有被编辑那么该属性就位null
Editor
sequenceNumber
对当前entry最近提交的编辑做的标志序列号,每成功提交一次,当然entry的该字段就++
long
lengths
当前entry中的每一个file的长度,在构造器中初始化,初始化长度为valueCount,实际上该lengths固定长度为1,也就是一个Entry代表了一个文件的缓存实体
long[]
方法名
方法说明
返回值
getLengths()
返回当前entry对象中所有文件的总长度
String
setLengths(String[] strs)
采用十进制的数字来设置每一个entry所封装的file的长度,来这是lengths数组每一个元素的值,通过读取日志的CLEAN行数据时调用此方法
void
getCleanFile(int i)
获取当前entry中某一个干净(有效)的缓存文件(图片),文件名称的格式为key+”.”+i
File
getDirtyFile(int i)
获取当前entry中每一个脏的(无效)的缓存文件(图片),名称的格式为key+”.”+i+”.tmp”
File
Editor(编辑器):每一个entry对象又都包含一个Editor,Editor类也是DiskLruCache的一个final类型的内部类;用来负责保存Entry中每一个图片File的文件输出流和输入流,以便于图片缓存文件的输出和读取,在调用缓存的save方法的时候就是获取方法参数imageUri所生成的key对应Entry中的一个Editor对象,从而获取imageUri图片的输出流来把图片写入到缓存中(directory目录中)。
该类提供一个构造器用来初始化,用来所要编辑的entry和written数组
属性
说明
类型
entry
final,代表当前Editor对象的所要编辑的entry对象
Entry
written
final,在构造器中初始化,如果entry已经发布的话就设置为null,否则就初始化长度为valueCount的数组,它的每一个元素用来标志entry中对应索引文件是否可写。
boolean[]
hasErrors
编辑是否出错,当把entry中的一个File输出时发生IO异常时设置为true,具体在Editor的内部类FaultHidingOutputStream中设置
boolean
committed
编辑是否已经提交
boolean
方法名
方法说明
返回类型
newInputStream(int index)
该方法返回一个无缓冲的输入流用来读取entry中第index条数据(也就是第index个图片文件,实际应用由于Entry中值对应一个文件所以index固定位0)上次提交的值,如果该条数据没有被提交的值就返回null。
1)
InputStream
getString(index)
获取entry中第index文件上次提交的值
String
newOutputStream(int index)
Entry中对应文件的输出流,向缓存写入数据时调用,目的是把图片文件保存到缓存。
调用save方法时调用,获取entry中第index文件(也就是第index个图片文件,实际应用由于Entry中值对应一个文件所以index固定位0)上的无缓冲的输出流用来把文件输出到缓存,
如果输出的过程中发成异常就设置Editor的hasErrors为true,即为编辑失败
OutputStream
set(index,String value)
向当然Editor中entry的第index个文件写入数据value
value
commit()
当编辑完成后调用这个方法使得该File对reader可见,同时释放线程锁以便于让其他editor对象对同一个key上的entry进行编辑操作。执行步骤如下:
1) 判断hasError是否为true,如果为true,则撤销此次提交
2)设置commited为true
void
abort()
终止对当前entry的第index文件的编辑操作。实际上是调用completeEdit(this,false)来撤销此次编辑,并释放锁
void
abortUnlessCommitted()
在没有提交的情况下,也就是commited=false的情况下终止本次编辑
Snapshot: 每一个Entry又有一个Snapshot(快照),当从缓存中调用DisCacheAware方法中的get(String iamgeUri)获取缓存图片时实际上获取的不是Entry,而是imageUri生成key对应Entry的一个快照Snapshot对象所封装的File对象
该类实现了Closeable,可以使用Java7的新特性 用try-with-resource来自动关闭流,该类包含的字段都在构造函数中进行初始化
属性名
说明
类型
key
entry的key
string
sequenceNumber
entry的sequenceNumber
long
files[]
entry中所有的file(实际上该files的长度只有一)
File[]
ins
entry中所有file的输入流
InputStream[]
lengths[]
entry所有file的总大小
long[]
edit()
返回该快照所对应的entry的Editor对象
getFile(int index)
获取快照中的第index个文件,从缓存中取出数据时调用
File
getInputStream(int index)
获取ins数组中第index个文件的输入流
InputStream
getString(int index)
把第index文件中的内容作为字符串返回
String
close()
循环遍历ins,关闭每一个输入流
void
所以Entry,Editor,Snapshot之间的关系通过key串联了起来:
日志文件:该缓存提供了一个名叫journal的日志文件,典型的日志文件看清来如下格式
每个日志文件的开头前面五行数据分别为
行号
该行的数据
1
libcore.io.DiskLruCache
2
该缓存的版本号 例如1
3
app的版本号 例如100
4
每一个Entry中所对应的File的数目例如2
5
空白行
第五行过后就是日志的正文,日志正文的格式如下
CLEAN行所代表的数据格式如下
CLEAN
entry所代表的key
f1.length
f2.length
…………….
\n
特别说明:f1.length 是key所对应的entry中第一个文件的大小,和f2.length之间用一个空格隔开,具体fn是多少由日志中第四行的数据所决定。如果追根究底的话,是由Entry对象中的lengths数组的长度来决定或者是DiskLruCache的valueCount字段来决定(因为lengths数组的长度初始化的时候就是valueCount)。
REMOVE行READ行以及DIRTY行显示的数据格式较为简单:
REMOVE
entry对象的key
\n
READ
同上
\n
DIRTY
同上
\n
下面是源代码中给出的日志样本文件,如图
每行字段的被写入日志文件的时机如下表:
DIRTY
写入该行数据的时机有两处:
1) 调用rebuildJournal重新新的日志文件时会把lruEntries中处于编辑状态的entry(entry.currentEditor!=null的状态)写入日志文件中去(日志格式件见上文)
2) 调动edit方法对entry进行编辑或者说调用save方法时会把当前的entry写入到日志文件
CLEAN
写入该行数据的时机有两处:
1) 调用rebuildJournal重新新的日志文件时会把lruEntries中处于非编辑状态的entry(entry.currentEditor==null的状态)写入日志文件中去(日志格式件见上文)
2) completeEdit方法中当前entry处于发布状态(readable=true)或者编辑成功的时候(success=true的时候)写入
READ
当调用get(key)方法获取entry的快照时会写入
REMOVE
写入该条数据的世纪有两处
1) 在completeEdit方法中如果当然Entry既不处于发布状态而且方法参数success为false的时候写入,并且把对应的缓存文件也从缓存中删除。
2) 调用remove(String key)方法删除key对应的缓存文件时写入,并且把对应的缓存文件也从缓存中删除。
既然有写入日志文件的时机,那么肯定也会提供一个读取日志文件的时机,具体的时机下面讲解open方法初始化缓存的时候会讲到。
当缓存被操作的时候日志文件就会被追加进来。日志文件偶尔会通过丢掉多余行的数据来实现对日志的简化;在对日志进行简化操作的过程中会用到一个名为journal.tmp的临时文件,当缓存被打开的情况下如果journal.tmp文件存在的话就会被删除。
在DiskLruCache类中与日志相关的字段如下所示:
staticfinal String JOURNAL_FILE = "journal";
staticfinal String JOURNAL_FILE_TEMP = "journal.tmp";
staticfinal String JOURNAL_FILE_BACKUP = "journal.bkp";
staticfinal String MAGIC = "libcore.io.DiskLruCache";
staticfinal String VERSION_1 = "1";
staticfinallongANY_SEQUENCE_NUMBER = -1;
staticfinal Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
privatestaticfinal String CLEAN = "CLEAN";
privatestaticfinal String DIRTY = "DIRTY";
privatestaticfinal String REMOVE = "REMOVE";
privatestaticfinal String READ = "READ";
privatefinal File journalFile;
//调用rebuildJorunal重新创建日志文件的时候会把日志信息写入到该文件中去
privatefinal File journalFileTmp;
privatefinal File journalFileBackup;
privatefinalintappVersion;
private Writer journalWriter;
private int reduantOpCount;//用来判断是否重建日志的字段
private int redundantOpCount;
注意:其中的journalWriter,该对象用来向日志文件中写入数据,同时该对象是否为null是作为缓存是否关闭的决定条件:下面三个方法可以说明这个结论
//检测缓存是否关闭
publicsynchronizedboolean isClosed() {
returnjournalWriter ==null;
}
//检测缓存是否未关闭
privatevoid checkNotClosed() {
if (journalWriter ==null) {
thrownew IllegalStateException("cache is closed");
}
}
/**
* 关闭缓存,journalWirter为空的话,说明缓存已经关闭:方法调用结束
* 否则的话循环遍历lruEntries中的每一个Entry,撤掉正在编辑的Entry。
* Closes this cache. Stored values will remain on thefilesystem. */
publicsynchronizedvoid close()throws IOException {
if (journalWriter ==null) {
return;//缓存已经关闭,直接退出方法调用
}
//对处于编辑中的entry进行撤销编辑操作
for (Entry entry :new ArrayList(lruEntries.values())) {
if (entry.currentEditor !=null) {
entry.currentEditor.abort();
}
}
trimToSize();
trimToFileCount();
//关闭日志输出流
journalWriter.close();
journalWriter =null;
}
---------------------------------------------------------------------------------------------------
其实,这个类中大部分的方法都是再操作这些日志文件,当然日志文件的大小也有限制,而这个限制就是有redundantOpCount字段决定的,如果如下方法返回true的话就重新建立一个新的日志文件,并把原来的日志文件删除掉
//判断是否需要重建日志文件当
privatebooleanjournalRebuildRequired() {
finalint redundantOpCompactThreshold = 2000;
returnredundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
redundantOpCount的值有在四处进行了设定:
get(String key),remove(Stringkey) completeEdit没调用一次这个方法就会对redundantOpCount进行++操作,而读取日志的方法readJournal则对该字段赋值为redundantOpCount = lineCount - lruEntries.size();事实上这个readJournal是在调用open方法初始化缓存的时候调用的,也就相当于对redundantOpCount进行了初始化操作。同时当journalRebuildRequired的时候redundantOpCount进行清零操作
-------------------------------------------------------------------------------
介绍了上面的一些基本概念下面说说具体怎么使用这个缓存(介绍流程为:初始化缓存,向缓存中存取数据,从缓存中删除数据以及关闭缓存来进行说明)以及LRU算法实现是怎么体现的。
初始化缓存由于DiskLruCache的构造函数是私有的,所以不能在外部进行该对象的初始化;DiskLruCache提供了一个静态的open()方法来进行缓存的初始化:
该方法进行如下操作
1) 创建日志文件:主要是对日志备份文件journal.bkp进行处理,如果journal.bkp文件和journal都存在的话就删除journal.bkp文件,如果journal.bkp文件存在而journal文件不存在就把journal.bkp重命名为journal文件。
2) 调用构造器进行缓存对象cache的初始化,初始化的的数据包括日志的三个文件:journal, journal.tmp,journal.bkp;同时还初始化了每一个Entry锁能存储的文件的个数valueCount,缓存的最大内存maxSize和最多缓存多少个文件的maxFileCount;
3) 如果cache.journalFile.exists()==true并且读取日记操作没有IO错误的话,就直接返回上面的cache,否者就重新初始化缓存对象。
也即是说打开缓存的时候有可能初始化两次DiskLruCache的对象,第一次初始化cache1的时候会判断日志文件journalFile是否存在,不存在的话就进行第二次初始化cache2;如果存在的话就进行对journalFile进行IO操作,如果没有出现异常的情况下直接返回cache1,否则返回cache2.逻辑代码的处理如下:
DiskLruCache cache = new DiskLruCache();//第一次初始化
if(cache.journalFile.exists()){
try{
对日志文件进行IO操作,具体操作的逻辑下文描述
return cache;
}catch(IOException journalFileException){
操作日志出现IO错误
cache.delete();删除缓存
}
}
cache.makDir();
cache = new DiskLruCache();//第二次初始化
cache.rebuildJournalFile();
return cache;
上文刚说过初始化的时候需要读日志进行读取操作,下面重点说说初始化缓存的时候对日志文件进行了哪些操作。
1) 读取日志的方法是由readJournal()来对日式文件journal一行一行的读取,对每一行日志文件的处理是由readJournalLine(String line)方法来决定的,对每一行日式数据的处理实际上式对每行日志的key在lruEnries中对应Entry对象的处理。对每一行文件的处理如下表:
DIRTY
当读取该条数据的时候,就实例化该key对应entry对象的currentEditor使之处于编辑状态
CLEAN
设置该条数据key对应的Entry的为发布状态,并且设置currentEditro=null
READ
对该条数据不作处理
REMOVE
当调用open方法读取日志文件的时候,改行数据中key锁对应的那个实体会从lruEntries中删除。注意:该key对应的Entry所代表的那个file文件在缓存已经删除
注意:除了读取到REMOVE行直接在lruEntries中删除对应的Entry之外,其余的每一行数据的需要进行如下判断然后在进行处理,主要是向lruEntries中添加Entry对象,(这是第一次添加):
附带具体方法实现:
//读入日志的一行数据
privatevoid readJournalLine(String line)throws IOException {
//获取第一个空格的位置
int firstSpace = line.indexOf(' ');
int keyBegin = firstSpace + 1;
//获取第二个空格的位置
int secondSpace = line.indexOf(' ', keyBegin);
//获取该行数据代表的所代表的key
final String key;
if (secondSpace == -1) {//如果第二个空格不存在
key = line.substring(keyBegin);
//如果改行数据是以REMOVE开头的情况下,就从lruEntries中删除该key锁代表的entry
if (firstSpace ==REMOVE.length() && line.startsWith(REMOVE)) {
lruEntries.remove(key);
return;
}
} else {
key = line.substring(keyBegin, secondSpace);
}
//得到日志文件当前行锁记录的key
Entry entry = lruEntries.get(key);
if (entry ==null) {
entry = new Entry(key);
//如果没有就创建一个Entry,并放入lruEntries中
lruEntries.put(key, entry);
}
//如果改行数据已ClEAN开头
if (secondSpace != -1 && firstSpace ==CLEAN.length() && line.startsWith(CLEAN)) {
String[] parts = line.substring(secondSpace + 1).split(" ");
entry.readable =true;//设置给Entry为发布状态
entry.currentEditor =null;//编辑对象设置为空
entry.setLengths(parts);//设置该entry中每一个file的大小
} //如果该行数据已DIRTY开头,那么设置key所对应的entry为编辑状态
elseif (secondSpace == -1 && firstSpace ==DIRTY.length() && line.startsWith(DIRTY)) {//如果该条数据是以DIRTY开头,就将该entry设置为编辑状态
entry.currentEditor =new Editor(entry);
} elseif (secondSpace == -1 && firstSpace ==READ.length() && line.startsWith(READ)) {
// This work was already done by calling lruEntries.get().
} else {
thrownew IOException("unexpected journal line: " + line);
}
}
2) 调用processJournal()对日志文件进一步处理:遍历lruEntries(lruEntires中的数据在步骤1中的readJournalLine方法中添加的)中的每一个Entry对象,对同编辑状态的Entry进行不同的处理;对于处于非编辑状态的entry。也就是entry.currentEditor==null的entry,计算他们的总的文件数目fileCount以及总的文件的大小size;而entry.currentEditor!=null的Entry(注意是由日志文件中的DIRTY对应的Entry),这些,删除这些entry对应的每一个file,也即是说直接从缓存中删除了这些缓存文件。
privatevoidprocessJournal()throws IOException {
deleteIfExists(journalFileTmp);
for (Iterator i =lruEntries.values().iterator(); i.hasNext(); ) {
Entry entry = i.next();
if (entry.currentEditor ==null) {
for (int t = 0; t
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?