首页 > 代码库 > Android-Universal-Image-Loader --LruDiscCach

Android-Universal-Image-Loader --LruDiscCach

在正式最近最久未使用缓存(LruDiscCache)之前,先介绍一个概念和重要的三个类:

key:是DiscCacheAware接口中save方法里面的imageUri参数通过调用FileNameGenerator的generate(imageUri)所生成的字符串,key必须满足[a-z0-9_-]{1,64};对应着Entry,Snapshot以及Editor的key字段。通过以如下方法来检测key的合法性

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. privatevoid validateKey(String key) {  
  2.         Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);  
  3.         if (!matcher.matches()) {  
  4.   
  5.             thrownew IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");  
  6.         }  
  7.       

Entry:是DiskLruCache里面的内部类,每一个key对应多个缓存图片文件,保存文件的个数是由Entrylengths数组的长度来决定的,如果追根就地的话是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类中与日志相关的字段如下所示:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1.    
  2.   
  3. staticfinal String JOURNAL_FILE = "journal";  
  4.   
  5.     staticfinal String JOURNAL_FILE_TEMP = "journal.tmp";  
  6.   
  7.     staticfinal String JOURNAL_FILE_BACKUP = "journal.bkp";  
  8.   
  9.     staticfinal String MAGIC = "libcore.io.DiskLruCache";  
  10.   
  11.     staticfinal String VERSION_1 = "1";  
  12.   
  13.     staticfinallongANY_SEQUENCE_NUMBER = -1;  
  14.   
  15.     staticfinal Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");  
  16.   
  17.     privatestaticfinal String CLEAN = "CLEAN";  
  18.   
  19.     privatestaticfinal String DIRTY = "DIRTY";  
  20.   
  21.     privatestaticfinal String REMOVE = "REMOVE";  
  22.   
  23.     privatestaticfinal String READ = "READ";  
  24.   
  25.     privatefinal File journalFile;  
  26.   
  27.     //调用rebuildJorunal重新创建日志文件的时候会把日志信息写入到该文件中去  
  28.   
  29.     privatefinal File journalFileTmp;  
  30.   
  31.     privatefinal File journalFileBackup;  
  32.   
  33.     privatefinalintappVersion;  
  34.   
  35. private Writer journalWriter;  
  36.   
  37. private int reduantOpCount;//用来判断是否重建日志的字段  
  38.   
  39. private int redundantOpCount;  


注意:其中的journalWriter,该对象用来向日志文件中写入数据,同时该对象是否为null是作为缓存是否关闭的决定条件:下面三个方法可以说明这个结论

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //检测缓存是否关闭  
  2. publicsynchronizedboolean isClosed() {  
  3.         returnjournalWriter ==null;  
  4.     }  
  5.     //检测缓存是否未关闭  
  6.     privatevoid checkNotClosed() {  
  7.         if (journalWriter ==null) {  
  8.             thrownew IllegalStateException("cache is closed");  
  9.         }  
  10.     }  
  11.   
  12.    /** 
  13.      * 关闭缓存,journalWirter为空的话,说明缓存已经关闭:方法调用结束 
  14.      * 否则的话循环遍历lruEntries中的每一个Entry,撤掉正在编辑的Entry。 
  15.      * Closes this cache. Stored values will remain on thefilesystem. */  
  16.     publicsynchronizedvoid close()throws IOException {  
  17.         if (journalWriter ==null) {  
  18.             return;//缓存已经关闭,直接退出方法调用  
  19.         }  
  20.         //对处于编辑中的entry进行撤销编辑操作  
  21.         for (Entry entry :new ArrayList<Entry>(lruEntries.values())) {  
  22.            if (entry.currentEditor !=null) {  
  23.               entry.currentEditor.abort();  
  24.             }  
  25.         }  
  26.         trimToSize();  
  27.         trimToFileCount();  
  28.         //关闭日志输出流  
  29.         journalWriter.close();  
  30.         journalWriter =null;  
  31.     }  


---------------------------------------------------------------------------------------------------

