DiskLruCache磁盘图片缓存分析

DiskLruCache磁盘缓存简单分析。

一个完善的图片缓存框架不仅包含LruCache(内存缓存),一般也包含DiskLruCache(文件缓存)本文就通过现象简单分析一下DiskLruCache的工作原理。

现象是什么呢?直接上图

通过图,可以发现有一个日志文件journal 其他的文件都是啥?你们懂的,图片文件。

 

再打开journal文件看一下内容。

libcore.io.DiskLruCache

1

1

1

 

DIRTY http192168192155bdpuploadedfiles2014102709480342637afcc64a1497e

CLEANhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e 1063

READhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e

DIRTYhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82

CLEANhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82 3172

DIRTYhttpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef

CLEANhttpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef 4603

READ httpstoragejdcombdpuploadedfiles201408051710c8a499ba33a6406e8ef

DIRTYhttp192168192155bdpuploadedfiles201406121340774687a6b7b14a13998

CLEANhttp192168192155bdpuploadedfiles201406121340774687a6b7b14a13998 6540

READhttpstoragejdcombdpuploadedfiles2014061815179cd7fbf6ad004addb82

 

第一行:libcore.io.DiskLruCache

在代码中定义      static final String MAGIC= "libcore.io.DiskLruCache";

第二行:该DiskLruCache的版本

第三行:应用程序的版本,
第四行:每个key中保存值的个数

第五行:以及一个空行。

该文件中,随后记录的都是一个entry的状态。每行包括下面几项内容:一个状态,一个key,和可选择的特定状态的值。


DIRTY
:追踪那些活跃的条目,它们目前正在被创建或更新。每一个成功的DIRTY行后应该跟随一个CLEANREMOVE行。DIRTY行如果没有一个CLEANREMOVE行与它匹配,表明那是一个临时文件应该被删除。
CLEAN
:跟踪一个发布成功的entry,并可以读取。一个发布行后跟着其文件的长度。
READ
:跟踪对LRU的访问。
REMOVE
:跟踪被删除的entry

 

接着这些现象继续分析以下问题,

1       journal什么时候创建的。    

2       图片文件什么时候生成的。 

3       journal DIRTYCLEAN, READ, REMOVE什么时候写入的。

4       怎么控制缓存文件总大小和文件总个数

5       怎么读取缓存文件.

 

分析这几个问题之前先看看 DiskLruCache磁盘缓存类的内部结构以及各个部分的职责。

DiskLruCache 这个是主类:两个比较重要成员:

       private Writer journalWriter;  用于读写日志文件。

       private final LinkedHashMap<String,Entry> lruEntries =

                     new LinkedHashMap<String,Entry>(0, 0.75f, true);缓存文件实体映射表。

       maxSize 控制缓存大小

       maxFileCount 控制文件个数

directory 文件缓存目录

内部类 Entry:实体类,得到她,我们可以拿到这个实体对应的缓存文件,文件大小,对缓存文件编辑更新的对象Editor等等。

内部类Editor:主要是可以获得缓存文件操作流,操作更新缓存文件。

内部类 FaultHidingOutputStream从名字可以看出来,就是缓存文件流的容错处理。

内部类 Snapshot我们获取从缓存文件列表中获取文件时就能拿到这个对象。就像名字一样表示缓存文件快照。主要包含文件名,文件操作流,文件大小等。

现在有了基本的认知,我们可以开始分析第一个问题:Journal的创建

先看DiskLruCache的创建函数open我把这个函数分成三段来分析。

       public static DiskLruCache open(Filedirectory, int appVersion, int valueCount, long maxSize, int maxFileCount)

                     throws IOException {

//第一段

              // If a bkp file exists, use itinstead.

              File backupFile = newFile(directory, JOURNAL_FILE_BACKUP);

              if (backupFile.exists()) {

                     File journalFile = newFile(directory, JOURNAL_FILE);

                     // If journal file alsoexists just delete backup file.

                     if (journalFile.exists()) {

                            backupFile.delete();

                     } else {

                            renameTo(backupFile,journalFile, false);

                     }

              }

//第二段

              // Prefer to pick up where we leftoff.

              DiskLruCache cache = new DiskLruCache(directory,appVersion, valueCount, maxSize, maxFileCount);

              if (cache.journalFile.exists()) {

                     try {

                            cache.readJournal();

                            cache.processJournal();

                            cache.journalWriter= new BufferedWriter(

                                          newOutputStreamWriter(new FileOutputStream(cache.journalFile, true),Util.US_ASCII));

                            return cache;

                     } catch (IOExceptionjournalIsCorrupt) {

                            System.out

                                          .println("DiskLruCache"

                                                        +directory

                                                        +" is corrupt: "

                                                        +journalIsCorrupt.getMessage()

                                                        +", removing");

                            cache.delete();

                     }

              }

//第三段

              // Create a new empty cache.

              directory.mkdirs();

              cache = newDiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);

              cache.rebuildJournal();

              return cache;

       }

