OkHttp3系列文章
如果有了解过OkHttp的执行流程,可以知道,在拦截器链中有一个缓存拦截器CacheInterceptor,里面决定了是由缓存中获取数据还是通过网络获取。笔者也以这个为入口,展开对OkHttp的缓存分析
1、CacheInterceptor # intercept
public Response intercept(Chain chain) throws IOException {
//判断是否设置cache
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//如果cache 不为null,从strategy中追踪Response
//主要是对networkRequest 或cacheResponse 进行计数
if (cache != null) {
cache.trackResponse(strategy);
}
//如果缓存不适用,则关闭IO流
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body());
}
// 如果网络被禁止并且无缓存,则返回失败504
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();
}
// 在无网络时从缓存中获取
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {
//调用下一个拦截器,访问网络
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());
}
}
// 如果缓存中已经存在对应的Response的处理
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();
//主要是更新response头部数据
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//写入缓存
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);
}
//移除networkRequest
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
CacheStrategy内部封装了网络请求对象networkRequest和cacheResponse(实际就是开始获取的候选缓存对象cacheCandidate)。它是OkHttp的缓存策略的核心。
回到上面代码,首先判断是否已设置cache,如果已设置,根据chain.request()返回的request查找cache中对应的response,然后创建一个CacheStrategy对象strategy 。后续再通过对网络状态和缓存状态进行判断,如果是网络获取且未缓存,得到response后,会先更新写入缓存中,再返回。而上面代码的核心都是对cache的操作(get、put、update、remove)。
2、Cache的产生
cache是InternalCache类型的对象,InternalCache是OkHttp的内部缓存接口。它又是怎么实现的呢?如果有了解过【OkHttp3 源码解析执行流程】这篇文章,会发现,在责任链开始执行之前就创建了CacheInterceptor对象,并从OkHttpClient获取了InternalCache的对象,在下面代码注释1处。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//1
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
client.internalCache()的内部实现
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
代码比较简洁,根据cache是否为null,返回相应的internalCache。针对于cache == null 时返回的internalCache,这个是OkHttpClient静态代码块中创建Internal对象重写setCache()方法返回的。看下面代码(省略部分代码):
static {
Internal.instance = new Internal() {
... ...
@Override
public void setCache(OkHttpClient.Builder builder, InternalCache internalCache) {
builder.setInternalCache(internalCache);
}
... ...
};
}
经过查找发现,并没有任何地方调用setCache这个方法。也就是说在初始化OkHttpClient对象时,如果没有通过调用Cache()方法进行配置缓存,client.internalCache()将返回空,即无缓存。那么主要关注Cache()之后会发生什么。下面一个简单的调用Cache()配置缓存。
private void doOkHttp(){
//缓存路径
String path = Environment.getExternalStorageDirectory().getPath()+"OkHttpCache";
//最大缓存空间
long size = 1024*1024*50;
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(new Cache(new File(path),size)) //配置缓存
.build();
}
这时在创建CacheInterceptor对象时调用client.internalCache()自然就返回了cache.internalCache。
Cache类的构造方法:
public Cache(File directory, long maxSize) {
this(directory, maxSize, FileSystem.SYSTEM);
}
Cache(File directory, long maxSize, FileSystem fileSystem) {
this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
在Cache类的构造方法中创建了DiskLruCache类型的cache 实例,这里的FileSystem.SYSTEM是FileSystem(文件系统接口)的实现,其内部是基于Okio的sink/source对缓存文件进行流操作。在DiskLruCache.Entry内部维护了两个数组,保存每个url请求对应文件的引用。然后通过DiskLruCache.Editor操作DiskLruCache.Entry中的数组,并为Cache.Entry提供Sink/source,对文件流进行操作。这点会在后续分析Cache的put和get中证实。
3、Cache的操作
经过上面的分析,在配置了缓存的情况下,最终返回cache.internalCache,进入Cache类内部的internalCache。
final InternalCache internalCache = new InternalCache() {
@Override
public Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override
public CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
@Override
public void remove(Request request) throws IOException {
Cache.this.remove(request);
}
@Override
public void update(Response cached, Response network) {
Cache.this.update(cached, network);
}
@Override
public void trackConditionalCacheHit() {
Cache.this.trackConditionalCacheHit();
}
@Override
public void trackResponse(CacheStrategy cacheStrategy) {
Cache.this.trackResponse(cacheStrategy);
}
};
到这里,可以发现,经过internalCache最终回调了Cache本身的put、get、update等方法进行操作。这里主要分析下put和get中做了什么事。
Cache.put()
CacheRequest put(Response response) {
//获取请求方法
String requestMethod = response.request().method();
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//如果不是GET请求时返回的response,则不进行缓存
if (!requestMethod.equals("GET")) {
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//把response封装在Cache.Entry中,调用DiskLruCache的edit()返回editor
Entry entry = new Entry(response);
DiskLruCache.Editor editor = null;
try {
//把url进行 md5(),并转换成十六进制格式
//将转换后的key作为DiskLruCache内部LinkHashMap的键值
editor = cache.edit(key(response.request().url()));
if (editor == null) {
return null;
}
//用editor提供的Okio的sink对文件进行写入
entry.writeTo(editor);
//利用CacheRequestImpl写入body
return new CacheRequestImpl(editor);
} catch (IOException e) {
abortQuietly(editor);
return null;
}
}
根据上面的代码发现,OkHttp只针对GET请求时返回的response进行缓存。官方解释:非GET请求下返回的response也可以进行缓存,但是这样做的复杂性高,且效益低。 在获取DiskLruCache.Editor对象editor后,调用writeTo()把url、请求方法、响应首部字段等写入缓存,然后返回一个CacheRequestImpl实例,在CacheInterceptor的intercept()方法内部调用cacheWritingResponse()写入body,最后调用CacheRequestImpl的close()完成提交(实际内部调用了Editor # commit() )。
首先进入DiskLruCache的edit()方法。
public @Nullable Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
//内部主要是利用FileSystem处理文件,如果这里出现了异常,
//在最后会构建新的日志文件,如果文件已存在,则替换
initialize();
//检测缓存是否已关闭
checkNotClosed();
//检测是否为有效key
validateKey(key);
//lruEntries是LinkHashMap的实例,先查找lruEntries是否存在
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
//如果有Editor在操作entry,返回null
if (entry != null && entry.currentEditor != null) {
return null;
}
//如果需要,进行clean操作
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
executor.execute(cleanupRunnable);
return null;
}
// 把当前key在对应文件中标记DIRTY状态,表示正在修改,
//清空日志缓冲区,防止泄露
journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
if (hasJournalErrors) {
return null; // 如果日志文件不能编辑
}
//为请求的url创建一个新的DiskLruCache.Entry实例
//并放入lruEntries中
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
return editor;
}
在最后一步创建DiskLruCache.Entry实例entry ,这个entry本身是不存放任何数据的,主要是维护key(请求url)对应的文件列表,且内部currentEditor不为null,表示当前entry处于编辑状态。这一步返回editor后,前面已经了解到会调用Cache.Entry的writeTo()对返回的editor进行操作。
Cache.Entry # writeTo()
public void writeTo(DiskLruCache.Editor editor) throws IOException {
BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
sink.writeUtf8(url)
.writeByte('\n');
sink.writeUtf8(requestMethod)
.writeByte('\n');
sink.writeDecimalLong(varyHeaders.size())
.writeByte('\n');
//... ...省略,都是利用sink进行写入操作
sink.close();
}
在上面的代码,通过editor.newSink()为上层Cache.Entry提供了一个sink ,然后进行文件写入操作。这里只是把url、请求方法、首部字段等写入缓存,并未写入reponse的body内容。到这里,put()方法已经结束。那么,对于response的body的写入在哪里?前面也提到过,在CacheInterceptor的intercept()方法内部调用cacheWritingResponse()写入body。
@Override
public Response intercept(Chain chain) throws IOException {
... ... //省略代码
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
//在内部实现了response的body的写入
return cacheWritingResponse(cacheRequest, response);
}
... ... //省略代码
)
Cache.get()
@Nullable
Response get(Request request) {
//把url转换成key
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//通过DiskLruCache的get()根据具体的key获取DiskLruCache.Snapshot实例
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
//通过snapshot.getSource()获取一个Okio的Source
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//根据snapshot获取缓存中的response
Response response = entry.response(snapshot);
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
return response;
}
相比于put过程,get过程相对简单点。DiskLruCache.Snapshot是DiskLruCache.Entry的一个快照值,内部封装了DiskLruCache.Entry对应文件的Source,简单的说:根据条件从DiskLruCache.Entry找到相应的缓存文件,并生成Source,封装在Snapshot内部,然后通过snapshot.getSource()获取Source,对缓存文件进行读取操作。
//DiskLruCache # get()
public synchronized Snapshot get(String key) throws IOException {
initialize();
checkNotClosed();
validateKey(key);
//从lruEntries查找entry,
Entry entry = lruEntries.get(key);
if (entry == null || !entry.readable) return null;
//得到Entry的快照值snapshot
Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;
redundantOpCount++;
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
//如果redundantOpCount超过2000,且超过lruEntries的大小时,进行清理操作
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return snapshot;
}
//DiskLruCache.Entry # snapshot()
Snapshot snapshot() {
if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
Source[] sources = new Source[valueCount];
// Defensive copy since these can be zeroed out.
long[] lengths = this.lengths.clone();
try {
//遍历已缓存的文件,生成相应的sources
for (int i = 0; i < valueCount; i++) {
sources[i] = fileSystem.source(cleanFiles[i]);
}
//创建Snapshot并返回
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;
}
}
Cache.Entry # response()
public Response response(DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
Request cacheRequest = new Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build();
return new Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build();
}
总结:
经过分析OkHttp源码,可以知道:Cache只是一个上层的执行者,内部真正的缓存是由DiskLruCache实现的。在DiskLruCache里面通过FileSystem,基于Okio的Sink/Source对文件进行流操作。