其实,这个类中大部分的方法都是再操作这些日志文件,当然日志文件的大小也有限制,而这个限制就是有redundantOpCount字段决定的,如果如下方法返回true的话就重新建立一个新的日志文件,并把原来的日志文件删除掉

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //判断是否需要重建日志文件当  
  2.   
  3. privatebooleanjournalRebuildRequired() {  
  4.         finalint redundantOpCompactThreshold = 2000;  
  5.        returnredundantOpCount >= redundantOpCompactThreshold  
  6.                 && redundantOpCount >= lruEntries.size();  
  7.     }  

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.逻辑代码的处理如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. DiskLruCache cache = new DiskLruCache();//第一次初始化  
  2. if(cache.journalFile.exists()){  
  3.   try{  
  4.       对日志文件进行IO操作,具体操作的逻辑下文描述  
  5.       return cache;  
  6. }catch(IOException journalFileException){  
  7.    操作日志出现IO错误  
  8.    cache.delete();删除缓存  
  9. }  
  10. }  
  11. cache.makDir();  
  12. cache = new DiskLruCache();//第二次初始化  
  13. cache.rebuildJournalFile();  
  14. 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对象,(这是第一次添加):

附带具体方法实现:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //读入日志的一行数据  
  2.    privatevoid readJournalLine(String line)throws IOException {  
  3.        //获取第一个空格的位置  
  4.        int firstSpace = line.indexOf(‘ ‘);  
  5.        int keyBegin = firstSpace + 1;  
  6.        //获取第二个空格的位置  
  7.       int secondSpace = line.indexOf(‘ ‘, keyBegin);  
  8.        //获取该行数据代表的所代表的key  
  9.        final String key;  
  10.        if (secondSpace == -1) {//如果第二个空格不存在            
  11.            key = line.substring(keyBegin);  
  12.    //如果改行数据是以REMOVE开头的情况下,就从lruEntries中删除该key锁代表的entry  
  13.            if (firstSpace ==REMOVE.length() && line.startsWith(REMOVE)) {  
  14.                lruEntries.remove(key);  
  15.                return;  
  16.            }  
  17.        } else {  
  18.            key = line.substring(keyBegin, secondSpace);  
  19.        }  
  20.        //得到日志文件当前行锁记录的key  
  21.        Entry entry = lruEntries.get(key);  
  22.        if (entry ==null) {  
  23.            entry = new Entry(key);  
  24.            //如果没有就创建一个Entry,并放入lruEntries中  
  25.            lruEntries.put(key, entry);  
  26.        }  
  27.        //如果改行数据已ClEAN开头  
  28.        if (secondSpace != -1 && firstSpace ==CLEAN.length() && line.startsWith(CLEAN)) {  
  29.           String[] parts = line.substring(secondSpace + 1).split(" ");  
  30.            entry.readable =true;//设置给Entry为发布状态  
  31.            entry.currentEditor =null;//编辑对象设置为空  
  32.            entry.setLengths(parts);//设置该entry中每一个file的大小  
  33.        } //如果该行数据已DIRTY开头,那么设置key所对应的entry为编辑状态  
  34.        elseif (secondSpace == -1 && firstSpace ==DIRTY.length() && line.startsWith(DIRTY)) {//如果该条数据是以DIRTY开头,就将该entry设置为编辑状态  
  35.            entry.currentEditor =new Editor(entry);  
  36.        } elseif (secondSpace == -1 && firstSpace ==READ.length() && line.startsWith(READ)) {  
  37.            // This work was already done by calling lruEntries.get().  
  38.        } else {  
  39.            thrownew IOException("unexpected journal line: " + line);  
  40.        }  
  41.    }  

