美文网首页
DiskLruCache缓存机制

DiskLruCache缓存机制

作者: 耀东wang | 来源:发表于2018-08-29 18:07 被阅读0次
    • 关于Android的三级缓存,其中最主要的就是内存缓存和硬盘缓存。这两种缓存机制的实现都应用到了LruCache(Least Recently Used),采用LRU算法的缓存有两种:LruCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU算法。
    • 一般来说,缓存策略主要包含缓存的添加、获取和删除这三类操作。
    • 而LruCache和DisLruCache一般用于缓存图片信息。

    缓存目录

    /storage/emulated/0/Android/data/package_name/cache 当应用卸载后,存储数据也会被删除。(系统所认定的缓存的路径)
    /storage/emulated/0/mDiskCache 这种存储路径,即使应用卸载,存储数据依然会存在。
    好了不多说,今天我们讲讲DiskLruCache如何缓存请求数据。我会先贴几个类,第一个类不多说大家都很熟悉:

    DiskLruCache.java
    public final class DiskLruCache implements Closeable {
        static final String JOURNAL_FILE = "journal";
        static final String JOURNAL_FILE_TEMP = "journal.tmp";
        static final String JOURNAL_FILE_BACKUP = "journal.bkp";
        static final String MAGIC = "libcore.io.DiskLruCache";
        static final String VERSION_1 = "1";
        static final long ANY_SEQUENCE_NUMBER = -1;
        static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}";
        static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN);
        private static final String CLEAN = "CLEAN";
        private static final String DIRTY = "DIRTY";
        private static final String REMOVE = "REMOVE";
        private static final String READ = "READ";
    
        /*
         * 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 File journalFileBackup;
        private final int appVersion;
        private 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;
    
        /**
         * This cache uses a single background thread to evict entries.
         */
        final ThreadPoolExecutor executorService =
                new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        private final Callable<Void> cleanupCallable = new Callable<Void>() {
            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_TEMP);
            this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
            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 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
         * @return DiskLruCache
         * @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");
            }
    
            System.out.println("......appVersion: " + appVersion + "......");
    
            // 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);
            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();
                }
            }
    
            // Create a new empty cache.
            directory.mkdirs();
            cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
            cache.rebuildJournal();
            return cache;
        }
    
        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();
    
                // 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 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);
            }
        }
    
        /**
         * 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 OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
            try {
                writer.write(MAGIC);
                writer.write("\n");
                writer.write(VERSION_1);
                writer.write("\n");
                writer.write(Integer.toString(appVersion));
                writer.write("\n");
                writer.write(Integer.toString(valueCount));
                writer.write("\n");
                writer.write("\n");
    
                for (Entry entry : lruEntries.values()) {
                    if (entry.currentEditor != null) {
                        writer.write(DIRTY + ' ' + entry.key + '\n');
                    } else {
                        writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
                    }
                }
            } finally {
                writer.close();
            }
    
            if (journalFile.exists()) {
                renameTo(journalFile, journalFileBackup, true);
            }
            renameTo(journalFileTmp, journalFile, false);
            journalFileBackup.delete();
    
            journalWriter = new BufferedWriter(
                    new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
        }
    
        private static void deleteIfExists(File file) throws IOException {
            if (file.exists() && !file.delete()) {
                throw new IOException();
            }
        }
    
        private static void renameTo(File from, File to, boolean deleteDestination) throws IOException {
            if (deleteDestination) {
                deleteIfExists(to);
            }
            if (!from.renameTo(to)) {
                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.
         *
         * @param key key
         * @return Snapshot
         * @throws IOException e
         */
        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!
                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, ins, entry.lengths);
        }
    
        /**
         * Returns an editor for the entry named {@code key}, or null if another
         * edit is in progress.
         *
         * @param key key
         * @return Editor
         * @throws IOException e
         */
        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;
        }
    
        /**
         * Returns the directory where this cache stores its data.
         *
         * @return File
         */
        public File getDirectory() {
            return directory;
        }
    
        /**
         * Returns the maximum number of bytes that this cache should use to store
         * its data.
         *
         * @return long
         */
        public synchronized long getMaxSize() {
            return maxSize;
        }
    
    
        public synchronized void setMaxSize(long maxSize) {
            this.maxSize = maxSize;
            executorService.submit(cleanupCallable);
        }
    
    
        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 (!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;
                    }
                } 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 || 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 redundantOpCompactThreshold = 2000;
            return redundantOpCount >= redundantOpCompactThreshold //
                    && redundantOpCount >= lruEntries.size();
        }
    
        /**
         * Drops the entry for {@code key} if it exists and can be removed. Entries
         * actively being edited cannot be removed.
         *
         * @param key key
         * @return true if an entry was removed.
         * @throws IOException e
         */
        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.exists() && !file.delete()) {
                    throw new IOException("failed to delete " + file);
                }
                size -= entry.lengths[i];
                entry.lengths[i] = 0;
            }
    
            redundantOpCount++;
            journalWriter.append(REMOVE + ' ' + key + '\n');
            lruEntries.remove(key);
    
            if (journalRebuildRequired()) {
                executorService.submit(cleanupCallable);
            }
    
            return true;
        }
    
        /**
         * Returns true if this cache has been closed.
         *
         * @return true
         */
        public synchronized boolean isClosed() {
            return journalWriter == null;
        }
    
        private void checkNotClosed() {
            if (journalWriter == null) {
                throw new IllegalStateException("cache is closed");
            }
        }
    
        /**
         * Force buffered operations to the filesystem.
         *
         * @throws IOException e
         */
        public synchronized void flush() throws IOException {
            checkNotClosed();
            trimToSize();
            journalWriter.flush();
        }
    
        /**
         * Closes this cache. Stored values will remain on the filesystem.
         *
         * @throws IOException e
         */
        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.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.
         *
         * @throws IOException e
         */
        public void delete() throws IOException {
            close();
            Util.deleteContents(directory);
        }
    
        private void validateKey(String key) {
            Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
            if (!matcher.matches()) {
                throw new IllegalArgumentException("keys must match regex "
                        + STRING_KEY_PATTERN + ": \"" + key + "\"");
            }
        }
    
        private static String inputStreamToString(InputStream in) throws IOException {
            return Util.readFully(new InputStreamReader(in, Util.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 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.
             *
             * @return di
             * @throws IOException e
             */
            public Editor edit() throws IOException {
                return DiskLruCache.this.edit(key, sequenceNumber);
            }
    
            /**
             * Returns the unbuffered stream with the value for {@code index}.
             *
             * @param index index
             * @return InputStream
             */
            public InputStream getInputStream(int index) {
                return ins[index];
            }
    
            /**
             * Returns the string value for {@code index}.
             *
             * @param index index
             * @return String
             * @throws IOException e
             */
            public String getString(int index) throws IOException {
                return inputStreamToString(getInputStream(index));
            }
    
            /**
             * Returns the byte length of the value for {@code index}.
             *
             * @param index index
             * @return long
             */
            public long getLength(int index) {
                return lengths[index];
            }
    
            public void close() {
                for (InputStream in : ins) {
                    Util.closeQuietly(in);
                }
            }
        }
    
        private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
            @Override
            public void write(int b) throws IOException {
                // Eat all writes silently. Nom nom.
            }
        };
    
        /**
         * Edits the values for an entry.
         */
        public final class Editor {
            private final Entry entry;
            private final boolean[] written;
            private boolean hasErrors;
            private boolean committed;
    
            private Editor(Entry entry) {
                this.entry = entry;
                this.written = (entry.readable) ? null : new boolean[valueCount];
            }
    
            /**
             * Returns an unbuffered input stream to read the last committed value,
             * or null if no value has been committed.
             *
             * @param index in
             * @return InputStream
             * @throws IOException e
             */
            public InputStream newInputStream(int index) throws IOException {
                synchronized (DiskLruCache.this) {
                    if (entry.currentEditor != this) {
                        throw new IllegalStateException();
                    }
                    if (!entry.readable) {
                        return null;
                    }
                    try {
                        return new FileInputStream(entry.getCleanFile(index));
                    } catch (FileNotFoundException e) {
                        return null;
                    }
                }
            }
    
            /**
             * Returns the last committed value as a string, or null if no value
             * has been committed.
             *
             * @param index in
             * @return string
             * @throws IOException e
             */
            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.
             *
             * @param index in
             * @return out
             * @throws IOException e
             */
            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);
                }
            }
    
            /**
             * Sets the value at {@code index} to {@code value}.
             *
             * @param index in
             * @param value v
             * @throws IOException e
             */
            public void set(int index, String value) throws IOException {
                Writer writer = null;
                try {
                    writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8);
                    writer.write(value);
                } finally {
                    Util.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.
             *
             * @throws IOException e
             */
            public void commit() throws IOException {
                if (hasErrors) {
                    completeEdit(this, false);
                    remove(entry.key); // The previous entry is stale.
                } else {
                    completeEdit(this, true);
                }
                committed = true;
            }
    
            /**
             * Aborts this edit. This releases the edit lock so another edit may be
             * started on the same key.
             *
             * @throws IOException e
             */
            public void abort() throws IOException {
                completeEdit(this, false);
            }
    
            public void abortUnlessCommitted() {
                if (!committed) {
                    try {
                        abort();
                    } catch (IOException ignored) {
                    }
                }
            }
    
            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: " + 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");
            }
        }
    }
    
    StreamUtil.java
    public class StreamUtil {
    
    
        /**
         *
         * @param inputStream inputStream
         * @param cls cls
         * @param <D> T
         * @return D
         */
        public static  <D> List<D> readListStream(InputStream inputStream, Class<D> cls) {
    
            System.out.println("...... className : " + cls.getSimpleName() + " ......");
    
            List<D> resultList = new ArrayList<>();
            ObjectInputStream ois = null;
    
            try {
                ois = new ObjectInputStream(inputStream);
                ArrayList<D> list_ext = (ArrayList<D>) ois.readObject();
    
                for (D obj : list_ext) {
                    if (obj != null) {
                        resultList.add(obj);
                    }
                }
                return resultList;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
    
                if (ois != null) {
                    try {
                        ois.close();
                    } catch (IOException e2) {
                        e2.printStackTrace();
                    }
                }
            }
            return resultList;
        }
    
    
    
        public static <D> D readStream(InputStream inputStream, Class<D> cls) {
    
            System.out.println("...... className : " + cls.getSimpleName() + " ......");
    
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(inputStream);
                D object = (D) ois.readObject();
                return object;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (ois != null) {
                    try {
                        ois.close();
                    } catch (IOException e2) {
                        e2.printStackTrace();
                    }
                }
            }
    
            return null;
        }
    
    
    
        public static boolean writeToStream(OutputStream fos, Object object) {
    
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(fos);
                oos.writeObject(object);
                oos.flush();
                return true;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (oos != null) {
                    try {
                        oos.close();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }
            }
            return false;
        }
    
    
    
    
        /**
         * url to diskStream
         *
         * @param imageUrl     imageUrl
         * @param outputStream outputStream
         * @return is
         */
        public static boolean writeUrlToStream(String imageUrl, OutputStream outputStream) {
            HttpURLConnection urlConnection = null;
            BufferedOutputStream out = null;
            BufferedInputStream in = null;
            try {
                final URL url = new URL(imageUrl);
                urlConnection = (HttpURLConnection) url.openConnection();
                in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
                out = new BufferedOutputStream(outputStream, 8 * 1024);
                int b;
                while ((b = in.read()) != -1) {
                    out.write(b);
                }
                return true;
            } catch (final IOException e) {
                e.printStackTrace();
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                }
                try {
                    if (out != null) {
                        out.close();
                    }
                    if (in != null) {
                        in.close();
                    }
                } catch (final IOException e) {
                    e.printStackTrace();
                }
            }
            return false;
        }
    }
    
    
    StrictLineReader.java
    public class StrictLineReader implements Closeable {
      private static final byte CR = (byte) '\r';
      private static final byte LF = (byte) '\n';
    
      private final InputStream in;
      private final Charset charset;
    
      /*
       * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end
       * and the data in the range [pos, end) is buffered for reading. At end of input, if there is
       * an unterminated line, we set end == -1, otherwise end == pos. If the underlying
       * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1.
       */
      private byte[] buf;
      private int pos;
      private int end;
    
      /**
       * Constructs a new {@code LineReader} with the specified charset and the default capacity.
       *
       * @param in the {@code InputStream} to read data from.
       * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
       * supported.
       * @throws NullPointerException if {@code in} or {@code charset} is null.
       * @throws IllegalArgumentException if the specified charset is not supported.
       */
      public StrictLineReader(InputStream in, Charset charset) {
        this(in, 8192, charset);
      }
    
      /**
       * Constructs a new {@code LineReader} with the specified capacity and charset.
       *
       * @param in the {@code InputStream} to read data from.
       * @param capacity the capacity of the buffer.
       * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are
       * supported.
       * @throws NullPointerException if {@code in} or {@code charset} is null.
       * @throws IllegalArgumentException if {@code capacity} is negative or zero
       * or the specified charset is not supported.
       */
      public StrictLineReader(InputStream in, int capacity, Charset charset) {
        if (in == null || charset == null) {
          throw new NullPointerException();
        }
        if (capacity < 0) {
          throw new IllegalArgumentException("capacity <= 0");
        }
        if (!(charset.equals(Util.US_ASCII))) {
          throw new IllegalArgumentException("Unsupported encoding");
        }
    
        this.in = in;
        this.charset = charset;
        buf = new byte[capacity];
      }
    
      /**
       * Closes the reader by closing the underlying {@code InputStream} and
       * marking this reader as closed.
       *
       * @throws IOException for errors when closing the underlying {@code InputStream}.
       */
      public void close() throws IOException {
        synchronized (in) {
          if (buf != null) {
            buf = null;
            in.close();
          }
        }
      }
    
      /**
       * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"},
       * this end of line marker is not included in the result.
       *
       * @return the next line from the input.
       * @throws IOException for underlying {@code InputStream} errors.
       * @throws EOFException for the end of source stream.
       */
      public String readLine() throws IOException {
        synchronized (in) {
          if (buf == null) {
            throw new IOException("LineReader is closed");
          }
    
          // Read more data if we are at the end of the buffered data.
          // Though it's an error to read after an exception, we will let {@code fillBuf()}
          // throw again if that happens; thus we need to handle end == -1 as well as end == pos.
          if (pos >= end) {
            fillBuf();
          }
          // Try to find LF in the buffered data and return the line if successful.
          for (int i = pos; i != end; ++i) {
            if (buf[i] == LF) {
              int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i;
              String res = new String(buf, pos, lineEnd - pos, charset.name());
              pos = i + 1;
              return res;
            }
          }
    
          // Let's anticipate up to 80 characters on top of those already read.
          ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) {
            @Override
            public String toString() {
              int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count;
              try {
                return new String(buf, 0, length, charset.name());
              } catch (UnsupportedEncodingException e) {
                throw new AssertionError(e); // Since we control the charset this will never happen.
              }
            }
          };
    
          while (true) {
            out.write(buf, pos, end - pos);
            // Mark unterminated line in case fillBuf throws EOFException or IOException.
            end = -1;
            fillBuf();
            // Try to find LF in the buffered data and return the line if successful.
            for (int i = pos; i != end; ++i) {
              if (buf[i] == LF) {
                if (i != pos) {
                  out.write(buf, pos, i - pos);
                }
                pos = i + 1;
                return out.toString();
              }
            }
          }
        }
      }
    
      public boolean hasUnterminatedLine() {
        return end == -1;
      }
    
      /**
       * Reads new input data into the buffer. Call only with pos == end or end == -1,
       * depending on the desired outcome if the function throws.
       */
      private void fillBuf() throws IOException {
        int result = in.read(buf, 0, buf.length);
        if (result == -1) {
          throw new EOFException();
        }
        pos = 0;
        end = result;
      }
    }
    
    
    Util.java
    public final class Util {
        public static final Charset US_ASCII = Charset.forName("US-ASCII");
        public static final Charset UTF_8 = Charset.forName("UTF-8");
    
        private Util() {
        }
    
        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();
            }
        }
    
        /**
         * Deletes the contents of {@code dir}. Throws an IOException if any file
         * could not be deleted, or if {@code dir} is not a readable directory.
         */
        public static void deleteContents(File dir) throws IOException {
            File[] files = dir.listFiles();
            if (files == null) {
                throw new IOException("not a readable directory: " + dir);
            }
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteContents(file);
                }
                if (!file.delete()) {
                    throw new IOException("failed to delete file: " + file);
                }
            }
        }
    
        public static void closeQuietly(/*Auto*/Closeable closeable) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (RuntimeException rethrown) {
                    throw rethrown;
                } catch (Exception ignored) {
                }
            }
        }
    
    
        public static String getCacheDir(Context context) {
            String cachePath;
            if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                    || !Environment.isExternalStorageRemovable()) {
                cachePath = context.getExternalCacheDir().getPath();
            } else {
                cachePath = context.getCacheDir().getPath();
            }
            return cachePath;
        }
    
    
        public static int getAppVersion(Context context) {
            try {
                PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                return info.versionCode;
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
            return 1;
        }
    
    
        public static <T> T requireNonNull(T object, String message) {
            if (object == null) {
                throw new NullPointerException(message);
            }
            return object;
        }
    
        //获取Cache 存储目录
        public static File getCacheFile(Context context,String uniqueName){
    
            String cachePath = null;
            if((Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||
              !Environment.isExternalStorageRemovable()
              && context.getExternalCacheDir().getPath()!=null)){
                cachePath = context.getExternalCacheDir().getPath();
            }else{
                cachePath = context.getCacheDir().getPath();
            }
            return new File(cachePath + File.separator +uniqueName);
    
        }
    }
    
    MD5.java
    /**
     * Created by SerryWang
     * on 2018/8/29
     */
    public class MD5 {
        /**
         * 主要是将key进行MD5编码
         */
        public static String encodeKey(String key){
            String cacheKey;
            try {
                final MessageDigest mDigest = MessageDigest.getInstance("MD5");
                mDigest.update(key.getBytes());
                cacheKey = bytesToHexString(mDigest.digest());
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
                cacheKey = String.valueOf(key.hashCode());
            }
            return cacheKey;
        }
    
        private static String bytesToHexString(byte[] bytes){
    
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i<bytes.length;i++){
                String hex = Integer.toHexString(0xFF & bytes[i]);
                if(hex.length() == 1){
                    sb.append('0');
                }
                sb.append(hex);
            }
            return sb.toString();
        }
    
    }
    
    

    以上几个就是配合使用DiskLruCache的工具类。
    那么具体该如何使用呢?

    • 初始化DiskLruCache
     //初始化DiskLruCache
        public DiskLruCache.Editor initDiskLruCaches(){
            File cacheFile = getCacheFile(getApplicationContext(),"testCache");//获取文件路径
            try {
                mDiskLruCache =DiskLruCache.open(cacheFile,getVersionCode(getApplicationContext()),1,10*1024*1024);
                String myCache = MD5.encodeKey("DiskCache");
                editor = mDiskLruCache.edit(myCache);
                if(editor != null){
                    return editor;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    • 写入缓存
      //写入缓存
        public boolean writeToDisk(List<ListItem>data){
            try {
                if(initDiskLruCaches()!=null){
                    OutputStream outputStream = initDiskLruCaches().newOutputStream(0);//零为索引
                    if(StreamUtil.writeToStream(outputStream,data)){
                        return true;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return false;
        }
    
    • 得到缓存
        //得到缓存
        public List<ListItem> getCacheFromDisk(){
            String myCache = MD5.encodeKey("DiskCache");
            try {
                DiskLruCache.Snapshot snapshot =mDiskLruCache.get(myCache);//Snapshot为空
                if(snapshot!=null){
                    InputStream inputStream = snapshot.getInputStream(0);//所引与写入流保持一致
                    return StreamUtil.readListStream(inputStream,ListItem.class);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
    • Demo使用
      写入缓存
        @Override
        public void showData(BaseResponse<List<ListItem>> data) {
            //更新界面
            List<ListItem>listData = data.getResults();
            //添加进内存缓存
            //MyCachedata.put("cache",listData);
            //添加进磁盘缓存
            if(writeToDisk(listData)){
                try {
                    editor.commit();
                    Toast.makeText(MainActivity.this,"添加磁盘缓存成功!",Toast.LENGTH_SHORT).show();
                } catch (IOException e) {
                    e.printStackTrace();
                }
    
            }else{
                try {
                    editor.abort();
                    Toast.makeText(MainActivity.this,"添加磁盘缓存失败!",Toast.LENGTH_SHORT).show();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            myadapter.setData(listData);
            myadapter.notifyDataSetChanged();
    
    
    
        }
    

    读取缓存

    private void initAdapter() {
            //设置排列格式
            LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
            linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    
            //读磁盘缓存
            if(getCacheFromDisk()!=null){
                data = getCacheFromDisk();
            }
            myadapter = new Myadapter(this,data);
            myadapter.notifyDataSetChanged();
            mRecyclerView.setAdapter(myadapter);
            mRecyclerView.setLayoutManager(linearLayoutManager);
    
        }
    

    最后来一个断网,读取缓存并加载的Demo图片


    cache.PNG

    需要注意得是

    我们得ListItem数据模型需要实现Serializable这个接口。

    相关文章

      网友评论

          本文标题:DiskLruCache缓存机制

          本文链接:https://www.haomeiwen.com/subject/usffwftx.html