android缓存系列:DiskLruCache源码分析

来源:互联网 发布:护肤品使用手法 知乎 编辑:程序博客网 时间:2024/06/14 03:31

disklrucache源码分析

一.项目介绍

LRU是一种算法,disklrucache基于LRU算法实现的磁盘缓存方案。在很多开源项目中都可以看到它的身影,比如universal imageloader等等。

二.简单用法

首先,这个框架会涉及到一个文件,叫做journal,这个文件中会存储每次读取操作的记录;
对于获取一个DiskLruCache,是这样的:

DiskLruCache.open(directory, appVersion,
valueCount, maxSize) ;

关于存一般是这么使用的:

String key = generateKey(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OuputStream os = editor.newOutputStream(0);

因为每个实体都是个文件,所以你可以认为这个os指向一个文件的FileOutputStream,然后把你想存的东西写入就行了,写完以后记得调用:editor.commit()。

关于取一般是这样的:

DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot != null) {
InputStream is = snapShot.getInputStream(0);
}

还是那句,因为每个实体都是文件,所以你返回的is是个FileInputStream,你可以利用is读取出里面的内容,然后做你想做的。

三.总体设计

通过这个代码结构图,我们可以很清晰的看到DiskLruCachen内部有三个内部类,构造方法是私有的,所以必然采用了单例模式进行封装。

对外暴露的主要的公共方法主要有:open、get、edit、remove、flush、delete、remove等,覆盖了缓存管理的增删各个方面。

接下来看一下几个内部类的作用:

Snapshot: Snapshot的中文意思是快照,它的英文备注是:A snapshot of the values for an entry.大致意思是一个缓存值,只不过使用对象封装了一下,提供了更多的信息。

Editor:每个entry对应一个editor(代理模式),我们可以通过这个代理来操作缓存文件

Entity:这是一个bean类,每个entry对应一个缓存文件,保留有缓存文件的一些信息和操作。

四.核心模块分述

1.journal文件

journal文件你打开以后呢,是这个格式;

libcore.io.DiskLruCache111DIRTY c3bac86f2e7a291a1a200b853835b664CLEAN c3bac86f2e7a291a1a200b853835b664 4698READ c3bac86f2e7a291a1a200b853835b664DIRTY c59f9eec4b616dc6682c7fa8bd1e061fCLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698READ c59f9eec4b616dc6682c7fa8bd1e061fDIRTY be8bdac81c12a08e15988555d85dfd2bCLEAN be8bdac81c12a08e15988555d85dfd2b 99READ be8bdac81c12a08e15988555d85dfd2bDIRTY 536788f4dbdffeecfbb8f350a941eea3REMOVE 536788f4dbdffeecfbb8f350a941eea3 

首先看前五行:

第一行固定字符串libcore.io.DiskLruCache
第二行DiskLruCache的版本号,源码中为常量1
第三行为你的app的版本号,当然这个是你自己传入指定的
第四行指每个key对应几个文件,一般为1
第五行,空行
ok,以上5行可以称为该文件的文件头,DiskLruCache初始化的时候,如果该文件存在需要校验该文件头。

接下来的行,可以认为是操作记录。

DIRTY 表示一个entry正在被写入(其实就是把文件的OutputStream交给你了)。那么写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。
REMOVE除了上述的情况呢,当你自己手动调用remove(key)方法的时候也会写入一条REMOVE记录。
READ就是说明有一次读取的记录。
每个CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字(参照文件头第四行)。
从这里看出,只有CLEAN且没有REMOVE的记录,才是真正可用的Cache Entry记录。

2.DiskLruCache#open

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)            throws IOException {        if (maxSize <= 0) {            throw new IllegalArgumentException("maxSize <= 0");        }        if (maxFileCount <= 0) {            throw new IllegalArgumentException("maxFileCount <= 0");        }        if (valueCount <= 0) {            throw new IllegalArgumentException("valueCount <= 0");        }        // If a bkp file exists, use it instead.        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);        if (backupFile.exists()) {            File journalFile = new File(directory, JOURNAL_FILE);            // If journal file also exists just delete backup file.            if (journalFile.exists()) {                backupFile.delete();            } else {                renameTo(backupFile, journalFile, false);            }        }        // Prefer to pick up where we left off.        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);        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 journalIsCorrupt) {                System.out                        .println("DiskLruCache "                                + directory                                + " is corrupt: "                                + journalIsCorrupt.getMessage()                                + ", removing");                cache.delete();            }        }        // Create a new empty cache.        directory.mkdirs();        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);        cache.rebuildJournal();        return cache;    }

首先检查存不存在journal.bkp(journal的备份文件).如果存在:然后检查journal文件是否存在,如果正主在,bkp文件就可以删除了。如果不存在,将bkp文件重命名为journal文件。

接下里判断journal文件是否存在:

如果不存在。创建directory;重新构造disklrucache;调用rebuildJournal建立journal文件

    private void readJournal() throws IOException {        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);        try {            String magic = reader.readLine();            String version = reader.readLine();            String appVersionString = reader.readLine();            String valueCountString = reader.readLine();            String blank = reader.readLine();            if (!MAGIC.equals(magic)                    || !VERSION_1.equals(version)                    || !Integer.toString(appVersion).equals(appVersionString)                    || !Integer.toString(valueCount).equals(valueCountString)                    || !"".equals(blank)) {                throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "                        + valueCountString + ", " + blank + "]");            }            int lineCount = 0;            while (true) {                try {                    readJournalLine(reader.readLine());                    lineCount++;                } catch (EOFException endOfJournal) {                    break;                }            }            redundantOpCount = lineCount - lruEntries.size();        } finally {            Util.closeQuietly(reader);        }    }

可以看到首先构建一个journal.tmp文件,然后写入文件头(5行),然后readJournalLine。

   private void readJournalLine(String line) throws IOException {        int firstSpace = line.indexOf(' ');        if (firstSpace == -1) {            throw new IOException("unexpected journal line: " + line);        }        int keyBegin = firstSpace + 1;        int secondSpace = line.indexOf(' ', keyBegin);        final String key;        if (secondSpace == -1) {            key = line.substring(keyBegin);            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {                lruEntries.remove(key);                return;            }        } else {            key = line.substring(keyBegin, secondSpace);        }        Entry entry = lruEntries.get(key);        if (entry == null) {            entry = new Entry(key);            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 Editor(entry);        } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {            // This work was already done by calling lruEntries.get().        } else {            throw new IOException("unexpected journal line: " + line);        }    }

如果journal文件存在,那么调用readJournal。首先校验文件头,接下来调用readJournalLine按行读取内容。

好了,回到readJournal方法,在我们按行读取的时候,会记录一下lineCount,然后最后给redundantOpCount赋值,这个变量记录的应该是没用的记录条数(文件的行数-真正可以的key的行数)。

最后,如果读取过程中发现journal文件有问题,则重建journal文件。没有问题的话,初始化下journalWriter,关闭reader。

processJournal源码如下:

    /**     * Computes the initial size and collects garbage as a part of opening the     * cache. Dirty entries are assumed to be inconsistent and will be deleted.     */    private void processJournal() throws IOException {        deleteIfExists(journalFileTmp);        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {            Entry entry = i.next();            if (entry.currentEditor == null) {                for (int t = 0; t < valueCount; t++) {                    size += entry.lengths[t];                    fileCount++;                }            } else {                entry.currentEditor = null;                for (int t = 0; t < valueCount; t++) {                    deleteIfExists(entry.getCleanFile(t));                    deleteIfExists(entry.getDirtyFile(t));                }                i.remove();            }        }    }

看英文注释就可以知道主要是统计所有可用的cache占据的容量,赋值给size;对于所有非法DIRTY状态(就是DIRTY单独出现的)的entry,如果存在文件则删除,并且从lruEntries中移除。此时,剩的就真的只有CLEAN状态的key记录了。

3.存入缓存

String key = generateKey(url);  DiskLruCache.Editor editor = mDiskLruCache.edit(key); OuputStream os = editor.newOutputStream(0); 

那么首先就是editor方法;

public Editor edit(String key) throws IOException {        return edit(key, ANY_SEQUENCE_NUMBER);    }    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {        checkNotClosed();        validateKey(key);        Entry entry = lruEntries.get(key);        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null                || entry.sequenceNumber != expectedSequenceNumber)) {            return null; // Snapshot is stale.        }        if (entry == null) {            entry = new Entry(key);            lruEntries.put(key, entry);        } else if (entry.currentEditor != null) {            return null; // Another edit is in progress.        }        Editor editor = new Editor(entry);        entry.currentEditor = editor;        // Flush the journal before creating files to prevent file leaks.        journalWriter.write(DIRTY + ' ' + key + '\n');        journalWriter.flush();        return editor;    }

首先验证key,可以必须是字母、数字、下划线、横线(-)组成,且长度在1-120之间。

然后通过key获取实体,因为我们是存,只要不是正在编辑这个实体,理论上都能返回一个合法的editor对象。

所以接下来判断,如果不存在,则创建一个Entry加入到lruEntries中(如果存在,直接使用),然后为entry.currentEditor进行赋值为new Editor(entry);,最后在journal文件中写入一条DIRTY记录,代表这个文件正在被操作。

注意,如果entry.currentEditor != null不为null的时候,意味着该实体正在被编辑,会retrun null ;

拿到editor对象以后,就是去调用newOutputStream去获得一个文件输入流了。

/**         * Returns a new unbuffered output stream to write the value at         * {@code index}. If the underlying output stream encounters errors         * when writing to the filesystem, this edit will be aborted when         * {@link #commit} is called. The returned output stream does not throw         * IOExceptions.         */        public OutputStream newOutputStream(int index) throws IOException {            synchronized (DiskLruCache.this) {                if (entry.currentEditor != this) {                    throw new IllegalStateException();                }                if (!entry.readable) {                    written[index] = true;                }                File dirtyFile = entry.getDirtyFile(index);                FileOutputStream outputStream;                try {                    outputStream = new FileOutputStream(dirtyFile);                } catch (FileNotFoundException e) {                    // Attempt to recreate the cache directory.                    directory.mkdirs();                    try {                        outputStream = new FileOutputStream(dirtyFile);                    } catch (FileNotFoundException e2) {                        // We are unable to recover. Silently eat the writes.                        return NULL_OUTPUT_STREAM;                    }                }                return new FaultHidingOutputStream(outputStream);            }        }

首先校验index是否在valueCount范围内,一般我们使用都是一个key对应一个文件所以传入的基本都是0。接下来就是通过entry.getDirtyFile(index);拿到一个dirty File对象,为什么叫dirty file呢,其实就是个中转文件,文件格式为key.index.tmp。
将这个文件的FileOutputStream通过FaultHidingOutputStream封装下传给我们。

最后,别忘了我们通过os写入数据以后,需要调用commit方法。

public void commit() throws IOException {      if (hasErrors) {        completeEdit(this, false);        remove(entry.key); // The previous entry is stale.      } else {        completeEdit(this, true);      }      committed = true;    }

首先通过hasErrors判断,是否有错误发生,如果有调用completeEdit(this, false)且调用remove(entry.key);。如果没有就调用completeEdit(this, true);。

那么这里这个hasErrors哪来的呢?还记得上面newOutputStream的时候,返回了一个os,这个os是FileOutputStream,但是经过了FaultHidingOutputStream封装么,这个类实际上就是重写了FilterOutputStream的write相关方法,将所有的IOException给屏蔽了,如果发生IOException就将hasErrors赋值为true.

这样的设计还是很nice的,否则直接将OutputStream返回给用户,如果出错没法检测,还需要用户手动去调用一些操作。

接下来看completeEdit方法。

    private synchronized void completeEdit(Editor editor, boolean success) throws IOException {        Entry entry = editor.entry;        if (entry.currentEditor != editor) {            throw new IllegalStateException();        }        // If this edit is creating the entry for the first time, every index must have a value.        if (success && !entry.readable) {            for (int i = 0; i < 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 (int i = 0; i < 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;                    size = size - oldLength + newLength;                    fileCount++;                }            } else {                deleteIfExists(dirty);            }        }        redundantOpCount++;        entry.currentEditor = null;        if (entry.readable | success) {            entry.readable = true;            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');            if (success) {                entry.sequenceNumber = nextSequenceNumber++;            }        } else {            lruEntries.remove(entry.key);            journalWriter.write(REMOVE + ' ' + entry.key + '\n');        }        journalWriter.flush();        if (size > maxSize || fileCount > maxFileCount || journalRebuildRequired()) {            executorService.submit(cleanupCallable);        }    }

首先判断if (success && !entry.readable)是否成功,且是第一次写入(如果以前这个记录有值,则readable=true),内部的判断,我们都不会走,因为written[i]在newOutputStream的时候被写入true了。而且正常情况下,getDirtyFile是存在的。

接下来,如果成功,将dirtyFile 进行重命名为 cleanFile,文件名为:key.index。然后刷新size的长度。如果失败,则删除dirtyFile.

接下来,如果成功或者readable为true,将readable设置为true,写入一条CLEAN记录。如果第一次提交且失败,那么就会从lruEntries.remove(key),写入一条REMOVE记录。

写入缓存,肯定要控制下size。于是最后,判断是否超过了最大size,或者需要重建journal文件,什么时候需要重建呢?

 private boolean journalRebuildRequired() {    final int redundantOpCompactThreshold = 2000;    return redundantOpCount >= redundantOpCompactThreshold //        && redundantOpCount >= lruEntries.size();  }

如果redundantOpCount达到2000,且超过了lruEntries.size()就重建,这里就可以看到redundantOpCount的作用了。防止journal文件过大。

ok,到此我们的存入缓存就分析完成了。再次总结下,首先调用editor,拿到指定的dirtyFile的OutputStream,你可以尽情的进行写操作,写完以后呢,记得调用commit.
commit中会检测是你是否发生IOException,如果没有发生,则将dirtyFile->cleanFile,将readable=true,写入CLEAN记录。如果发生错误,则删除dirtyFile,从lruEntries中移除,然后写入一条REMOVE记录。

4.取出缓存

DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  if (snapShot != null) {    InputStream is = snapShot.getInputStream(0);  }
   public synchronized Snapshot get(String key) throws IOException {        checkNotClosed();        validateKey(key);        Entry entry = lruEntries.get(key);        if (entry == null) {            return null;        }        if (!entry.readable) {            return null;        }        // Open all streams eagerly to guarantee that we see a single published        // snapshot. If we opened streams lazily then the streams could come        // from different edits.        File[] files = new File[valueCount];        InputStream[] ins = new InputStream[valueCount];        try {            File file;            for (int i = 0; i < valueCount; i++) {                file = entry.getCleanFile(i);                files[i] = file;                ins[i] = new FileInputStream(file);            }        } catch (FileNotFoundException e) {            // A file must have been deleted manually!            for (int i = 0; i < valueCount; i++) {                if (ins[i] != null) {                    Util.closeQuietly(ins[i]);                } else {                    break;                }            }            return null;        }        redundantOpCount++;        journalWriter.append(READ + ' ' + key + '\n');        if (journalRebuildRequired()) {            executorService.submit(cleanupCallable);        }        return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);    }

get方法比较简单,如果取到的为null,或者readable=false,则返回null.否则将cleanFile的FileInputStream进行封装返回Snapshot,且写入一条READ语句。
然后getInputStream就是返回该FileInputStream了。

好了,到此,我们就分析完成了创建DiskLruCache,存入缓存和取出缓存的源码。

参考资料

Android DiskLruCache完全解析,硬盘缓存的最佳方案
http://blog.csdn.net/guolin_blog/article/details/28863651

0 0