DiskLruCache是google开源的一个本地缓存类,虽然没有成为android的库类,但是非常好用,我个人推荐使用这个类进行缓存。

当然也已经有很多人在使用这个类了,但是很多事情,我们不但要知其然,更要知其所以然,这篇文章将带大家一起解读DiskLrucache的源码,看一些大神是怎么写代码的。

先来看DiskLrucache的一些基本属性

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");//IO默认UTF-8编码private static final int IO_BUFFER_SIZE = 8 * 1024;//8K的IO缓冲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;//日志文件的writerprivate final LinkedHashMap<String, Entry> lruEntries= new LinkedHashMap<String, Entry>(0, 0.75f, true);//缓存map,关于LinkedHashMap的设置,大家可以看我另外一篇lrucache的文章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;

也许你现在还看不懂上面属性的用处,接下来我将会提到,现在我们来看构造方法

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;//同一个key可以对应多少个缓存文件this.maxSize = maxSize;//最多可以缓存多少个字节}

这是私有的构造方法,四个参数分别是 数据的缓存地址当前应用程序的版本号指定同一个key可以对应多少个缓存文件(也就是说我们会把一个文件分成多个小文件来缓存),基本都是传1,第四个参数指定 最多可以缓存多少字节的数据

然后我们看在哪里调用了构造方法,就知道类的入口了,于是找到

/*** 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 offDiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);//生成DiskLruCache缓存对象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) {cache.delete();//操作失败,删除缓存}}// create a new empty cachedirectory.mkdirs();cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);cache.rebuildJournal();return cache;}

open方法生成了一个cache对象,接下来我们要对这个cache对象的日志文件进行检查,判断是否存在,我先忽略掉已经存在的那一段,先看

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

这里创建了一个新的缓存文件夹,new了一个DiskLruCache,然后调用了它的rebuildJournal()方法来新建一个日志文件,我们来看这个方法

/*** 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("\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');}}writer.close();journalFileTmp.renameTo(journalFile);journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);}

由上述方法知,我们先写入临时文件,然后前四行分别是MAGIC(特定标志),VERSION_1(LRUCACHE版本),appVersion(app版本),valueCount(每个缓存可以分成多少个缓存文件)

然后遍历LruEntries,当然第一次我们是没有对象在里面的,那我们忽略掉这一段

最后将临时文件重命名为日志文件,在获得日志文件的writer对象。

到这里为止,DiskLrucache对象我们就成功创建了,但是还有几个疑惑,首先是map存储的Entry到底是什么,查找源码,发现是一个内部类(可想而知,这是一个bean类,每个entry对应一个缓存文件,保留有缓存文件的一些信息和操作)

 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".*///设置每一个cache文件的长度(即lengths[i]的长度)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");}}

entry的一些属性看注释,每个entry对应一个editor(代理模式),我们可以通过这个代理来操作缓存文件,来看editor

/*** 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.*///返回一个最后提交的entry的不缓存输入流,如果没有值被提交过返回nullpublic 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.*///返回最后提交的entry的文件内容,字符串形式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.*///返回一个新的无缓冲的输出流,写文件时如果潜在的输出流存在错误,这个edit将被废弃。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}.*///设置entry的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.*///commit提交编辑的结果,释放edit锁然后其它edit可以启动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.*///废弃edit,释放edit锁然后其它edit可以启动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;}}}}

从editor的构造函数可以看出,editor与entry是相互持有的。看上面的注释,我们可以看出各个方法的作用。

每次使用完editor以后,我们要commit,然后调用completeEdit函数,这个函数比较长,主要作用就是检查写入是否错误,然后写入缓存文件,更新日志,值得一提的是,完成editor操作以后,还开启了一个线程去整理缓存

另外dirty跟clean大家可能不大明白是什么意思,dirty就是没有写入数据的文件,clean就是已经写入数据的,缓存文件就算没有写入数据,我们都写入一个dirty标志,表示文件就绪

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 valueif (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);}}}//将dirty写为cleanfor (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');}if (size > maxSize || journalRebuildRequired()) {executorService.submit(cleanupCallable);//开启线程整理缓存}}

