您当前的位置: 首页 >  android

郭梧悠

暂无认证

  • 2浏览

    0关注

    402博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Android-Universal-Image-Loader学习笔记(二)--LruDiscCache

郭梧悠 发布时间:2014-06-30 17:03:21 ,浏览量:2

在正式最近最久未使用缓存(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             
关注
打赏
1663674776
查看更多评论
0.0423s