2) 调用processJournal()对日志文件进一步处理:遍历lruEntries(lruEntires中的数据在步骤1中的readJournalLine方法中添加的)中的每一个Entry对象,对同编辑状态的Entry进行不同的处理;对于处于非编辑状态的entry。也就是entry.currentEditor==null的entry,计算他们的总的文件数目fileCount以及总的文件的大小size;而entry.currentEditor!=null的Entry(注意是由日志文件中的DIRTY对应的Entry),这些,删除这些entry对应的每一个file,也即是说直接从缓存中删除了这些缓存文件。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. privatevoidprocessJournal()throws IOException {  
  2.         deleteIfExists(journalFileTmp);  
  3.         for (Iterator<Entry> i =lruEntries.values().iterator(); i.hasNext(); ) {  
  4.             Entry entry = i.next();  
  5.             if (entry.currentEditor ==null) {  
  6.                 for (int t = 0; t <valueCount; t++) {  
  7.                     size += entry.lengths[t];  
  8.                     fileCount++;  
  9.                 }  
  10.             } else {  
  11.                 entry.currentEditor =null;  
  12.                 for (int t = 0; t <valueCount; t++) {  
  13.                     deleteIfExists(entry.getCleanFile(t));  
  14.                     deleteIfExists(entry.getDirtyFile(t));  
  15.                 }  
  16.                 i.remove();  
  17.             }  
  18.         }  
  19.     }  

3) 初始化journalWriter

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ache.journalWriter =new BufferedWriter(  
  2.                         new OutputStreamWriter(new FileOutputStream(cache.journalFile,true), Util.US_ASCII))  

到此完成了对缓存的初始化操作,注意这是在读取日志文件时没有抛出异常的时候完成的初始化,如果抛出异常的话,就再次进行如下初始化

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. directory.mkdirs();  
  2. cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);  
  3. cache.rebuildJournal();//把原来的日志文件删除,并重新创建一个写入前五行数据的日志文件,以及重新创建journalWirt  
到现在最终的缓存初始化操作才算真正的完成! 

数据写入缓存

   向缓存写入数据时通过DiscCacheWare的两个save方法来实现的:核心思想是从lruEntries中获取指定key(假设该key123456对应的Entry对象(如果没有就往lruEntries中添加),然后获取该Entry对象的Editor对象editor,并返回editor。因为这个editor包含了文件的输出流,用该输出流来想缓存中写入数据,从而达到缓存图片的目的。(注意此时向journal文件中写入了DIRTY记录)>

此时lruEntries中和journal文件中的包含的数据如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. DiskLruCache.Editor editor = cache.edit(getKey(imageUri));//这个editor是一个新的编辑器对象  
  2. privatesynchronized Editor edit(String key,long expectedSequenceNumber)throws IOException {  
  3.         //检测缓存是否关闭  
  4.         checkNotClosed();  
  5.         //检测key是否符合规则  
  6.         validateKey(key);  
  7.         //从lruEntries中获取指定对象的key  
  8.         Entry entry = lruEntries.get(key);  
  9.         if (expectedSequenceNumber !=ANY_SEQUENCE_NUMBER && (entry ==null  
  10.                 || entry.sequenceNumber != expectedSequenceNumber)) {  
  11.             returnnull;//快照已经陈旧  
  12.         }  
  13.         //如果在lruEntries中没有对应的entry对象,则创建并添加  
  14.         if (entry ==null) {  
  15.             entry = new Entry(key);  
  16.             lruEntries.put(key, entry);  
  17.         }//如果Entry存在并且还处于编辑状态的话就返回一个null  
  18.         elseif (entry.currentEditor != null) {  
  19.             returnnull;// Another edit is in progress.  
  20.         }  
  21.        //创建一个新的编辑器对象  
  22.         Editor editor = new Editor(entry);  
  23.         entry.currentEditor = editor;  
  24.         // Flush the journal before creating files to prevent file leaks.  
  25.         //创建新的文件之前刷新Writer以阻止文件泄露  
  26.         journalWriter.write(DIRTY +‘ ‘ + key + ‘\n‘);  
  27.         journalWriter.flush();  
  28.        return editor;  
  29.     }  
  写入缓存成功后调用editor.commit()来完成保存数据的操作, (注意此时向 journal 文件中写入了 CLEAN 记录)

如果失败的话调用editor.abort()来撤掉此次的编辑,同时从lruEntries中删除此entry.(注意此时向journal文件中写入了REMOVE记录)

从缓存中取数据:

从缓存中取数据是通过调用DiscCacheWare的get(String imageUri)方法来实现的。前面说过,从缓存中取数据的时候获取的实际上是一个Entry的Snapshot,具体的方法如下:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. publicsynchronized Snapshot get(String key)throws IOException {  
  2.         checkNotClosed();  
  3.         validateKey(key);     
  4.         //如果lruEntries中没有该Entry,直接翻译一个null  
  5.         Entry entry = lruEntries.get(key);  
  6.         if (entry ==null) {  
  7.             returnnull;  
  8.         }  
  9.        //如果该Entry还没有发布,那么也返回一个null  
  10.         if (!entry.readable) {  
  11.             returnnull;  
  12.         }  
  13.         // Open all streams eagerly to guarantee that we see a single published  
  14.         // snapshot. If we opened streams lazily then the streams could come  
  15.         // from different edits.  
  16.        //循环遍历Entry中的每一个file,以及每一个file所代表的输入流  
  17.         File[] files = new File[valueCount];  
  18.         InputStream[] ins = new InputStream[valueCount];  
  19.         try {  
  20.             File file;  
  21.            for (int i = 0; i <valueCount; i++) {  
  22.                 file = entry.getCleanFile(i);  
  23.                 files[i] = file;  
  24.                 ins[i] = new FileInputStream(file);  
  25.             }  
  26.         } catch (FileNotFoundException e) {  
  27.             // A file must have been deleted manually!  
  28.             for (int i = 0; i <valueCount; i++) {  
  29.                 if (ins[i] !=null) {  
  30.                    Util.closeQuietly(ins[i]);  
  31.                 } else {  
  32.                     break;  
  33.                }  
  34.             }  
  35.             returnnull;  
  36.         }  
  37.         redundantOpCount++;  
  38.         //标记哪一个文件正在读取  
  39.         journalWriter.append(READ +‘ ‘ + key + ‘\n‘);  
  40.         if (journalRebuildRequired()) {  
  41.             executorService.submit(cleanupCallable);  
  42.         }  
  43.         returnnew Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);  
  44.     }  

