OKHttp源码解析(七)--中阶之缓存机制
- 1.OkHttp源码解析(一):OKHttp初阶
- 2 OkHttp源码解析(二):OkHttp连接的"前戏"——HTTP的那些事
- 3 OkHttp源码解析(三):OKHttp中阶之线程池和消息队列
- 4 OkHttp源码解析(四):OKHttp中阶之拦截器及调用链
- 5 OkHttp源码解析(五):OKHttp中阶之OKio简介
- 6 OkHttp源码解析(六):OKHttp中阶之缓存基础
- 7 OkHttp源码解析(七):OKHttp中阶之缓存机制
- 8 OkHttp源码解析(八):OKHttp中阶之连接与请求值前奏
- 9 OkHttp源码解析(九):OKHTTP连接中三个"核心"RealConnection、ConnectionPool、StreamAllocation
- 10 OkHttp源码解析(十) OKHTTP中连接与请求
- 11 OkHttp的感谢
上一章主要讲解了HTTP中的缓存以及OKHTTP中的缓存,今天我们主要讲解OKHTTP中缓存体系的精髓---DiskLruCache,由于篇幅限制,今天内容看似不多,大概分为两个部分
1.DiskLruCache内部类详解
2.DiskLruCache类详解
3.OKHTTP的缓存的实现---CacheInterceptor的具体执行流程
一、DiskLruCache
在看DiskLruCache前先看下他的几个内部类
1、Entry.class(DiskLruCache的内部类)
Entry内部类是实际用于存储的缓存数据的实体类,每一个url对应一个Entry实体
private final class Entry {
final String key;
/** 实体对应的缓存文件 */
/** Lengths of this entry's files. */
final long[] lengths; //文件比特数
final File[] cleanFiles;
final File[] dirtyFiles;
/** 实体是否可读,可读为true,不可读为false*/
/** True if this entry has ever been published. */
boolean readable;
/** 编辑器,如果实体没有被编辑过,则为null*/
/** The ongoing edit or null if this entry is not being edited. */
Editor currentEditor;
/** 最近提交的Entry的序列号 */
/** The sequence number of the most recently committed edit to this entry. */
long sequenceNumber;
//构造器 就一个入参 key,而key又是url,所以,一个url对应一个Entry
Entry(String key) {
this.key = key;
//valueCount在构造DiskLruCache时传入的参数默认大小为2
//具体请看Cache类的构造函数,里面通过DiskLruCache.create()方法创建了DiskLruCache,并且传入一个值为2的ENTRY_COUNT常量
lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];
// The names are repetitive so re-use the same builder to avoid allocations.
StringBuilder fileBuilder = new StringBuilder(key).append('.');
int truncateTo = fileBuilder.length();
//由于valueCount为2,所以循环了2次,一共创建了4份文件
//分别为key.1文件和key.1.tmp文件
// key.2文件和key.2.tmp文件
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}
通过上述代码咱们知道了,一个url对应一个Entry对象,同时,每个Entry对应两个文件,key.1存储的是Response的headers,key.2文件存储的是Response的body
2、Snapshot (DiskLruCache的内部类)
/** A snapshot of the values for an entry. */
public final class Snapshot implements Closeable {
private final String key; //也有一个key
private final long sequenceNumber; //序列号
private final Source[] sources; //可以读入数据的流 这么多的流主要是从cleanFile中读取数据
private final long[] lengths; //与上面的流一一对应
//构造器就是对上面这些属性进行赋值
Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
this.sources = sources;
this.lengths = lengths;
}
public String key() {
return key;
}
//edit方法主要就是调用DiskLruCache的edit方法了,入参是该Snapshot对象的两个属性key和sequenceNumber.
/**
* 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 Source getSource(int index) {
return sources[index];
}
/** Returns the byte length of the value for {@code index}. */
public long getLength(int index) {
return lengths[index];
}
public void close() {
for (Source in : sources) {
Util.closeQuietly(in);
}
}
}
这时候再回来看下Entry里面的snapshot()方法
/**
* Returns a snapshot of this entry. This opens 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.
*/
Snapshot snapshot() {
//首先判断 线程是否有DiskLruCache对象的锁
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
//new了一个Souce类型数组,容量为2
Source[] sources = new Source[valueCount];
//clone一个long类型的数组,容量为2
long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
//获取cleanFile的Source,用于读取cleanFile中的数据,并用得到的souce、Entry.key、Entry.length、sequenceNumber数据构造一个Snapshot对象
try {
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
return new Snapshot(key, sequenceNumber, sources, lengths);
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (sources[i] != null) {
Util.closeQuietly(sources[i]);
} else {
break;
}
}
// Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
// size.)
try {
removeEntry(this);
} catch (IOException ignored) {
}
return null;
}
}
由上面代码可知Spapshot里面的key,sequenceNumber,sources,lenths都是一个entry,其实也就可以说一个Entry对象一一对应一个Snapshot对象
3、Editor.class(DiskLruCache的内部类)
Editro类的属性和构造器貌似看不到什么东西,不过通过构造器,我们知道,在构造一个Editor的时候必须传入一个Entry,莫非Editor是对这个Entry操作类。
/** Edits the values for an entry. */
public final class Editor {
final Entry entry;
final boolean[] written;
private boolean done;
Editor(Entry entry) {
this.entry = entry;
this.written = (entry.readable) ? null : new boolean[valueCount];
}
/**
* Prevents this editor from completing normally. This is necessary either when the edit causes
* an I/O error, or if the target entry is evicted while this editor is active. In either case
* we delete the editor's created files and prevent new files from being created. Note that once
* an editor has been detached it is possible for another editor to edit the entry.
*这里说一下detach方法,当编辑器(Editor)处于io操作的error的时候,或者editor正在被调用的时候而被清
*除的,为了防止编辑器可以正常的完成。我们需要删除编辑器创建的文件,并防止创建新的文件。如果编
*辑器被分离,其他的编辑器可以编辑这个Entry
*/
void detach() {
if (entry.currentEditor == this) {
for (int i = 0; i < valueCount; i++) {
try {
fileSystem.delete(entry.dirtyFiles[i]);
} catch (IOException e) {
// This file is potentially leaked. Not much we can do about that.
}
}
entry.currentEditor = null;
}
}
/**
* Returns an unbuffered input stream to read the last committed value, or null if no value has
* been committed.
* 获取cleanFile的输入流 在commit的时候把done设为true
*/
public Source newSource(int index) {
synchronized (DiskLruCache.this) {
//如果已经commit了,不能读取了
if (done) {
throw new IllegalStateException();
}
//如果entry不可读,并且已经有编辑器了(其实就是dirty)
if (!entry.readable || entry.currentEditor != this) {
return null;
}
try {
//通过filesystem获取cleanFile的输入流
return fileSystem.source(entry.cleanFiles[index]);
} catch (FileNotFoundException e) {
return 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.
* 获取dirty文件的输出流,如果在写入数据的时候出现错误,会立即停止。返回的输出流不会抛IO异常
*/
public Sink newSink(int index) {
synchronized (DiskLruCache.this) {
//已经提交,不能操作
if (done) {
throw new IllegalStateException();
}
//如果编辑器是不自己的,不能操作
if (entry.currentEditor != this) {
return Okio.blackhole();
}
//如果entry不可读,把对应的written设为true
if (!entry.readable) {
written[index] = true;
}
//如果文件
File dirtyFile = entry.dirtyFiles[index];
Sink sink;
try {
//如果fileSystem获取文件的输出流
sink = fileSystem.sink(dirtyFile);
} catch (FileNotFoundException e) {
return Okio.blackhole();
}
return new FaultHidingSink(sink) {
@Override protected void onException(IOException e) {
synchronized (DiskLruCache.this) {
detach();
}
}
};
}
}
/**
* 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操作对数据进行提交,我们要把dirtyFiles里面的内容移动到cleanFiles里才能够让别的editor访问到
*/
public void commit() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
completeEdit(this, true);
}
done = true;
}
}
/**
* Aborts this edit. This releases the edit lock so another edit may be started on the same
* key.
*/
public void abort() throws IOException {
synchronized (DiskLruCache.this) {
if (done) {
throw new IllegalStateException();
}
if (entry.currentEditor == this) {
//这个方法是DiskLruCache的方法在后面讲解
completeEdit(this, false);
}
done = true;
}
}
public void abortUnlessCommitted() {
synchronized (DiskLruCache.this) {
if (!done && entry.currentEditor == this) {
try {
completeEdit(this, false);
} catch (IOException ignored) {
}
}
}
}
}
哎,看到这个了类的注释,发现Editor的确就是编辑entry类的。
Editor里面的几个方法Source newSource(int index) ,Sink newSink(int index),commit(),abort(),abortUnlessCommitted() ,既然是编辑器,我们看到上面的方法应该可以猜到,上面的方法一次对应如下
方法 | 意义 |
---|---|
Source newSource(int index) | 返回指定index的cleanFile的读入流 |
Sink newSink(int index) | 向指定index的dirtyFiles文件写入数据 |
commit() | 这里执行的工作是提交数据,并释放锁,最后通知DiskLruCache刷新相关数据 |
abort() | 终止编辑,并释放锁 |
abortUnlessCommitted() | 除非正在编辑,否则终止 |
abort()和abortUnlessCommitted()最后都会执行completeEdit(Editor, boolean) 这个方法这里简单说下:
success情况提交:dirty文件会被更名为clean文件,entry.lengths[i]值会被更新,DiskLruCache,size会更新(DiskLruCache,size代表的是所有整个缓存文件加起来的总大小),redundantOpCount++,在日志中写入一条Clean信息
failed情况:dirty文件被删除,redundantOpCount++,日志中写入一条REMOVE信息
至此DiskLruCache的内部类就全部介绍结束了。现在咱们正式关注下DiskLruCache类
二、DiskLruCache类详解
(一)、重要属性
DiskLruCache里面有一个属性是lruEntries如下:
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
/** Used to run 'cleanupRunnable' for journal rebuilds. */
private final Executor executor;
LinkedHashMap自带Lru算法的光环属性,详情请看LinkedHashMap源码说明
DiskLruCache也有一个线程池属性 executor,不过该池最多有一个线程工作,用于清理,维护缓存数据。创建一个DiskLruCache对象的方法是调用该方法,而不是直接调用构造器。
(二)、构造函数和创建对象
DiskLruCache有一个构造函数,但是不是public的所以DiskLruCache只能被包内中类调用,不能在外面直接new。不过DiskLruCache提供了一个静态方法create,对外提供DiskLruCache对象
//DiskLruCache.java
/**
* Create a cache which will reside in {@code directory}. This cache is lazily initialized on
* first access and will be created if it does not exist.
*
* @param directory a writable directory
* @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
*/
public static DiskLruCache create(FileSystem fileSystem, File directory, int appVersion,
int valueCount, long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
//这个executor其实就是DiskLruCache里面的executor
// Use a single background thread to evict entries.
Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
return new DiskLruCache(fileSystem, directory, appVersion, valueCount, maxSize, executor);
}
static final String JOURNAL_FILE = "journal";
static final String JOURNAL_FILE_TEMP = "journal.tmp";
static final String JOURNAL_FILE_BACKUP = "journal.bkp"
DiskLruCache(FileSystem fileSystem, File directory, int appVersion, int valueCount, long maxSize,
Executor executor) {
this.fileSystem = fileSystem;
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;
this.executor = executor;
}
该构造器会在制定的目录下创建三份文件,这三个文件是DiskLruCache的工作日志文件。在执行DiskLruCache的任何方法之前都会执行initialize()方法来完成DiskLruCache的初始化,有人会想为什么不在DiskLruCache的构造器中完成对该方法的调用,其实是为了延迟初始化,因为初始化会创建一系列的文件和对象,所以做了延迟初始化。
(三)、初始化
那么来看下initialize里面的代码
public synchronized void initialize() throws IOException {
//断言,当持有自己锁的时候。继续执行,没有持有锁,直接抛异常
assert Thread.holdsLock(this);
//如果已经初始化过,则不需要再初始化,直接rerturn
if (initialized) {
return; // Already initialized.
}
// If a bkp file exists, use it instead.
//如果有journalFileBackup文件
if (fileSystem.exists(journalFileBackup)) {
// If journal file also exists just delete backup file.
//如果有journalFile文件
if (fileSystem.exists(journalFile)) {
//有journalFile文件 则删除journalFileBackup文件
fileSystem.delete(journalFileBackup);
} else {
//没有journalFile,则将journalFileBackUp更名为journalFile
fileSystem.rename(journalFileBackup, journalFile);
}
}
// Prefer to pick up where we left off.
if (fileSystem.exists(journalFile)) {
//如果有journalFile文件,则对该文件,则分别调用readJournal()方法和processJournal()方法
try {
readJournal();
processJournal();
//设置初始化过标志
initialized = true;
return;
} catch (IOException journalIsCorrupt) {
Platform.get().log(WARN, "DiskLruCache " + directory + " is corrupt: "
+ journalIsCorrupt.getMessage() + ", removing", journalIsCorrupt);
}
// The cache is corrupted, attempt to delete the contents of the directory. This can throw and
// we'll let that propagate out as it likely means there is a severe filesystem problem.
try {
//如果没有journalFile则删除
delete();
} finally {
closed = false;
}
}
//重新建立journal文件
rebuildJournal();
initialized = true;
}
大家发现没有,如论是否有journal文件,最后都会将initialized设为true,该值不会再被设置为false,除非DiskLruCache对象呗销毁。这表明initialize()放啊在DiskLruCache对象的整个生命周期中只会执行一次。该动作完成日志的写入和lruEntries集合的初始化。
这里面分别调用了readJournal()方法和processJournal()方法,那咱们依次分析下这两个方法,这里面有大量的okio里面的代码,如果大家对okio不熟悉能读上一篇文章。
private void readJournal() throws IOException {
//获取journalFile的source即输入流
BufferedSource source = Okio.buffer(fileSystem.source(journalFile));
try {
//读取相关数据
String magic = source.readUtf8LineStrict();
String version = source.readUtf8LineStrict();
String appVersionString = source.readUtf8LineStrict();
String valueCountString = source.readUtf8LineStrict();
String blank = source.readUtf8LineStrict();
//做校验
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(source.readUtf8LineStrict());
lineCount++;
} catch (EOFException endOfJournal) {
break;
}
}
//读取出来的行数减去lruEntriest的集合的差值,即日志多出的"冗余"记录
redundantOpCount = lineCount - lruEntries.size();
// If we ended on a truncated line, rebuild the journal before appending to it.
//source.exhausted()表示是否还多余字节,如果没有多余字节,返回true,有多月字节返回false
if (!source.exhausted()) {
//如果有多余字节,则重新构建下journal文件,主要是写入头文件,以便下次读的时候,根据头文件进行校验
rebuildJournal();
} else {
//获取这个文件的Sink
journalWriter = newJournalWriter();
}
} finally {
Util.closeQuietly(source);
}
}
这里说一下ource.readUtf8LineStrict()方法,这个方法是BufferedSource接口的方法,具体实现是RealBufferedSource,所以大家要去RealBufferedSource里面去找具体实现。我这里简单说下,就是从source里面按照utf-8编码取出一行的数据。这里面读取了magic,version,appVersionString,valueCountString,blank,然后进行校验,这个数据是在"写"的时候,写入的,具体情况看DiskLruCache的rebuildJournal()方法。随后记录redundantOpCount的值,该值的含义就是判断当前日志中记录的行数和lruEntries集合容量的差值,即日志中多出来的"冗余"记录。
读取的时候又调用了readJournalLine()方法,咱们来研究下这个方法
private void readJournalLine(String line) throws IOException {
获取空串的position,表示头
int firstSpace = line.indexOf(' ');
//空串的校验
if (firstSpace == -1) {
throw new IOException("unexpected journal line: " + line);
}
//第一个字符的位置
int keyBegin = firstSpace + 1;
// 方法返回第一个空字符在此字符串中第一次出现,在指定的索引即keyBegin开始搜索,所以secondSpace是爱这个字符串中的空字符(不包括这一行最左侧的那个空字符)
int secondSpace = line.indexOf(' ', keyBegin);
final String key;
//如果没有中间的空字符
if (secondSpace == -1) {
//截取剩下的全部字符串构成key
key = line.substring(keyBegin);
if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
//如果解析的是REMOVE信息,则在lruEntries里面删除这个key
lruEntries.remove(key);
return;
}
} else {
//如果含有中间间隔的空字符,则截取这个中间间隔到左侧空字符之间的字符串,构成key
key = line.substring(keyBegin, secondSpace);
}
//获取key后,根据key取出Entry对象
Entry entry = lruEntries.get(key);
//如果Entry为null,则表明内存中没有,则new一个,并把它放到内存中。
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//如果是CLEAN开头
if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
//line.substring(secondSpace + 1) 为获取中间空格后面的内容,然后按照空字符分割,设置entry的属性,表明是干净的数据,不能编辑。
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)) {
//如果是以DIRTY开头,则设置一个新的Editor,表明可编辑
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);
}
}
这里面主要是具体的解析,如果每次解析的是非REMOVE信息,利用该key创建一个entry,如果是判断信息是CLEAN则设置ENTRY为可读,并设置entry.currentEditor表明当前Entry不可编辑,调用entry.setLengths(String[]),设置该entry.lengths的初始值。如果判断是Dirty则设置enry.currentEdtor=new Editor(entry);表明当前Entry处于被编辑状态。
通过上面我得到了如下的结论:
- 1、如果是CLEAN的话,对这个entry的文件长度进行更新
- 2、如果是DIRTY,说明这个值正在被操作,还没有commit,于是给entry分配一个Editor。
- 3、如果是READ,说明这个值被读过了,什么也不做。
看下journal文件你就知道了
1 * libcore.io.DiskLruCache
2 * 1
3 * 100
4 * 2
5 *
6 * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
7 * DIRTY 335c4c6028171cfddfbaae1a9c313c52
8 * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
9 * REMOVE 335c4c6028171cfddfbaae1a9c313c52
10 * DIRTY 1ab96a171faeeee38496d8b330771a7a
11 * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
12 * READ 335c4c6028171cfddfbaae1a9c313c52
13 * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
然后又调用了processJournal()方法,那我们来看下:
/**
* Computes the initial size and collects garbage as a part of opening the cache. Dirty entries
* are assumed to be inconsistent and will be deleted.
*/
private void processJournal() throws IOException {
fileSystem.delete(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++) {
fileSystem.delete(entry.cleanFiles[t]);
fileSystem.delete(entry.dirtyFiles[t]);
}
i.remove();
}
}
}
先是删除了journalFileTmp文件
然后调用for循环获取链表中的所有Entry,如果Entry的中Editor!=null,则表明Entry数据时脏的DIRTY,所以不能读,进而删除Entry下的缓存文件,并且将Entry从lruEntries中移除。如果Entry的Editor==null,则证明该Entry下的缓存文件可用,记录它所有缓存文件的缓存数量,结果赋值给size。
readJournal()方法里面调用了rebuildJournal(),initialize()方法同样会readJourna,但是这里说明下:readJournal里面调用的rebuildJournal()是有条件限制的,initialize()是一定会调用的。那我们来研究下readJournal()
/**
* Creates a new journal that omits redundant information. This replaces the current journal if it
* exists.
*/
synchronized void rebuildJournal() throws IOException {
//如果写入流不为空
if (journalWriter != null) {
//关闭写入流
journalWriter.close();
}
//通过okio获取一个写入BufferedSinke
BufferedSink writer = Okio.buffer(fileSystem.sink(journalFileTmp));
try {
//写入相关信息和读取向对应,这时候大家想下readJournal
writer.writeUtf8(MAGIC).writeByte('\n');
writer.writeUtf8(VERSION_1).writeByte('\n');
writer.writeDecimalLong(appVersion).writeByte('\n');
writer.writeDecimalLong(valueCount).writeByte('\n');
writer.writeByte('\n');
//遍历lruEntries里面的值
for (Entry entry : lruEntries.values()) {
//如果editor不为null,则为DIRTY数据
if (entry.currentEditor != null) {
在开头写上 DIRTY,然后写上 空字符
writer.writeUtf8(DIRTY).writeByte(' ');
//把entry的key写上
writer.writeUtf8(entry.key);
//换行
writer.writeByte('\n');
} else {
//如果editor为null,则为CLEAN数据, 在开头写上 CLEAN,然后写上 空字符
writer.writeUtf8(CLEAN).writeByte(' ');
//把entry的key写上
writer.writeUtf8(entry.key);
//结尾接上两个十进制的数字,表示长度
entry.writeLengths(writer);
//换行
writer.writeByte('\n');
}
}
} finally {
//最后关闭写入流
writer.close();
}
//如果存在journalFile
if (fileSystem.exists(journalFile)) {
//把journalFile文件重命名为journalFileBackup
fileSystem.rename(journalFile, journalFileBackup);
}
然后又把临时文件,重命名为journalFile
fileSystem.rename(journalFileTmp, journalFile);
//删除备份文件
fileSystem.delete(journalFileBackup);
//拼接一个新的写入流
journalWriter = newJournalWriter();
//设置没有error标志
hasJournalErrors = false;
//设置最近重新创建journal文件成功
mostRecentRebuildFailed = false;
}
总结下:
获取一个写入流,将lruEntries集合中的Entry对象写入tmp文件中,根据Entry的currentEditor的值判断是CLEAN还是DIRTY,写入该Entry的key,如果是CLEAN还要写入文件的大小bytes。然后就是把journalFileTmp更名为journalFile,然后将journalWriter跟文件绑定,通过它来向journalWrite写入数据,最后设置一些属性。
我们可以砍到,rebuild操作是以lruEntries为准,把DIRTY和CLEAN的操作都写回到journal中。但发现没有,其实没有改动真正的value,只不过重写了一些事务的记录。事实上,lruEntries和journal文件共同确定了cache数据的有效性。lruEntries是索引,journal是归档。至此序列化部分就已经结束了
(四)、关于Cache类调用的几个方法
上回书说道Cache调用DiskCache的几个方法,如下:
- 1.DiskLruCache.get(String)获取DiskLruCache.Snapshot
- 2.DiskLruCache.remove(String)移除请求
- 3.DiskLruCache.edit(String);获得一个DiskLruCache.Editor对象,
- 4.DiskLruCache.Editor.newSink(int);获得一个sink流 (具体看Editor类)
- 5.DiskLruCache.Snapshot.getSource(int);获取一个Source对象。 (具体看Editor类)
- 6.DiskLruCache.Snapshot.edit();获得一个DiskLruCache.Editor对象,
1、DiskLruCache.Snapshot get(String)方法
public synchronized Snapshot get(String key) throws IOException {
//初始化
initialize();
//检查缓存是否已经关闭
checkNotClosed();
//检验key
validateKey(key);
//如果以上都通过,先获取内存中的数据,即根据key在linkedList查找
Entry entry = lruEntries.get(key);
//如果没有值,或者有值,但是值不可读
if (entry == null || !entry.readable) return null;
//获取entry里面的snapshot的值
Snapshot snapshot = entry.snapshot();
//如果有snapshot为null,则直接返回null
if (snapshot == null) return null;
//如果snapshot不为null
//计数器自加1
redundantOpCount++;
//把这个内容写入文档中
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
//如果超过上限
if (journalRebuildRequired()) {
//开始清理
executor.execute(cleanupRunnable);
}
//返回数据
return snapshot;
}
/**
* We only rebuild the journal when it will halve the size of the journal and eliminate at least
* 2000 ops.
*/
boolean journalRebuildRequired() {
//最大计数单位
final int redundantOpCompactThreshold = 2000;
//清理的条件
return redundantOpCount >= redundantOpCompactThreshold
&& redundantOpCount >= lruEntries.size();
}
主要就是先去拿snapshot,然后会用journalWriter向journal写入一条read记录,最后判断是否需要清理。
清理的条件是当前redundantOpCount大于2000,并且redundantOpCount的值大于linkedList里面的size。咱们接着看下清理任务
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
//如果没有初始化或者已经关闭了,则不需要清理
if (!initialized | closed) {
return; // Nothing to do
}
try {
trimToSize();
} catch (IOException ignored) {
//如果抛异常了,设置最近的一次清理失败
mostRecentTrimFailed = true;
}
try {
//如果需要清理了
if (journalRebuildRequired()) {
//重新创建journal文件
rebuildJournal();
//计数器归于0
redundantOpCount = 0;
}
} catch (IOException e) {
//如果抛异常了,设置最近的一次构建失败
mostRecentRebuildFailed = true;
journalWriter = Okio.buffer(Okio.blackhole());
}
}
}
};
void trimToSize() throws IOException {
//如果超过上限
while (size > maxSize) {
//取出一个Entry
Entry toEvict = lruEntries.values().iterator().next();
//删除这个Entry
removeEntry(toEvict);
}
mostRecentTrimFailed = false;
}
boolean removeEntry(Entry entry) throws IOException {
if (entry.currentEditor != null) {
//让这个editor正常的结束
entry.currentEditor.detach(); // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
//删除entry对应的clean文件
fileSystem.delete(entry.cleanFiles[i]);
//缓存大小减去entry的小小
size -= entry.lengths[i];
//设置entry的缓存为0
entry.lengths[i] = 0;
}
//计数器自加1
redundantOpCount++;
//在journalWriter添加一条删除记录
journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
//linkedList删除这个entry
lruEntries.remove(entry.key);
//如果需要重新构建
if (journalRebuildRequired()) {
//开启清理任务
executor.execute(cleanupRunnable);
}
return true;
}
看下cleanupRunnable对象,看他的run方法得知,主要是调用了trimToSize()和rebuildJournal()两个方法对缓存数据进行维护。rebuildJournal()前面已经说过了,这里主要关注下trimToSize()方法,trimToSize()方法主要是遍历lruEntries(注意:这个遍历科室通过accessOrder来的,也就是隐含了LRU这个算法),来一个一个移除entry直到size小于maxSize,而removeEntry操作就是讲editor里的diryFile以及cleanFiles进行删除就是,并且要向journal文件里写入REMOVE操作,以及删除lruEntrie里面的对象。
cleanup主要是用来调整整个cache的大小,以防止它过大,同时也能用来rebuildJournal,如果trim或者rebuild不成功,那之前edit里面也是没有办法获取Editor来进行数据修改操作的。
下面来看下boolean remove(String key)方法
/**
* Drops the entry for {@code key} if it exists and can be removed. If the entry for {@code key}
* is currently being edited, that edit will complete normally but its value will not be stored.
*根据key来删除对应的entry,如果entry存在则将会被删除,如果这个entry正在被编辑,编辑将被正常结束,但是编辑的内容不会保存
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
//初始化
initialize();
//检查是否被关闭
checkNotClosed();
//key是否符合要求
validateKey(key);
//根据key来获取Entry
Entry entry = lruEntries.get(key);
//如果entry,返回false表示删除失败
if (entry == null) return false;
//然后删除这个entry
boolean removed = removeEntry(entry);
//如果删除成功且缓存大小小于最大值,则设置最近清理标志位
if (removed && size <= maxSize) mostRecentTrimFailed = false;
return removed;
}
这这部分很简单,就是先做判断,然后通过key获取Entry,然后删除entry
那我们继续,来看下DiskLruCache.edit(String);方法
/**
* Returns an editor for the entry named {@code key}, or null if another edit is in progress.
* 返回一entry的编辑器,如果其他正在编辑,则返回null
* 我的理解是根据key找entry,然后根据entry找他的编辑器
*/
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//初始化
initialize();
//流关闭检测
checkNotClosed();
//检测key
validateKey(key);
//根据key找到Entry
Entry entry = lruEntries.get(key);
//如果快照是旧的
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
//如果 entry.currentEditor != null 表明正在编辑,是DIRTY
if (entry != null && entry.currentEditor != null) {
return null; // Another edit is in progress.
}
//如果最近清理失败,或者最近重新构建失败,我们需要开始清理任务
//我大概翻译下注释:操作系统已经成为我们的敌人,如果清理任务失败,它意味着我们存储了过多的数据,因此我们允许超过这个限制,所以不建议编辑。如果构建日志失败,writer这个写入流就会无效,所以文件无法及时更新,导致我们无法继续编辑,会引起文件泄露。如果满足以上两种情况,我们必须进行清理,摆脱这种不好的状态。
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
// The OS has become our enemy! If the trim job failed, it means we are storing more data than
// requested by the user. Do not allow edits so we do not go over that limit any further. If
// the journal rebuild failed, the journal writer will not be active, meaning we will not be
// able to record the edit, causing file leaks. In both cases, we want to retry the clean up
// so we can get out of this state!
//开启清理任务
executor.execute(cleanupRunnable);
return null;
}
// Flush the journal before creating files to prevent file leaks.
//写入DIRTY
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
//如果journal有错误,表示不能编辑,返回null
if (hasJournalErrors) {
return null; // Don't edit; the journal can't be written.
}
//如果entry==null,则new一个,并放入lruEntries
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
//根据entry 构造一个Editor
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
上面代码注释说的很清楚,这里就提几个注意事项
注意事项:
(1)如果已经有个别的editor在操作这个entry了,那就返回null
(2)无时无刻不在进行cleanup判断进行cleanup操作
(3)会把当前的key在journal文件标记为dirty状态,表示这条记录正在被编辑
(4)如果没有entry,会new一个出来
这个方法已经结束了,那我们来看下 在Editor内部类commit()方法里面调用的completeEdit(Editor,success)方法
synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
//如果entry的编辑器不是editor则抛异常
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// If this edit is creating the entry for the first time, every index must have a value.
//如果successs是true,且entry不可读表明 是第一次写回,必须保证每个index里面要有数据,这是为了保证完整性
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 (!fileSystem.exists(entry.dirtyFiles[i])) {
editor.abort();
return;
}
}
}
//遍历entry下的所有文件
for (int i = 0; i < valueCount; i++) {
File dirty = entry.dirtyFiles[i];
if (success) {
//把dirtyFile重命名为cleanFile,完成数据迁移;
if (fileSystem.exists(dirty)) {
File clean = entry.cleanFiles[i];
fileSystem.rename(dirty, clean);
long oldLength = entry.lengths[i];
long newLength = fileSystem.size(clean);
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
//删除dirty数据
fileSystem.delete(dirty);
}
}
//计数器加1
redundantOpCount++;
//编辑器指向null
entry.currentEditor = null;
if (entry.readable | success) {
//开始写入数据
entry.readable = true;
journalWriter.writeUtf8(CLEAN).writeByte(' ');
journalWriter.writeUtf8(entry.key);
entry.writeLengths(journalWriter);
journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
//删除key,并且记录
lruEntries.remove(entry.key);
journalWriter.writeUtf8(REMOVE).writeByte(' ');
journalWriter.writeUtf8(entry.key);
journalWriter.writeByte('\n');
}
journalWriter.flush();
//检查是否需要清理
if (size > maxSize || journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
}
这样下来,数据都写入cleanFile了,currentEditor也重新设为null,表明commit彻底结束了。
总结起来DiskLruCache主要的特点:
- 1、通过LinkedHashMap实现LRU替换
- 2、通过本地维护Cache操作日志保证Cache原子性与可用性,同时为防止日志过分膨胀定时执行日志精简。
- 3、 每一个Cache项对应两个状态副本:DIRTY,CLEAN。CLEAN表示当前可用的Cache。外部访问到cache快照均为CLEAN状态;DIRTY为编辑状态的cache。由于更新和创新都只操作DIRTY状态的副本,实现了读和写的分离。
- 4、每一个url请求cache有四个文件,两个状态(DIRY,CLEAN),每个状态对应两个文件:一个0文件对应存储meta数据,一个文件存储body数据。
至此所有的关于缓存的相关类都介绍完毕,为了帮助大家更好的理解缓存,咱们在重新看下CacheInterceptor里面执行的流程
三.OKHTTP的缓存的实现---CacheInterceptor的具体执行流程
(一)原理和注意事项:
1、原理
(1)、okhttp的网络缓存是基于http协议,不清楚请仔细看上一篇文章
(2)、使用DiskLruCache的缓存策略,具体请看本片文章的第一章节
2、注意事项:
1、目前只支持GET,其他请求方式需要自己实现。
2、需要服务器配合,通过head设置相关头来控制缓存
3、创建OkHttpClient时候需要配置Cache
(二)流程:
1、如果配置了缓存,则从缓存中取出(可能为null)
2、获取缓存的策略.
3、监测缓存
4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回
5、如果缓存有效,使用网络,不使用网络
6、如果缓存无效,执行下一个拦截器
7、本地有缓存、根据条件判断是使用缓存还是使用网络的response
8、把response缓存到本地
(三)源码对比:
@Override public Response intercept(Chain chain) throws IOException {
//1、如果配置了缓存,则从缓存中取出(可能为null)
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//2、获取缓存的策略.
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//3、监测缓存
if (cache != null) {
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
//4、如果禁止使用网络(比如飞行模式),且缓存无效,直接返回
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
//5、如果缓存有效,使用网络,不使用网络
// If we don't need the network, we're done.
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//6、如果缓存无效,执行下一个拦截器
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
//7、本地有缓存、根据条件判断是使用缓存还是使用网络的response
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//这个response是用来返回的
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//8、把response缓存到本地
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
(四)倒序具体分析:
1、什么是“倒序具体分析”?
这里的倒序具体分析是指先分析缓存,在分析使用缓存,因为第一次使用的时候,肯定没有缓存,所以肯定先发起请求request,然后收到响应response的时候,缓存起来,等下次调用的时候,才具体获取缓存策略。
PS:由于涉及到的类全部讲过了一遍了,下面涉及的代码就不全部粘贴了,只赞贴核心代码了。
2、先分析获取响应response的流程,保存的流程是如下
在CacheInterceptor的代码是
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
}
核心代码是CacheRequest cacheRequest = cache.put(response);
cache就是咱们设置的Cache对象,put(reponse)方法就是调用Cache类的put方法
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
entry.writeTo(editor);
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
先是 用resonse作为参数来构造Cache.Entry对象,这里强烈提示下,是Cache.Entry对象,不是DiskLruCache.Entry对象。 然后 调用的是DiskLruCache类的edit(String key)方法,而DiskLruCache类的edit(String key)方法调用的是DiskLruCache类的edit(String key, long expectedSequenceNumber)方法,在DiskLruCache类的edit(String key, long expectedSequenceNumber)方法里面其实是通过lruEntries的 lruEntries.get(key)方法获取的DiskLruCache.Entry对象,然后通过这个DiskLruCache.Entry获取对应的编辑器,获取到编辑器后, 再次这个编辑器(editor)通过okio把Cache.Entry写入这个编辑器(editor)对应的文件上。注意,这里是写入的是http中的header的内容 ,最后 返回一个CacheRequestImpl对象
紧接着又调用了 CacheInterceptor.cacheWritingResponse(CacheRequest, Response)方法
主要就是通过配置好的cache写入缓存,都是通过Cache和DiskLruCache来具体实现
总结:缓存实际上是一个比较复杂的逻辑,单独的功能块,实际上不属于OKhttp上的功能,实际上是通过是http协议和DiskLruCache做了处理。
LinkedHashMap可以实现LRU算法,并且在这个case里,它被用作对DiskCache的内存索引
告诉你们一个秘密,Universal-Imager-Loader里面的DiskLruCache的实现跟这里的一模一样,除了io使用inputstream/outputstream
使用LinkedHashMap和journal文件同时记录做过的操作,其实也就是有索引了,这样就相当于有两个备份,可以互相恢复状态
通过dirtyFiles和cleanFiles,可以实现更新和读取同时操作,在commit的时候将cleanFiles的内容进行更新就好了