首先看参数:directory 这个就是缓存文件的路径了。

             appVersion  应用的版本号

                      valuecount 一个key可以对应值得个数

             maxSize    磁盘缓存大小

             maxFileCount  磁盘缓存文件的最大个数

第一段:检查journal备份文件是否存在,然后检查journal文件是否存在,存在删除备份文件,不存在把备份文件重命名为journal文件。

 

第二段:创建DiskLruCache对象,检查日志文件journal是否存在,如果存在,

首先调用cache.readJournal readJournalLine函数读取并解析日志文件,并把解析出来的日志作为一个Entry保存到lruEntries主要是保存 CLEAN DIRTY状态的字段。

然后调用 cache.processJournal();统计当前缓存文件总大小和总个数,处理lruEntries DIRTY数据并删除。这里要注意,因为我们日志文件里面每一条 DIRTY数据后面都会跟着一条 CLEAN或者 REMOVE或者READ,

readJournalLine有一段代码如下:

if(secondSpace != -1 && firstSpace == CLEAN.length() &&line.startsWith(CLEAN)) {

                     String[] parts =line.substring(secondSpace + 1).split(" ");

                     entry.readable = true;

                     entry.currentEditor = null;//只要clean在后面最终entry .currentEditor为空

                     entry.setLengths(parts);

} else if(secondSpace == -1 && firstSpace == DIRTY.length() &&line.startsWith(DIRTY)) {

                     entry.currentEditor = newEditor(entry);//否则 entry.currentEditor不为空

 

processJournal清除DIRTY数据逻辑如下:

                     if (entry.currentEditor ==null) {

                            for (int t = 0; t< valueCount; t++) {

                                   size +=entry.lengths[t];  //统计文件缓存总大小。

                                   fileCount++; //统计文件缓存总个数。

                            }

                     } else { //删除只有DIRTY数据的脏数据。

                            entry.currentEditor= null;

                            for (int t = 0; t< valueCount; t++) {

                                   deleteIfExists(entry.getCleanFile(t));

                                   deleteIfExists(entry.getDirtyFile(t));

                            }

                            i.remove();

                     }

最后cache.journalWriter = new BufferedWriter(newOutputStreamWriter(new FileOutputStream(cache.journalFile, true),Util.US_ASCII));

获得日志文件写操作对象。返回磁盘缓存对象。

 

第三段:第二段代码是在 journal存在的情况下的case如果不存在则走第三段代码。

首先创建缓存路径,同时创建DiskLruCache对象。

然后调用cache.rebuildJournal();创建日志文件。

这个函数比较简单,

第一步创建一个临时日志文件写入库名,版本号,应用版本号,key保存值得个数。

第二步创建把当前 lruEntries数据写入临时日志文件。

第三步就是备份当前日志文件把临时日志文件重命名为journal日志文件,删除备份日志文件。最后获得新日志文件的写操作对象。

 

到这里journal文件创建完成。其实第二个问题缓存文件的生成过程伴随着 jounal文件中 DIRTYCLEAN, READ, REMOVE记录的写入以及缓存文件总大小和文件个数总大小的控制,下面通过保存文件的流程在分析这三个问题。

 

       public boolean save(String imageUri,Bitmap bitmap) throws IOException {

              DiskLruCache.Editor editor =cache.edit(getKey(imageUri));

              if (editor == null) {

                     return false;

              }

              OutputStream os = newBufferedOutputStream(editor.newOutputStream(0), bufferSize);

              boolean savedSuccessfully = false;

              try {

                     savedSuccessfully =bitmap.compress(compressFormat, compressQuality, os);

              } finally {

                     IoUtils.closeSilently(os);

              }

              if (savedSuccessfully) {

                     editor.commit();

              } else {

                     editor.abort();

              }

              return savedSuccessfully;

       }

DiskLruCache.Editoreditor = cache.edit(getKey(imageUri)); edit函数主要代码如下:

                     entry = new Entry(key);

                     lruEntries.put(key, entry);

              journalWriter.write(DIRTY + ' ' +key + '\n');

              journalWriter.flush();

先创建一个entry对象加入到lruEntries然后往日志文件里面写入一行

DIRTYhttp192168192155bdpuploadedfiles2014102709480342637afcc64a1497e

第三个问题 DIRTY记录就是这样在日志文件中写入的。

创建Editor对象。

editor.newOutputStream(0),这个函数主要就是新建一个将要缓存的文件,同时创建一个返回文件输出流。

第二个问题缓存文件就是这么生成的。

savedSuccessfully= bitmap.compress(compressFormat, compressQuality, os);这个就不说了,把要缓存的内容写入到缓存文件。

 

              if (savedSuccessfully) {

                     editor.commit();

              } else {

                     editor.abort();

              }

editor.commit();这个函数调用

completeEdit(editor,success = true)主要工作就是

// If thisedit is creating the entry for the first time, every index must have a value.

第一步:检查如果entry是第一次创建,保证每一个index都有值,每一个index的意思就是一个key可以对应多个value在创建DiskLruCache对象时赋值的valueCount

第二步:把dirty.temp文件转换文 CLEAN .index的文件这就是为什么因为我们的valuecount设置为 1所以所有图片缓存文件都是以.0结束。然后开始统计工作

统计 size缓存文件总大小filecount文件总个数, redundantOpCount对日志文件操作记录次数。

第三步:一切OK,日志文件中写入CLEAN记录

 journalWriter.write(CLEAN + ' ' + entry.key +entry.getLengths() + '\n');

第三个问题CLEAN记录就是这样在日志文件中写入的。

 

如果失败editor.abort();在调用completeEdit(editor,success = false)

lruEntries移除记录在日志文件中写入 REMOVE记录。

                     journalWriter.write(REMOVE+ ' ' + entry.key + '\n');

第三个问题REMOVE记录就是这样在日志文件中写入的。

 

快分析完了,视乎没有看到第四个问题的解释,不着急曹操来了

completeEdit函数最后两行.

              if (size > maxSize || fileCount> maxFileCount || journalRebuildRequired()) {

                     executorService.submit(cleanupCallable);

              }

Sizefile Count  redundantOpCount什么意思前面已近解释过了,

ClaenupCallablecall函数内容如下:

                            trimToSize();

                            trimToFileCount();

                            if(journalRebuildRequired()) {

                                   rebuildJournal();//上面已经有分析了。

                                   redundantOpCount= 0;

                            }

TrimToSize(); trimToFileCount();函数都调用 remove(String key)

Remove函数不做分析了,主要工作就是往日志文件里面写入remove记录把缓存文件删除,从lruEntries中删除 key对应的 Entry          

journalWriter.append(REMOVE + ' ' + key +'\n');

lruEntries.remove(key);

 

最后一个问题是啥来着?哦,怎么读取缓存文件。直接上函数吧!

       public synchronized Snapshot get(Stringkey) throws IOException {

              Entry entry = lruEntries.get(key);

              File[] files = newFile[valueCount];

              InputStream[] ins = newInputStream[valueCount];

                     File file;

                     for (int i = 0; i <valueCount; i++) {

                            file =entry.getCleanFile(i);

                            files[i] = file;

                            ins[i] = newFileInputStream(file);

                     }

              redundantOpCount++;

              journalWriter.append(READ + ' ' +key + '\n');

              if (journalRebuildRequired()) {

                     executorService.submit(cleanupCallable);

              }

              return new Snapshot(key,entry.sequenceNumber, files, ins, entry.lengths);

       }

其实分析完前面四个问题,这个问题就So easy了!

第一步:通过keylruEntries中拿到 entry对象

第二步:通过entry对象和我们之前创建磁盘缓存对象的valueCount(一个key对应几个缓存文件)分别拿到clean文件名和文件输入流,有了缓存文件的输入流,目的已经达到了,接下来进入这篇文章的尾声了,然后统计redundantOpCount++;对日志文件操作次数,往日志文件中写入:

journalWriter.append(READ+ ' ' + key + '\n');

executorService.submit(cleanupCallable);该清除啥,清除啥!

第三步:最后返回一个 Snapshot对象 key,文件,文件大小,文件输入流等。

 

到此结束另外DiskLruCache 源码 universalimageloader 图片加载框架源码里面有。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值