那么我使用时怎么获取editor呢

public Editor edit(String key) throws IOException {return edit(key, ANY_SEQUENCE_NUMBER);}private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER&& (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {return null; // snapshot is stale}if (entry == null) {entry = new Entry(key);lruEntries.put(key, entry);} else if (entry.currentEditor != null) {return null; // another edit is in progress}Editor editor = new Editor(entry);entry.currentEditor = editor;// flush the journal before creating files to prevent file leaksjournalWriter.write(DIRTY + ' ' + key + '\n');journalWriter.flush();return editor;}

上面代码比较简单,就不仔细说明了。
缓存写入成功以后,我们来看怎么读取,get方法返回了一个Snapshot对象

/*** 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.*///读取每个缓存文件,汇合成一个inputstreamInputStream[] 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 + '\n');if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}return new Snapshot(key, entry.sequenceNumber, ins);}

再来看缓存文件的镜像对象,我们关注它的getInputStream方法,可以发现,就是返回了缓存文件汇合输出流

/*** 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);}}}

OK,到此为止,diskLrucache的读写都说明了,接着我要说明一下dirty,clean,remove等的作用(其实就是日志文件里面标记了对缓存的操作)

DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。

DiskLruCache会在每一行CLEAN记录的最后加上该条缓存数据的大小,以字节为单位。

然后我们在看回open函数,我们就可以看到对日志的具体操作了

涉及的方法比较多,我们从readJournal()方法看起

private void readJournal() throws IOException {InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);//获取缓存文件输出流try {String magic = readAsciiLine(in);//读取缓存文件第一行(DiskLrucache缓存的标记,说明这个文件是由DiskLruCache缓存的)String version = readAsciiLine(in);//读取缓存文件第二行(DiskLrucache版本)String appVersionString = readAsciiLine(in);//读取缓存文件第三行(应用版本)String valueCountString = readAsciiLine(in);//读取缓存文件第四行(同一个key可以对应多少个缓存文件)String blank = readAsciiLine(in);//读取缓存文件第五行(其实是空)if (!MAGIC.equals(magic)|| !VERSION_1.equals(version)|| !Integer.toString(appVersion).equals(appVersionString)|| !Integer.toString(valueCount).equals(valueCountString)|| !"".equals(blank)) {throw new IOException("unexpected journal header: ["+ magic + ", " + version + ", " + valueCountString + ", " + blank + "]");}while (true) {try {readJournalLine(readAsciiLine(in));} catch (EOFException endOfJournal) {break;}}} finally {closeQuietly(in);}}

又遇到一个新的方法,readAsciiLine(),我再来看它

/*** Returns the ASCII characters up to but not including the next "\r\n", or* "\n".** @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 insteadStringBuilder result = new StringBuilder(80);while (true) {int c = in.read();if (c == -1) {throw new EOFException();} else if (c == '\n') {//只读取一行break;}result.append((char) c);}int length = result.length();if (length > 0 && result.charAt(length - 1) == '\r') {//删除换行符result.setLength(length - 1);}return result.toString();}

从上面代码可以看出,我们读取日志文件中的一行,将文件内容拼接成字符串返回

readJournal读取了前五行以后,是一个判断,用于判断该缓存文件的内容是否跟我想写入的一致,也就是说日志文件里面,前四行其实是缓存文件的一些基本信息

           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);}

我再来看readJournalLine(readAsciiLine(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);}}

注意到当parts[0]为REMOVE时,我们调用了LruEntries的remove方法。

接下来是processJournal()方法,处理日志,计算初始化cache的初始化大小和收集垃圾。Dirty entry假定不一致将会被删掉。

/*** 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();}}}

OK,一个open函数讲下来,终于把disklrucache的运行过程讲了个大概,但是disklrucache还有许多细致的函数,由于篇幅有限没有提及,这些函数都比较简单,大家可以通过查看源码了解其作用

下面贴出完整代码

/** 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.*/package com.example.android.bitmapfun.util;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.* 一个使用空间大小有边界的文件cache,每一个entry包含一个key和values。values是byte序列,按文件或者流来访问的。* 每一个value的长度在0---Integer.MAX_VALUE之间。** <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.* cache使用目录文件存储数据。文件路径必须是唯一的,可以删除和重写目录文件。多个进程同时使用同样的文件目录是不正确的* * <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.* cache限制了大小,当超出空间大小时,cache就会后台删除entry直到空间没有达到上限为止。空间大小限制不是严格的,* cache可能会暂时超过limit在等待文件删除的过程中。cache的limit不包括文件系统的头部和日志,* 所以空间大小敏感的应用应当设置一个保守的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.*调用edit()来创建或者更新entry的值,一个entry同时只能有一个editor;如果值不可被编辑就返回null。*当entry被创建时必须提供一个value。空的value应当用占位符表示。当entry被编辑的时候,必须提供value。*每次调用必须有匹配Editor commit或abort,commit是原子操作,读必须在commit前或者后,不会造成值混乱。** <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.* 调用get来读entry的快照。当get调用时读者读其值,更新或者删除不会影响先前的读** <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.* 该类可以容忍一些I/O errors。如果文件丢失啦,相应的entry就会被drop。写cache时如果error发生,edit将失败。* 调用者应当相应的处理其它问题*/
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;//8K/** This cache uses a journal file named "journal". A typical journal file* looks like this:*     libcore.io.DiskLruCache*     1           //the disk cache's version*     100         //the application's version*     2           //value count**    //state  key                            optional*     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.*     Dirty是entry被创建或者更新,每一个dirty应当被clean或remove action,如果有一行dirty没有*     匹配的clean或Remove action,就表示临时文件需要被删除。*   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.*     Clean entry已经成功的发布并且可能会被读过。一个发布行*   o READ lines track accesses for LRU.*   Read表示LRU访问*   o REMOVE lines track entries that have been deleted.*   Remove表示entry已经被删除*   * 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.* 日志文件在cache操作发生时添加,日志可能O尔删除的冗余行来压缩。一个临时的名字为journal.tmp的文件将被使用* 在压缩期间。当cache被opened的时候文件应当被删除。*/private final File directory;private final File journalFile;//日志文件private final File journalFileTmp;//日志文件临时文件private final int appVersion;//应用ersionprivate final long maxSize;//最大空间private final int valueCount;//key对应的value的个数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.* 区分老的和当前的快照,每一个实体在每次编辑被committed时都被赋予一个序列号。* 一个快照的序列号如果不等于entry的序列号那它就是废弃的。*/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.* 返回String的值,然后close*/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 "\r\n", or* "\n".** @throws java.io.EOFException if the stream is exhausted before the next newline*     character.*  读取输入流中返回的某行ASCII码字符*/public static String readAsciiLine(InputStream in) throws IOException {// TODO: support UTF-8 here insteadStringBuilder result = new StringBuilder(80);while (true) {int c = in.read();if (c == -1) {throw new EOFException();} else if (c == '\n') {break;}result.append((char) c);}int length = result.length();if (length > 0 && result.charAt(length - 1) == '\r') {result.setLength(length - 1);}return result.toString();}/*** Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.* closeable关闭*/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}.* 递归删除dir*/// TODO: this should specify paths as Strings rather than as Filespublic 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.*  后台单线程回收entry  */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();//回收到满足maxsizeif (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.* 创建cache* * @param directory a writable directory* @param appVersion* @param valueCount the number of values per cache entry. Must be positive.* 每一个key相对应的value的数目* @param maxSize the maximum number of bytes this cache should use to store* @throws IOException if reading or writing the cache directory fails*/public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)throws IOException {if (maxSize <= 0) {//maxsize必须大于0throw new IllegalArgumentException("maxSize <= 0");}if (valueCount <= 0) {//valuecount也必须大于0throw new IllegalArgumentException("valueCount <= 0");}// prefer to pick up where we left off优先处理先前的cacheDiskLruCache 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创建一个空新的cachedirectory.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.* 处理日志* 计算初始化cache的初始化大小和收集垃圾。Dirty entry假定不一致将会被删掉。*/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("\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');}}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.* 返回key对应的entry的snapshot,当key相应的entry不存在或者当前不可读时返回null。* 如果返回相应的值,它就会被移动到LRU队列的头部。*/public synchronized Snapshot get(String key) throws IOException {checkNotClosed();//检查cache是否已关闭validateKey(key);//验证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 + '\n');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);}//有key和序列号生成一个editorprivate synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {checkNotClosed();//检查cache关闭与否validateKey(key);//验证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 leaksjournalWriter.write(DIRTY + ' ' + key + '\n');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;}//完成Edit动作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 valueif (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() + '\n');if (success) {entry.sequenceNumber = nextSequenceNumber++;}} else {lruEntries.remove(entry.key);journalWriter.write(REMOVE + ' ' + entry.key + '\n');}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.* 当日志大小减半并且删掉至少2000项时重新构造日志*/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.* 删除key相应的entry,被编辑的Entry不能被remove* @return true if an entry was removed.*/public synchronized boolean remove(String key) throws IOException {checkNotClosed();//检查cache是否已经关闭validateKey(key);//验证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 + '\n');lruEntries.remove(key);if (journalRebuildRequired()) {executorService.submit(cleanupCallable);}return true;}/*** Returns true if this cache has been closed.* 判断cache是否已经关闭*/public boolean isClosed() {return journalWriter == null;}//检查cache是否已经关闭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();//检查cache是否关闭trimToSize();//满足最大空间limitjournalWriter.flush();}/*** Closes this cache. Stored values will remain on the filesystem.* 关闭cache。*/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;}//回收删除某些entry到空间大小满足maxsizeprivate 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.* 关闭删除cache*/public void delete() throws IOException {close();deleteContents(directory);}//验证key格式的正确性private void validateKey(String key) {if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {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.* entry的快照*/public final class Snapshot implements Closeable {private final String key;//keyprivate 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.* 返回entry快照的editor,如果entry已经更新了或者另一个edit正在处理过程中返回null。*/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.* 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.* 返回一个最后提交的entry的不缓存输入流,如果没有值被提交过返回null*/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.* 返回最后提交的entry的文件内容,字符串形式*/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.* 返回一个新的无缓冲的输出流,写文件时如果潜在的输出流存在错误,这个edit将被废弃。*/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}.* 设置entry的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.* commit提交编辑的结果,释放edit锁然后其它edit可以启动*/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.* 废弃edit,释放edit锁然后其它edit可以启动*/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;}}}}/** Entry 最终类*/private final class Entry {private final String key;/** Lengths of this entry's files. */private final long[] lengths;//每一个cache文件的长度/** 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".* 设置每一个cache文件的长度(即lengths[i]的长度)*/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源码解析相关推荐

  1. Android之DiskLruCache源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

  2. DiskLruCache 源码解析

    DiskLruCache 描述: DiskLruCache 是用来缓存一些数据,比如网络访问的Json,加载的图片等等. LruCache 是把数据缓存到内存中,而DisLruCache 是把数据缓存 ...

  3. 彻底理解OkHttp - OkHttp 源码解析及OkHttp的设计思想

    OkHttp 现在统治了Android的网络请求领域,最常用的框架是:Retrofit+okhttp.OkHttp的实现原理和设计思想是必须要了解的,读懂和理解流行的框架也是程序员进阶的必经之路,代码 ...

  4. Android经典著名的百大框架源码解析(retrofit、Okhttp、Glide、Zxing、dagger等等)

    我们Android程序员每天都要和源码打交道.经过数年的学习,大多数程序员可以"写"代码,或者至少是拷贝并修改代码.而且,我们教授编程的方式强调编写代码的艺术,而不是如何阅读代码. ...

  5. Glide 4.9源码解析-缓存策略

    本文Glide源码基于4.9,版本下载地址如下:Glide 4.9 前言 在分析了Glide的图片加载流程后,更加发觉到Glide的强大,于是这篇文章将继续深入分析Glide的缓存策略.不过今天的文章 ...

  6. BAT高级架构师合力熬夜15天,肝出了这份PDF版《Android百大框架源码解析》,还不快快码住。。。

    前言 为什么要阅读源码? 现在中高级Android岗位面试中,对于各种框架的源码都会刨根问底,从而来判断应试者的业务能力边际所在.但是很多开发者习惯直接搬运,对各种框架的源码都没有过深入研究,在面试时 ...

  7. OkHttp 源码解析(4.9.1 版本)

    文章目录 1.OkHttp 简介 2.OkHttp 配置与基本用法 2.1 依赖引入与配置 2.2 基本用法 3.OkHttp 常见对象介绍 4.OkHttp 源码解析 4.1 当我们调用`okhtt ...

  8. Android 图片加载框架Gilde源码解析

    1.使用Gilde显示一张图片 Glide.with(this).load("https://cn.bing.com/sa/simg/hpb/xxx.jpg").into(imag ...

  9. Glide的源码解析(一)(附方法调用图)

    前言 上一篇博客讲了Glide的基本使用,知其然,也要知其所以然,这篇博客一起来学习Glide的源码.如果不知道Glide的基本使用,可以看上一篇博客:http://blog.csdn.net/luo ...

最新文章

  1. Armv8-A 架构概述
  2. [云炬创业学笔记]第二章决定成为创业者测试16
  3. 07-11 Linux命令操作
  4. 怎么修改ppt的虚线间隔_还有一小时下班,领导交给我一份ppt,做不完不许走!...
  5. xamarin_如何实现声明性Xamarin表单验证
  6. java gui 单选_JavaGUI――swing组件基础(三)JCheckBox/JRadioButton/ButtonGroup
  7. excel重复上一步快捷键_13个excel快捷键,让你的1秒钟,抵得上同事的5分钟
  8. vue3 el-form表单验证 自定义校验
  9. 如何在 Ubuntu 上安装 MongoDB
  10. 商品预约抢购实践及redis性能测试
  11. 《近匠》专访机智云 CTO 刘琰——从 0 到 1 开启智能化硬件开发
  12. Simulink仿真入门到精通(八) M语言对Simulink模型的自动化操作及配置
  13. win10忘记开机密码
  14. 服务器系统能装cad吗,服务器主机用CAD画图吗
  15. 115、神州数码交换机配置基本命令
  16. 环信即时通信聊天理解
  17. “我爱淘”冲刺阶段Scrum站立会议8
  18. 使用conda安装pytorch时出现问题CondaSSLError: OpenSSL appears to be unavailable on this machine.
  19. SublimeText3和EverEdit 的一些使用感受
  20. 大雨瓢泼!多地告急!告诉你雨天行车的全部秘密。

热门文章

  1. 11月编程排行榜来了,Python依旧占据榜首
  2. 使用FTP协议的服务,包括匿名用户验证访问,用户验证访问,虚拟用户验证访问等
  3. 实验7 Oracle数据库安全管理
  4. 广播风暴原因可能及排除方法
  5. 市面上有哪几种门_卧室门怎么选?市面上5种常见房门大揭秘
  6. 如何在 FoxyProxy 中设置 YiLu Proxy
  7. 华为为什么认可这四所大学?(附微电子高校排行榜)
  8. 计算机专业需要用独显吗,集显 or 独显 我的程序用哪个必须由我定
  9. ZZNUOJ_C语言1123:最佳校友(附完整源码)
  10. E-CATT 录入测试数据的方法