此时向日志中加READ 记录

通过代码可以发现,当向缓存中取数据的时候需要检测是否重建日志,具体怎么重建,见下文。在此暂不做描述。

从缓存中删除数据

调用DiscCacheAware接口的remove(String imageUri方法)来实现,具体的删除的主要逻辑:

删除缓存中对应的文件,向日志中追加REMOVE行,从lruEntires中删除对应的entry

关闭缓存,清空缓存

通过close方法来实现,具体的逻辑为:

   对正在编辑的entry进行撤销操作;

   调用trimToSize使得已经使用缓存的大小不超过maxSize

   调用trimToFileCount()方法使得缓存中的文件方法小于maxfileCount

   关闭日志journalWriter

清空缓存的clear除了以上的逻辑外还对directory进行了删除操作。

--------------------------------------------------------------------------------------------------------------

LruDiscCache,看到这个类的名字就是到该类用到了最近最久未使用(LRU)算法来处理文件缓存。该算法的在该类核心思想的体现就是选择在最近一段时间里最久没有使用过的缓存文件删除

该类也提供了跟BasicDiscCache一样的默认属性,比如默认缓存的大小为32k,默认压缩后的图片格式为png等等。另外也提供了后备缓存reserveCacheDir,不过跟BasicDiscCache不同的是BasicDiscCache中代表缓存目录的cacheDir在LruDiscCache中用DiskLruCache对象的引用cache来代替。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //用DiskLruCache来代替,在BasicDiscCache中用File cacheDir来表示  
  2.   protected DiskLruCachecache;  
  3.   private FilereserveCacheDir;  
  4.   protectedfinal FileNameGeneratorfileNameGenerator;  

