• 基于Demo解析缓存工具DiskLruCache


    https://github.com/mlxy/DiskLruCacheTest

    首先是DiskLruCache(下称DLC)的源码,就一个文件,随便扔在哪用就行。

    /*
     * Copyright (C) 2011 The Android Open Source Project
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    import java.io.BufferedInputStream;
    import java.io.BufferedWriter;
    import java.io.Closeable;
    import java.io.EOFException;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.FileWriter;
    import java.io.FilterOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.OutputStreamWriter;
    import java.io.Reader;
    import java.io.StringWriter;
    import java.io.Writer;
    import java.lang.reflect.Array;
    import java.nio.charset.Charset;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Iterator;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    /**
     ******************************************************************************
     * Taken from the JB source code, can be found in:
     * libcore/luni/src/main/java/libcore/io/DiskLruCache.java
     * or direct link:
     * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
     ******************************************************************************
     *
     * A cache that uses a bounded amount of space on a filesystem. Each cache
     * entry has a string key and a fixed number of values. Values are byte
     * sequences, accessible as streams or files. Each value must be between {@code
     * 0} and {@code Integer.MAX_VALUE} bytes in length.
     *
     * <p>The cache stores its data in a directory on the filesystem. This
     * directory must be exclusive to the cache; the cache may delete or overwrite
     * files from its directory. It is an error for multiple processes to use the
     * same cache directory at the same time.
     *
     * <p>This cache limits the number of bytes that it will store on the
     * filesystem. When the number of stored bytes exceeds the limit, the cache will
     * remove entries in the background until the limit is satisfied. The limit is
     * not strict: the cache may temporarily exceed it while waiting for files to be
     * deleted. The limit does not include filesystem overhead or the cache
     * journal so space-sensitive applications should set a conservative limit.
     *
     * <p>Clients call {@link #edit} to create or update the values of an entry. An
     * entry may have only one editor at one time; if a value is not available to be
     * edited then {@link #edit} will return null.
     * <ul>
     *     <li>When an entry is being <strong>created</strong> it is necessary to
     *         supply a full set of values; the empty value should be used as a
     *         placeholder if necessary.
     *     <li>When an entry is being <strong>edited</strong>, it is not necessary
     *         to supply data for every value; values default to their previous
     *         value.
     * </ul>
     * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
     * or {@link Editor#abort}. Committing is atomic: a read observes the full set
     * of values as they were before or after the commit, but never a mix of values.
     *
     * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
     * observe the value at the time that {@link #get} was called. Updates and
     * removals after the call do not impact ongoing reads.
     *
     * <p>This class is tolerant of some I/O errors. If files are missing from the
     * filesystem, the corresponding entries will be dropped from the cache. If
     * an error occurs while writing a cache value, the edit will fail silently.
     * Callers should handle other problems by catching {@code IOException} and
     * responding appropriately.
     */
    public final class DiskLruCache implements Closeable {
        static final String JOURNAL_FILE = "journal";
        static final String JOURNAL_FILE_TMP = "journal.tmp";
        static final String MAGIC = "libcore.io.DiskLruCache";
        static final String VERSION_1 = "1";
        static final long ANY_SEQUENCE_NUMBER = -1;
        private static final String CLEAN = "CLEAN";
        private static final String DIRTY = "DIRTY";
        private static final String REMOVE = "REMOVE";
        private static final String READ = "READ";
    
        private static final Charset UTF_8 = Charset.forName("UTF-8");
        private static final int IO_BUFFER_SIZE = 8 * 1024;
    
        /*
         * 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.
         */
    
        private final File directory;
        private final File journalFile;
        private final File journalFileTmp;
        private final int appVersion;
        private final long maxSize;
        private final int valueCount;
        private long size = 0;
        private Writer journalWriter;
        private final LinkedHashMap<String, Entry> lruEntries
                = new LinkedHashMap<String, Entry>(0, 0.75f, true);
        private int redundantOpCount;
    
        /**
         * To differentiate between old and current snapshots, each entry is given
         * a sequence number each time an edit is committed. A snapshot is stale if
         * its sequence number is not equal to its entry's sequence number.
         */
        private long nextSequenceNumber = 0;
    
        /* From java.util.Arrays */
        @SuppressWarnings("unchecked")
        private static <T> T[] copyOfRange(T[] original, int start, int end) {
            final int originalLength = original.length; // For exception priority compatibility.
            if (start > end) {
                throw new IllegalArgumentException();
            }
            if (start < 0 || start > originalLength) {
                throw new ArrayIndexOutOfBoundsException();
            }
            final int resultLength = end - start;
            final int copyLength = Math.min(resultLength, originalLength - start);
            final T[] result = (T[]) Array
                    .newInstance(original.getClass().getComponentType(), resultLength);
            System.arraycopy(original, start, result, 0, copyLength);
            return result;
        }
    
        /**
         * Returns the remainder of 'reader' as a string, closing it when done.
         */
        public static String readFully(Reader reader) throws IOException {
            try {
                StringWriter writer = new StringWriter();
                char[] buffer = new char[1024];
                int count;
                while ((count = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, count);
                }
                return writer.toString();
            } finally {
                reader.close();
            }
        }
    
        /**
         * Returns the ASCII characters up to but not including the next "
    ", or
         * "
    ".
         *
         * @throws java.io.EOFException if the stream is exhausted before the next newline
         *     character.
         */
        public static String readAsciiLine(InputStream in) throws IOException {
            // TODO: support UTF-8 here instead
    
            StringBuilder result = new StringBuilder(80);
            while (true) {
                int c = in.read();
                if (c == -1) {
                    throw new EOFException();
                } else if (c == '
    ') {
                    break;
                }
    
                result.append((char) c);
            }
            int length = result.length();
            if (length > 0 && result.charAt(length - 1) == '
    ') {
                result.setLength(length - 1);
            }
            return result.toString();
        }
    
        /**
         * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
         */
        public static void closeQuietly(Closeable closeable) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (RuntimeException rethrown) {
                    throw rethrown;
                } catch (Exception ignored) {
                }
            }
        }
    
        /**
         * Recursively delete everything in {@code dir}.
         */
        // TODO: this should specify paths as Strings rather than as Files
        public static void deleteContents(File dir) throws IOException {
            File[] files = dir.listFiles();
            if (files == null) {
                throw new IllegalArgumentException("not a directory: " + dir);
            }
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteContents(file);
                }
                if (!file.delete()) {
                    throw new IOException("failed to delete file: " + file);
                }
            }
        }
    
        /** This cache uses a single background thread to evict entries. */
        private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
                60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        private final Callable<Void> cleanupCallable = new Callable<Void>() {
            @Override public Void call() throws Exception {
                synchronized (DiskLruCache.this) {
                    if (journalWriter == null) {
                        return null; // closed
                    }
                    trimToSize();
                    if (journalRebuildRequired()) {
                        rebuildJournal();
                        redundantOpCount = 0;
                    }
                }
                return null;
            }
        };
    
        private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
            this.directory = directory;
            this.appVersion = appVersion;
            this.journalFile = new File(directory, JOURNAL_FILE);
            this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
            this.valueCount = valueCount;
            this.maxSize = maxSize;
        }
    
        /**
         * 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 java.io.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;
        }
    
        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);
            }
        }
    
        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);
            }
        }
    
        /**
         * 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];
                    }
                } else {
                    entry.currentEditor = null;
                    for (int t = 0; t < valueCount; t++) {
                        deleteIfExists(entry.getCleanFile(t));
                        deleteIfExists(entry.getDirtyFile(t));
                    }
                    i.remove();
                }
            }
        }
    
        /**
         * Creates a new journal that omits redundant information. This replaces the
         * current journal if it exists.
         */
        private synchronized void rebuildJournal() throws IOException {
            if (journalWriter != null) {
                journalWriter.close();
            }
    
            Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
            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() + '
    ');
                }
            }
    
            writer.close();
            journalFileTmp.renameTo(journalFile);
            journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
        }
    
        private static void deleteIfExists(File file) throws IOException {
    //        try {
    //            Libcore.os.remove(file.getPath());
    //        } catch (ErrnoException errnoException) {
    //            if (errnoException.errno != OsConstants.ENOENT) {
    //                throw errnoException.rethrowAsIOException();
    //            }
    //        }
            if (file.exists() && !file.delete()) {
                throw new IOException();
            }
        }
    
        /**
         * Returns a snapshot of the entry named {@code key}, or null if it doesn't
         * exist is not currently readable. If a value is returned, it is moved to
         * the head of the LRU queue.
         */
        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.
             */
            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!
                return null;
            }
    
            redundantOpCount++;
            journalWriter.append(READ + ' ' + key + '
    ');
            if (journalRebuildRequired()) {
                executorService.submit(cleanupCallable);
            }
    
            return new Snapshot(key, entry.sequenceNumber, ins);
        }
    
        /**
         * Returns an editor for the entry named {@code key}, or null if another
         * edit is in progress.
         */
        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 + '
    ');
            journalWriter.flush();
            return editor;
        }
    
        /**
         * Returns the directory where this cache stores its data.
         */
        public File getDirectory() {
            return directory;
        }
    
        /**
         * Returns the maximum number of bytes that this cache should use to store
         * its data.
         */
        public long maxSize() {
            return maxSize;
        }
    
        /**
         * Returns the number of bytes currently being used to store the values in
         * this cache. This may be greater than the max size if a background
         * deletion is pending.
         */
        public synchronized long size() {
            return size;
        }
    
        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 (!entry.getDirtyFile(i).exists()) {
                        editor.abort();
                        throw new IllegalStateException("edit didn't create file " + i);
                    }
                }
            }
    
            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 = null;
            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 + '
    ');
            }
    
            if (size > maxSize || journalRebuildRequired()) {
                executorService.submit(cleanupCallable);
            }
        }
    
        /**
         * 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 REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
            return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
                    && redundantOpCount >= lruEntries.size();
        }
    
        /**
         * Drops the entry for {@code key} if it exists and can be removed. Entries
         * actively being edited cannot be removed.
         *
         * @return true if an entry was removed.
         */
        public synchronized boolean remove(String key) throws IOException {
            checkNotClosed();
            validateKey(key);
            Entry entry = lruEntries.get(key);
            if (entry == null || entry.currentEditor != null) {
                return false;
            }
    
            for (int i = 0; i < valueCount; i++) {
                File file = entry.getCleanFile(i);
                if (!file.delete()) {
                    throw new IOException("failed to delete " + file);
                }
                size -= entry.lengths[i];
                entry.lengths[i] = 0;
            }
    
            redundantOpCount++;
            journalWriter.append(REMOVE + ' ' + key + '
    ');
            lruEntries.remove(key);
    
            if (journalRebuildRequired()) {
                executorService.submit(cleanupCallable);
            }
    
            return true;
        }
    
        /**
         * Returns true if this cache has been closed.
         */
        public boolean isClosed() {
            return journalWriter == null;
        }
    
        private void checkNotClosed() {
            if (journalWriter == null) {
                throw new IllegalStateException("cache is closed");
            }
        }
    
        /**
         * Force buffered operations to the filesystem.
         */
        public synchronized void flush() throws IOException {
            checkNotClosed();
            trimToSize();
            journalWriter.flush();
        }
    
        /**
         * Closes this cache. Stored values will remain on the filesystem.
         */
        public synchronized void close() throws IOException {
            if (journalWriter == null) {
                return; // already closed
            }
            for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
                if (entry.currentEditor != null) {
                    entry.currentEditor.abort();
                }
            }
            trimToSize();
            journalWriter.close();
            journalWriter = null;
        }
    
        private void trimToSize() throws IOException {
            while (size > maxSize) {
    //            Map.Entry<String, Entry> toEvict = lruEntries.eldest();
                final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
                remove(toEvict.getKey());
            }
        }
    
        /**
         * Closes the cache and deletes all of its stored values. This will delete
         * all files in the cache directory including files that weren't created by
         * the cache.
         */
        public void delete() throws IOException {
            close();
            deleteContents(directory);
        }
    
        private void validateKey(String key) {
            if (key.contains(" ") || key.contains("
    ") || key.contains("
    ")) {
                throw new IllegalArgumentException(
                        "keys must not contain spaces or newlines: "" + key + """);
            }
        }
    
        private static String inputStreamToString(InputStream in) throws IOException {
            return readFully(new InputStreamReader(in, UTF_8));
        }
    
        /**
         * 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 Snapshot(String key, long sequenceNumber, InputStream[] ins) {
                this.key = key;
                this.sequenceNumber = sequenceNumber;
                this.ins = ins;
            }
    
            /**
             * 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));
            }
    
            @Override public void close() {
                for (InputStream in : ins) {
                    closeQuietly(in);
                }
            }
        }
    
        /**
         * Edits the values for an entry.
         */
        public final class Editor {
            private final Entry entry;
            private boolean hasErrors;
    
            private Editor(Entry entry) {
                this.entry = entry;
            }
    
            /**
             * Returns an unbuffered input stream to read the last committed value,
             * or null if no value has been committed.
             */
            public InputStream newInputStream(int index) throws IOException {
                synchronized (DiskLruCache.this) {
                    if (entry.currentEditor != this) {
                        throw new IllegalStateException();
                    }
                    if (!entry.readable) {
                        return null;
                    }
                    return new FileInputStream(entry.getCleanFile(index));
                }
            }
    
            /**
             * Returns the last committed value as a string, or null if no value
             * has been committed.
             */
            public String getString(int index) throws IOException {
                InputStream in = newInputStream(index);
                return in != null ? inputStreamToString(in) : null;
            }
    
            /**
             * 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();
                    }
                    return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
                }
            }
    
            /**
             * Sets the value at {@code index} to {@code value}.
             */
            public void set(int index, String value) throws IOException {
                Writer writer = null;
                try {
                    writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
                    writer.write(value);
                } finally {
                    closeQuietly(writer);
                }
            }
    
            /**
             * Commits this edit so it is visible to readers.  This releases the
             * edit lock so another edit may be started on the same key.
             */
            public void commit() throws IOException {
                if (hasErrors) {
                    completeEdit(this, false);
                    remove(entry.key); // the previous entry is stale
                } else {
                    completeEdit(this, true);
                }
            }
    
            /**
             * Aborts this edit. This releases the edit lock so another edit may be
             * started on the same key.
             */
            public void abort() throws IOException {
                completeEdit(this, false);
            }
    
            private class FaultHidingOutputStream extends FilterOutputStream {
                private FaultHidingOutputStream(OutputStream out) {
                    super(out);
                }
    
                @Override public void write(int oneByte) {
                    try {
                        out.write(oneByte);
                    } catch (IOException e) {
                        hasErrors = true;
                    }
                }
    
                @Override public void write(byte[] buffer, int offset, int length) {
                    try {
                        out.write(buffer, offset, length);
                    } catch (IOException e) {
                        hasErrors = true;
                    }
                }
    
                @Override public void close() {
                    try {
                        out.close();
                    } catch (IOException e) {
                        hasErrors = true;
                    }
                }
    
                @Override public void flush() {
                    try {
                        out.flush();
                    } catch (IOException e) {
                        hasErrors = true;
                    }
                }
            }
        }
    
        private final class Entry {
            private final String key;
    
            /** Lengths of this entry's files. */
            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: " + 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");
            }
        }
    }
    DiskLruCache

    ·DiskLruCacheHelper类

    然后根据这个小Demo的需要做一下简单封装,写了个DiskLruCacheHelper类。

    初始化

    DLC的构造函数是私有的,需要用静态方法初始化。

     DiskLruCache.open(File directory, int appVersion, int valueCount, long maxSize)

    第一个参数directory是缓存路径,可以直接用应用的缓存路径,这样在应用卸载的时候缓存也会自动清除。

    写一个简单的判断,插了SD卡或者SD卡不能移除就用SD卡缓存目录,否则用系统缓存目录。

    1 private static DiskLruCache mCache;
    2 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
    3         || !Environment.isExternalStorageRemovable()) {
    4     mCache = DiskLruCache.open(context.getExternalCacheDir(), 0, 1, 10 * 1024 * 1024);
    5 } else {
    6     mCache = DiskLruCache.open(context.getCacheDir(), 0, 1, 10 * 1024 * 1024);
    7 }

    第二个参数appVersion是应用版本,我这里因为只是个Demo就直接给了0,实际应用中可以用versionCode。

    顺手查了个代码,一并放上来得了。

    Context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode

    在版本号发生变动的时候DLC会自动删除所有缓存。

    第三个参数valueCount决定了一个key可以对应多少个缓存文件,正常情况下给1就行。

    ……

    但是本着负责任的态度我决定去试试改成2会怎么样。

    ……

    嗯它报错了。

    ……

    ……

    嗯我决定不整这些没用的。

    我大概能猜到结果是什么样的,但是我决定不把没根据的事拿上来说。

    第四个参数maxSize是缓存数据的总上限,单位是字节。好懂,不用多说。

    保存缓存

    DLC和SharedPreferences类似,也需要用一个Editor来编辑内容。

    Editor使用

    DiskLruCache.edit(String key)

    来初始化,key是缓存的键名,也就是缓存文件的文件名。

    我这个Demo缓存的是网络图片,于是用了图片URL的MD5作为键名,MD5的问题等会另开一篇说。

    之后就可以用

    Editor.newOutputStream(int index)

    打开一个输出流,之后想怎么写就怎么写。

    参数index是和初始化DLC时的valueCount有关,上面填了1,那这里直接给0就可以。

    如果上面不是1的话这里怎么填电视机前聪明的小朋友们应该也能想到。

    和SharedPreferences一样,Editor在编辑完数据后也需要使用commit()来提交修改。相反如果调用abort()就可以放弃修改。

    不commit的话虽然也有缓存,但是每次还是会从网上重新下载,后面讲日志的时候细说。

    读取缓存

    读取起来就很简单了,使用

    DiskLruCache.get(String key)

    方法拿到Snapshot对象,再用

    Snapshot.getInputStream(int index)

    方法拿到输入流就可以随便读了。

    写出日志

    最后是DiskLruCache.flush()方法,用过IO流的话应该知道它的作用,在这里也类似,是用于将修改同步到日志文件中的。

    在Activity的onPause()方法中调一次就行,不需要每次写出都执行一遍,影响执行效率。

    另外和IO流中一样,这个方法有的时候会自动调用,所以在某些场景下好像一直不用也没问题,但是保不齐什么时候就出个大岔子是不是。

    没用到而且不说也行的东西

    DiskLruCache.size():一看就懂,缓存的总大小。可以用来给用户看。

    DiskLruCache.remove(String key):一看就懂,删除某个缓存,但是反正DLC能自动清理,干嘛要自己删呢。

    DiskLruCache.delete():一看不太懂,但是说了就懂,清除所有缓存,可以作为手动清理缓存功能给用户用。但是为什么不叫clear或者empty。

    于是放一下完整代码:

     1 package com.mlxy.disklrucachetest.util;
     2 
     3 import android.content.Context;
     4 import android.os.Environment;
     5 
     6 import com.mlxy.disklrucachetest.disklrucache.DiskLruCache;
     7 
     8 import java.io.BufferedInputStream;
     9 import java.io.BufferedOutputStream;
    10 import java.io.IOException;
    11 import java.io.InputStream;
    12 import java.io.OutputStream;
    13 
    14 public class DiskLruCacheHelper {
    15     private static DiskLruCache mCache;
    16 
    17     /** 打开DiskLruCache。 */
    18     public static void openCache(Context context) {
    19         try {
    20             if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
    21                     || !Environment.isExternalStorageRemovable()) {
    22                 mCache = DiskLruCache.open(context.getExternalCacheDir(), 0, 1, 10 * 1024 * 1024);
    23             } else {
    24                 mCache = DiskLruCache.open(context.getCacheDir(), 0, 1, 10 * 1024 * 1024);
    25             }
    26         } catch (IOException e) {
    27             e.printStackTrace();
    28         }
    29     }
    30 
    31     /** 写出缓存。 */
    32     public static void dump(InputStream inputStream, String keyCache) throws IOException {
    33         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
    34 
    35         DiskLruCache.Editor mEditor = mCache.edit(keyCache);
    36 
    37         OutputStream outputStream = mEditor.newOutputStream(0);
    38 
    39         BufferedInputStream bin = new BufferedInputStream(inputStream);
    40         BufferedOutputStream bout = new BufferedOutputStream(outputStream);
    41 
    42         byte[] buf = new byte[1024];
    43         int len;
    44         while ((len = bin.read(buf)) != -1) {
    45             bout.write(buf, 0, len);
    46         }
    47 
    48         bout.close();
    49         outputStream.close();
    50 
    51         mEditor.commit();
    52     }
    53 
    54     /** 读取缓存。 */
    55     public static InputStream load(String keyCache) throws IOException {
    56         if (mCache == null) throw new IllegalStateException("Must call openCache() first!");
    57 
    58         DiskLruCache.Snapshot snapshot = mCache.get(keyCache);
    59 
    60         if (snapshot == null) return null;
    61         else return snapshot.getInputStream(0);
    62     }
    63 
    64     /** 同步日志。 */
    65     public static void syncLog() {
    66         try {
    67             mCache.flush();
    68         } catch (IOException e) {
    69             e.printStackTrace();
    70         }
    71     }
    72 }
    现在可以公开的情报

    ·关于日志

    缓存文件夹下除了自己起名的缓存文件之外还有一个叫journal的文件,内容像这样:

    libcore.io.DiskLruCache

    1

    0

    1

    DIRTY 5d11e89e475c9f38ddf14eeb58c1affa

    CLEAN 5d11e89e475c9f38ddf14eeb58c1affa 200635

    READ 5d11e89e475c9f38ddf14eeb58c1affa

    DIRTY 5d11e89e475c9f38ddf14eeb58c1affa

    REMOVE 5d11e89e475c9f38ddf14eeb58c1affa

    前四行分别是DLC的包名、DLC的版本、应用的版本和valueCount,后面两个就是初始化DiskLruCache类时传入的两个对应的参数。

    第五行像HTML一样是个空行,再下面就是操作日志了。

    再下面的记录分四种:

    1. DIRTY:

    在刚刚写入缓存没有commit也没有flush的时候DLC会写一条DIRTY记录,后面是缓存的键名。

    DIRTY数据在下一次打开DLC时会被删除。

    不commit谁知道你数据下完没有。

    2. CLEAN:

    commit之后就会再写入一条CLEAN记录,后面依然是键名,再后面是缓存的大小。这时候这条缓存就算写入完成了。

    3. REMOVE:

    写入缓存之后不commit而是调用Editor.abort()或者调用DiskLruCache.remove(String key)方法删除一条已有的缓存时,会写出一条REMOVE记录,后面还是键名我都懒得说了。

    abort之后会删除已写入的缓存文件,和不commit的情况略有不同。

    4. READ:

    顾名思义,每调用DiskLruCache.get(String key)时写一条,不多说。

    日志条数达到2000条之后DLC会自动清理日志,不需要担心大小问题。


     1 package com.mlxy.disklrucachetest.util;
     2 
     3 import java.io.IOException;
     4         import java.io.InputStream;
     5         import java.net.HttpURLConnection;
     6         import java.net.URL;
     7 
     8 public class NetworkAdministrator {
     9     /** 打开指定地址的输入流。 */
    10     public static InputStream openUrlInputStream(String url) throws IOException {
    11         HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
    12         return urlConnection.getInputStream();
    13     }
    14 }
    没啥好说的的类A
     1 package com.mlxy.disklrucachetest.util;
     2 
     3 import java.security.MessageDigest;
     4 import java.security.NoSuchAlgorithmException;
     5 
     6 public class Digester {
     7     /** 对数据作MD5加密。 */
     8     public static String hashUp(String src) {
     9         String hash = null;
    10 
    11         try {
    12             byte[] md5 = MessageDigest.getInstance("md5").digest(src.getBytes());
    13 
    14             StringBuilder builder = new StringBuilder();
    15             for (byte b : md5) {
    16                 String hex = Integer.toHexString(0xff & b);
    17                 if (hex.length() == 1) {
    18                     builder.append('0');
    19                 }
    20 
    21                 builder.append(hex);
    22             }
    23             hash = builder.toString();
    24         } catch (NoSuchAlgorithmException e) {
    25             e.printStackTrace();
    26         }
    27 
    28         return hash;
    29     }
    30 }
    等会细说的类B

    最后祝你,身体健康,再见。

  • 相关阅读:
    2017年上海金马五校程序设计竞赛:Problem C : Count the Number (模拟)
    2017年上海金马五校程序设计竞赛:Problem B : Sailing (广搜)
    2017年上海金马五校程序设计竞赛:Problem A : STEED Cards (STL全排列函数)
    之江学院第0届校赛 qwb去面试 (找规律)
    [leetcode-41-First Missing Positive]
    [leetcode-625-Minimum Factorization]
    [leetcode-623-Add One Row to Tree]
    [leetcode-624-Maximum Distance in Arrays]
    [leetcode-95-Unique Binary Search Trees II]
    [leetcode-96-Unique Binary Search Trees]
  • 原文地址:https://www.cnblogs.com/chihane/p/4517196.html
Copyright © 2020-2023  润新知