zoukankan      html  css  js  c++  java
  • DiskLruCache 源码详解

    https://github.com/JakeWharton/DiskLruCache

    1. 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.
    即 1 表示 diskCache 的版本,100 表示应用的版本,2 表示一个 key 对应多少的缓存文件
    

    接下来的每一行,都可以看作 [状态][key](可选状态的对应值)

    CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 对应[状态] [key] [缓存文件 1 的 size] [缓存文件 2 的 size]


    2.状态

    private static final String CLEAN = "CLEAN";
    
    private static final String DIRTY = "DIRTY";
    
    private static final String REMOVE = "REMOVE";
    
    private static final String READ = "READ";
    
    • DIRTY 创建或者修改一个缓存的时候,会有一条DIRTY记录,后面会跟一个CLEAN或REMOVE的记录。如果没有CLEAN或REMOVE,对应的缓存文件是无效的,会被删掉
    • CLEAN 表示对应的缓存操作成功了,后面会带上缓存文件的大小
    • REMOVE 表示对应的缓存被删除了
    • READ 表示对应的缓存被访问了,因为LRU需要READ记录来调整缓存的顺序

    3.源码解析

    3.1 open 函数

    DiskLruCacheopen()函数着手

    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");
        }
    
        //查看日志的备份文件是否存在
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        //如果日志的备份文件存在,且日志的正常文件存在,则将日志的备份文件删除
        //否则,将备份文件重命名为正常的日志文件,即j ournalFile
        if (backupFile.exists()) {
            File journalFile = new File(directory, JOURNAL_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);
        //如果日志文件存在,读取相关信息
        //目的是为了构建 entry 列表
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                return cache;
            } catch (IOException journalIsCorrupt) {
                System.out
                        .println("DiskLruCache "
                                + directory
                                + " is corrupt: "
                                + journalIsCorrupt.getMessage()
                                + ", removing");
                cache.delete();
            }
        }
    
        // 如果日志文件不存在,则新建一个
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }
    

    open()函数这,有两条路,一条是日志文件存在,则读取相关信息构建 entry 列表,否则新建日志文件

    先从简单的新建日志文件看起,即 rebuildJournal()函数

    private synchronized void rebuildJournal() throws IOException {
            if (journalWriter != null) {
                journalWriter.close();
            }
    		// writer 写的是 journalFileTmp 这个日志临时缓存文件
            Writer writer = new BufferedWriter(
                    new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
            //写入日志文件的文件头,不懂可以翻上去看 1
            try {
                writer.write(MAGIC);
                writer.write("
    ");
                writer.write(VERSION_1);
                writer.write("
    ");
                writer.write(Integer.toString(appVersion));
                writer.write("
    ");
                writer.write(Integer.toString(valueCount));
                writer.write("
    ");
                writer.write("
    ");
    
                for (Entry entry : lruEntries.values()) {
                    if (entry.currentEditor != null) {
                        writer.write(DIRTY + ' ' + entry.key + '
    ');
                    } else {
                        writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
    ');
                    }
                }
            } finally {
                writer.close();
            }
    		//如果日志文件已经存在,将会被上面的缓存文件所取代
            if (journalFile.exists()) {
                renameTo(journalFile, journalFileBackup, true);
            }
            renameTo(journalFileTmp, journalFile, false);
            journalFileBackup.delete();
    		//初始化 writer
            journalWriter = new BufferedWriter(
                    new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
        }
    

    重建日志文件部分已经讲完,回到 open()函数,对于日志文件存在的情况下,有两个函数,分别是 readJournal()processJournal()readJournal() 的作用是读取每一行 JournalFile 的记录,转换到 lruEntries 中。processJournal()负责将清除掉journalFileTmp中间文件,清除掉不一致的记录。

    private final LinkedHashMap<String, Entry> lruEntries =
                new LinkedHashMap<String, Entry>(0, 0.75f, true);
    
    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 {
                        //构建 lruEntries 列表
                        readJournalLine(reader.readLine());
                        lineCount++;
                    } catch (EOFException endOfJournal) {
                        break;
                    }
                }
                redundantOpCount = lineCount - lruEntries.size();
    
                // If we ended on a truncated line, rebuild the journal before appending to it.
                if (reader.hasUnterminatedLine()) {
                    rebuildJournal();
                } else {
                    journalWriter = new BufferedWriter(new OutputStreamWriter(
                            new FileOutputStream(journalFile, true), Util.US_ASCII));
                }
            } finally {
                Util.closeQuietly(reader);
            }
        }
        
       
    private final class Entry {
        private final String key;
    
        /** Lengths of this entry's files.
         *
         * 这个 entry 中每个文件的长度,一般为 1
         */
        private final long[] lengths;
    
        /** True if this entry has ever been published. */
        private boolean readable;
    
        /** The ongoing edit or null if this entry is not being edited. */
        private Editor currentEditor;
    
        /** The sequence number of the most recently committed edit to this entry.
         *  最近编辑他的序列号
         */
        private long sequenceNumber;
    
        private Entry(String key) {
            this.key = key;
            this.lengths = new long[valueCount];
        }
    
        public String getLengths() throws IOException {
            StringBuilder result = new StringBuilder();
            for (long size : lengths) {
                result.append(' ').append(size);
            }
            return result.toString();
        }
    
        /** Set lengths using decimal numbers like "10123". */
        private void setLengths(String[] strings) throws IOException {
            if (strings.length != valueCount) {
                throw invalidLengths(strings);
            }
    
            try {
                for (int i = 0; i < strings.length; i++) {
                    lengths[i] = Long.parseLong(strings[i]);
                }
            } catch (NumberFormatException e) {
                throw invalidLengths(strings);
            }
        }
    
        private IOException invalidLengths(String[] strings) throws IOException {
            throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
        }
    
        public File getCleanFile(int i) {
            return new File(directory, key + "." + i);
        }
    
        public File getDirtyFile(int i) {
            return new File(directory, key + "." + i + ".tmp");
        }
    }
    

    至此,open()函数流程基本走完。

    剩下get()edit()


    3.2 get()

    get()的操作比较简单,获取到指定的 Entry,拿 Entry的 CleanFile 生成InputStream,封装成 Snapshot` 给外界

    //通过 key 来获取对应的 Snapshot
    public synchronized Snapshot get(String key) throws IOException {
    	//如果日志文件的 writer 未被初始化,或者 key 异常直接抛出异常
    	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.
        InputStream[] ins = new InputStream[valueCount];
        try {
            for (int i = 0; i < valueCount; i++) {
                ins[i] = new FileInputStream(entry.getCleanFile(i));
            }
        } 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 + '
    ');
        if (journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    
        return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
    }
    
    
    /**
     * We only rebuild the journal when it will halve the size of the journal
     * and eliminate at least 2000 ops.
     */
    private boolean journalRebuildRequired() {
        final int redundantOpCompactThreshold = 2000;
        return redundantOpCount >= redundantOpCompactThreshold //
                && redundantOpCount >= lruEntries.size();
    }
    
    /** A snapshot of the values for an entry. */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private final InputStream[] ins;
        private final long[] lengths;
    
        private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
            this.key = key;
            this.sequenceNumber = sequenceNumber;
            this.ins = ins;
            this.lengths = lengths;
        }
    
        /**
         * Returns an editor for this snapshot's entry, or null if either the
         * entry has changed since this snapshot was created or if another edit
         * is in progress.
         */
        public Editor edit() throws IOException {
            return DiskLruCache.this.edit(key, sequenceNumber);
        }
    
        /** Returns the unbuffered stream with the value for {@code index}. */
        public InputStream getInputStream(int index) {
            return ins[index];
        }
    
        /** Returns the string value for {@code index}. */
        public String getString(int index) throws IOException {
            return inputStreamToString(getInputStream(index));
        }
    
        /** Returns the byte length of the value for {@code index}. */
        public long getLength(int index) {
            return lengths[index];
        }
    
        public void close() {
            for (InputStream in : ins) {
                Util.closeQuietly(in);
            }
        }
    }
    

    3.3 edit()

    edit() 的流程也不复杂,加入IruEntries,生成 Editor,向 journal 日志中写入 DIRTY 记录,后续 Editor 会获取 EntryDirtyFile 生成一个 OutputStream 提供给外部写入

    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 + '
    ');
        journalWriter.flush();
        return editor;
    }
    

    外部获取输出流

    public OutputStream newOutputStream(int index) throws IOException {
        if (index < 0 || index >= valueCount) {
            throw new IllegalArgumentException("Expected index " + index + " to "
                    + "be greater than 0 and less than the maximum value count "
                    + "of " + valueCount);
        }
        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);
        }
    }
    

    最后 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;
    }
    
    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;
                }
            }
        }
    
    	//将 Entry 的 DirtyFile 重命名为 CleanFile,计算总的 size 大小
        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;
                }
            } else {
                deleteIfExists(dirty);
            }
        }
    
        redundantOpCount++;
        //将 entry.currentEditor 赋空,表示写完成
        entry.currentEditor = null;
        //成功则将 entry 设置可读,增加一个 CLEAN 记录,否则失败了就移除掉 entry,增加一个 REMOVE 记录
        if (entry.readable | success) {
            entry.readable = true;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
    ');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '
    ');
        }
        journalWriter.flush();
    
    	//如果超果了容量限制,清理
        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }
    

    4. Last

    DiskLruCache为了防止写失败出现问题,都会先尝试写一个中间文件,成功后再重命名为目标文件

  • 相关阅读:
    JLOI2012:时间流逝
    bzoj 5217: [Lydsy2017省队十连测]航海舰队
    bzoj 4894: 天赋
    bzoj 4870: [Shoi2017]组合数问题
    bzoj 1558: [JSOI2009]等差数列
    bzoj 4945: [Noi2017]游戏
    bzoj 2142: 礼物
    bzoj 5248: [2018多省省队联测]一双木棋
    51nod2383
    codeforces24D
  • 原文地址:https://www.cnblogs.com/huaranmeng/p/14071708.html
Copyright © 2011-2022 走看看