LruDiscCache提供了一个主要构造函数,该构造函数里面的参数主要用来初始化fileNameGenerator和cache对象。在此构造器中可以设置最大缓存的大小,缓存最多可以保存多少条数据的参数,这些参数都是初始化cache对象所需要的数据。

DiscCache接口提供的方法在LruDiscCache中的核心实现都转移到了cache对象中(或者说是DiskLruCache中)。下面就说说DiskLruCache,然后在掉过头来说LruDiscCache。

该类也定义了一下变量:

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //缓存图片的目录  
  2.     privatefinal Filedirectory;  
  3.     //最大缓存的大小  
  4.     privatelongmaxSize;  
  5.     //最多缓存多少文件  
  6.     privateintmaxFileCount;  
  7.     //每一个entry所封装的文件的数目  
  8.     privatefinalintvalueCount;  
  9.     //缓存的大小  
  10.     privatelongsize = 0;  
  11.     //缓存文件的数目  
  12.     privateintfileCount = 0;  

另外该类还封装了一个重要类型为LinkedHashMap的属性lruEntries,用它来实现LRU算法(最近最久未使用算法)

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. //最后一个参数设置为true表示访问的顺序  
  2. privatefinalLinkedHashMap<String, Entry>lruEntries =  
  3.             newLinkedHashMap<String, Entry>(00.75f,true)  

用LinkedHashMap能实现LRU算法的原因是在迭代map遍历列表中的元素时最近访问的元素会排在LinkedHashMap的尾部 。在这里简单介绍一个例子作为说明:如果一个LinkedHashMap中通过一个for循环加入了a b c d e 五个元素,然后调用get方法获取元素a,那么当再次遍历该map的时候iterator.next().getValue()会依次输入b c d e a而不是a b c d e,这样通过最近经常使用的元素(比如get方法获取的元素a)就放在后面,最近最少使用的就排在了链表的前面,从而实现了LRU算法。

最后还有一个long 类型的nextSequenceNumber:为了对新的和旧的快照做区分,每一个entry对象在每一次编辑被提交的时候会获取一个序列号(nextSequenceNumber),如果这个序列号不等于entry的序列号的话,就说明该快照是旧的快照。(最近最久未使用的快照)

 在这个缓存中实现对最近最久未使用文件的删除的目的和时机如下:

   目的:

1)               在缓存文件总大小超出最大缓存大小maxSize时对最近最久未使用的图片缓存进行删除,核心方法为:trimToSize

2)               在缓存中的文件总数目超过缓存要求的最大文件数目fileCount时对最近最久未使用的的图片缓存进行删除,核心方法为:trimToFileCount

  这两个方法调用的时机从总体来说分为三个,由于这些操作涉及到IO操作,费时,所以在代码中交给了一个Callable去处理,具体的核心代码如下

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. final ThreadPoolExecutorexecutorService =  
  2.             new ThreadPoolExecutor(01, 60L, TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());  
  3.     //对缓存的清理操作  
  4.     privatefinal Callable<Void>cleanupCallable =new Callable<Void>() {  
  5.         public Void call()throws Exception {  
  6.             synchronized (DiskLruCache.this) {  
  7.                 //缓存已经关闭  
  8.                 if (journalWriter ==null) {  
  9.                     returnnull;// Closed.  
  10.                 }  
  11.                //删除缓存中处于非编辑状态的entry对应的文件,使得缓存大小I小于maxSize  
  12.                 trimToSize();  
  13.                 //删除对于的文件,使得缓存中的文件数少于maxFileCount  
  14.                 trimToFileCount();  
  15.                 //判断是否需要开启新的日志  
  16.                 if (journalRebuildRequired()) {  
  17.                     rebuildJournal();  
  18.                     redundantOpCount = 0;  
  19.                 }  
  20.             }  
  21.             returnnull;  
  22.         }  
  23.     };   

 执行Callable的三种时机:

1) 调用setMax方法设置缓存的最大值的时候

2)  成功缓存一个文件的时候,也就是说调用complete第二个参数为true的时候、

3)  对缓存中的文件进行删除的时候 


Android-Universal-Image-Loader --LruDiscCach