您的位置:首页 > 移动开发 > Android开发

Android DiskLruCache完全解析,硬盘缓存的最佳方案 对源码的理解

2015-08-30 14:29 639 查看
通过看郭大婶的博客发现了这个库,这么小小的一个库被冠以硬盘缓存的最佳方案,我很是好奇于是想自己一探究竟。

从构造一个DiskLruCache对象开始,通过open传入参数
/**
     * Opens the cache in {@code directory}, creating a cache if none exists
     * there.
     *
     * @param directory a writable directory
     * @param appVersion
     * @param valueCount the number of values per cache entry. Must be positive.
     * @param maxSize the maximum number of bytes this cache should use to store
     * @throws IOException if reading or writing the cache directory fails
     */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // prefer to pick up where we left off
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }


通过注释可以看出几个参数的含义。

然后我们可以看到21行开始new一个DiskLruCache对象,对象构造方法中也就是简单的对全局变量赋值。

第22行是判断cache.journalFile.exists() 从简单的字面意思可以看出这是判断journal文件是否存在,然而确实也是这样的,那么journal文件是用来干嘛的呢?源码里面告诉你了

/*
 * This cache uses a journal file named "journal". A typical journal file
 * looks like this:
 *     libcore.io.DiskLruCache
 *     1
 *     100
 *     2
 *
 *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
 *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
 *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
 *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
 *     DIRTY 1ab96a171faeeee38496d8b330771a7a
 *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
 *     READ 335c4c6028171cfddfbaae1a9c313c52
 *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
 *
 * The first five lines of the journal form its header. They are the
 * constant string "libcore.io.DiskLruCache", the disk cache's version,
 * the application's version, the value count, and a blank line.
 *
 * Each of the subsequent lines in the file is a record of the state of a
 * cache entry. Each line contains space-separated values: a state, a key,
 * and optional state-specific values.
 *   o DIRTY lines track that an entry is actively being created or updated.
 *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
 *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
 *     temporary files may need to be deleted.
 *   o CLEAN lines track a cache entry that has been successfully published
 *     and may be read. A publish line is followed by the lengths of each of
 *     its values.
 *   o READ lines track accesses for LRU.
 *   o REMOVE lines track entries that have been deleted.
 *
 * The journal file is appended to as cache operations occur. The journal may
 * occasionally be compacted by dropping redundant lines. A temporary file named
 * "journal.tmp" will be used during compaction; that file should be deleted if
 * it exists when the cache is opened.
 */

大概意思就是journal是一个日志文件的意思从上到下第一行 libcore.io.DiskLruCache就是相当于一个标志,没啥实际含义,第二行表示的这个缓存的版本号,第三行表示我们APP的版本号,是我们传参进来的,第四行也是我们传进去的CountValue就是我们每次传进去的Key对应的Value的个数,这里一般就是一个啦,第五行就是一个空白行,也是一个分割标记,这一行以下的内容就是真正记录的操作日志了。

从注释我们可以看出日志的格式为:操作状态+空格+key。

操作分四种:DIRTY CLEAN READ REMOVE

DIRTY是我们向DiskLruCache中传入数据(下面会讲怎么传)而没有提交的时候就是一个DIRTY数据

所以当我们commit后这个DIRTY数据操作状态也会变为CLEAN,而其他的两个操作状态就是我们去数据时和移除数据时向日志中写的内容。

下面继续分析刚刚那段源码,如果日志文件不存在就创建日志这里不多说,如果日志文件存在,我们可以看到24行 cache.readJournal(),开始读日志,下面看源码。
private void readJournal() throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
        try {
            String magic = readAsciiLine(in);
            String version = readAsciiLine(in);
            String appVersionString = readAsciiLine(in);
            String valueCountString = readAsciiLine(in);
            String blank = readAsciiLine(in);
            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 + "]");
            }

            while (true) {
                try {
                    readJournalLine(readAsciiLine(in));
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
        } finally {
            closeQuietly(in);
        }
    }
读这段源码之前我们得先看 readAsciiLine(InputStream inputstream);这个方法,其实这个方法就是一行一行读输入流,搞清楚这个我们就能继续读源码了。一直到16行,我们都可以看出是在读日志文件头部的那段数据,这里略过吧。到18行,开始循环读取日志中的数据了,我们又看到了readJournalLine方法,进去看看。

private void readJournalLine(String line) throws IOException {
        String[] parts = line.split(" ");
        if (parts.length < 2) {
            throw new IOException("unexpected journal line: " + line);
        }

        String key = parts[1];
        if (parts[0].equals(REMOVE) && parts.length == 2) {
            lruEntries.remove(key);
            return;
        }

        Entry entry = lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }

        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
            entry.readable = true;
            entry.currentEditor = null;
            entry.setLengths(copyOfRange(parts, 2, parts.length));
        } else if (parts[0].equals(DIRTY) && parts.length == 2) {
            entry.currentEditor = new Editor(entry);
        } else if (parts[0].equals(READ) && parts.length == 2) {
            // this work was already done by calling lruEntries.get()
        } else {
            throw new IOException("unexpected journal line: " + line);
        }
    }
第二行,通过String的split()方法以空格为分隔符将传进来的一行日志分成了字符串数组。如果读取到日志的操作字符是REMOVE,就从lruEntries中将对应的key remove掉,那lruEntries是什么呢,从源码中去寻找。

private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);

源码中第162行。原来是将日志中的内容转换到用LinkedHashMap来实现缓存信息储存,方便用户读取。这一下全都通了,原来我们进行了open操作后,就会自动读取日志,然后将内存中还存在的缓存信息储存在LinkedHashMap中。而对象Entry中就封装了获取key地址等操作。

所以我们open后相当于会进行一次遍历日志操作,将硬盘中的缓存读取到lruEntries中供我们操作。(这里为什么要一行行读日志,会不会太麻烦,为什么不将lruEntries的信息直接缓存到日志中,不会方便很多吗?)

逻辑就是这样,其余的都是一些普通的获取或写入方法了。可以看郭婶的blog
http://blog.csdn.net/guolin_blog/article/details/28863651
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: