android缓存系列:DiskLruCache源码分析

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.DiskLruCache
1
1
1

DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY be8bdac81c12a08e15988555d85dfd2b
CLEAN be8bdac81c12a08e15988555d85dfd2b 99
READ be8bdac81c12a08e15988555d85dfd2b
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 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

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页