在上一篇Okhttp解析(一)– 源码中,我们从源码角度分析了同步请求和异步请求的流程,这一篇我们主要是分析Okhttp的缓存实现。那么Okhttp的缓存是如何使用的呢,下面是简单的示例代码:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(new Cache(文件对象,1024*1024*5))
.build();
其实很简单就是在构建OkhttpClient的时候连缀了一个cache函数,并传入一个Cache实例,Cache构造要求两个参数,第一个是缓存的文件对象;第二个是缓存的大小(以字节为单位)。下面我们来看下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);
}
我们发现两个参数的构造会调用三个参数的构造,在该构造中会实例化一个DiskLruCache硬盘缓存对象,那么这个硬盘缓存对象是什么呢?
它是一个基于Lru算法的有限存储空间的文件系统,每个缓存实体都是以key-value的形式存在,key必须能够匹配正则[a-z0-9_-]{1,64}
,value是以字节序列的形式存在的,长度在0-Integer.MAX_VALUE之间。
这里我们介绍一下Lru算法吧,然后再继续。 Lru是Least Recently Used的缩写,即最近最少使用,假设我们的缓存一共可以存储1000条数据,当实际存储量小于1000时随便添加,但是当存储量超过1000呢?它会从之前缓存的数据中删除最老的一条,从而腾出空间保存新的。不过需要注意一下,所谓的最老并不是说就是第一条保存的数据,比如:第一次缓存第一条数据,在第1000次我们又请求了该数据,那么它就会保存在最前面(即最新),其它的所有保存的数据都会被顶到后面去。具体的实现,感兴趣的话可以自己去了解一下。
好了缓存现在已经配置了,那么它会在哪里使用呢?答案就是CacheInterceptor,我们去看一下它的intercept函数:
@Override public Response intercept(Chain chain) throws IOException {
//判断缓存类是否为null,按照上面的示范代码,默认是null,因为在初始化CacheInterceptor
//类时会传入一个InternalCache对象,而在使用空构造函数创建OkhttpClient时,
//默认的Builder是没有默认的InternalCache对象的
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
...
}
第一句就是我们想要的,其它的暂时省略。判断cache是否为null,不为null传入请求获取Response对象,认真的朋友可能会发现这个cache好像不是我们实例化的Cache对象,而是一个InternalCache对象,瞬间凌乱了。。。不要急,我们看下在初始化CacheInterceptor时传入的是什么?
//RealCall的getResponseWithInterceptorChain函数
Response getResponseWithInterceptorChain() throws IOException {
...
interceptors.add(new CacheInterceptor(client.internalCache()));
...
}
原来是调用OkhttpClient的internalCache函数:
InternalCache internalCache() {
return cache != null ? cache.internalCache : internalCache;
}
原来就是传入我们初始化的cache的internalCache 参数。我们进到Cache看看它:
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);
}
};
可以发现它是Cache的一个成员变量,是InternalCache接口的匿名实现类对象,不过认真看它的接口实现,实际上调用的都是Cache的函数,所以我们直接看Cache的即可。现在继续上面的话题,传入Request对象,获取Response对象,即Cache的get函数:
@Nullable Response get(Request request) {
//将url进行了UTF-8编码、md5,然后转换为16进制字符串
String key = key(request.url());
DiskLruCache.Snapshot snapshot;
Entry entry;
try {
//此处cache为DiskLruCache对象,通过DiskLruCache的get函数获取和key对应的value值的快照对象(一种备份)
snapshot = cache.get(key);
if (snapshot == null) {
return null;
}
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
try {
//根据快照的元信息来实例化一个Entry对象(这里的元信息主要就是一些响应头信息)
entry = new Entry(snapshot.getSource(ENTRY_METADATA));
} catch (IOException e) {
Util.closeQuietly(snapshot);
return null;
}
//获取响应结果
Response response = entry.response(snapshot);
//将请求和响应作一下对比,如果不匹配返回null
if (!entry.matches(request, response)) {
Util.closeQuietly(response.body());
return null;
}
//返回结果
return response;
}
在Cache的get函数中主要做了以下几个操作:
(1)对url进行编码生成key
(2)调用DiskLruCache对象的get函数,获取快照对象
(3)通过响应头信息的标识(索引为0),从快照中获取响应头信息的字节流对象(Source可以理解为流)然后封装成Entry对象
(4)调用Entry对象的response函数,传入快照,获取响应对象(根据快照信息,会通过Okio来读取ResposeBody)
(5)检查请求和响应是否匹配
(6)返回结果
现在对上面比较关键的作一下分析,首先是第二点:
/**
* 通过key获取Entry的快照信息
*/
public synchronized Snapshot get(String key) throws IOException {
initialize();
//检查是否关闭
checkNotClosed();
//判断key是否有效
validateKey(key);
//根据key,从LinkHashMap中获取缓存的Entry实体对象(注意这里的lruEntries缓存的Entry只包含元信息,没有Body信息)
Entry entry = lruEntries.get(key);
if (entry == null || !entry.readable) return null;
//获取快照对象
Snapshot snapshot = entry.snapshot();
if (snapshot == null) return null;
redundantOpCount++;
journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
if (journalRebuildRequired()) {
executor.execute(cleanupRunnable);
}
return snapshot;
}
对于第三点,主要是元信息是个什么东西,源代码中是这么注释的:
//获取了响应头的HTTP响应Entry应该是这样的
{
http://google.com/foo
GET
2
Accept-Language: fr-CA
Accept-Charset: UTF-8
HTTP/1.1 200 OK
3
Content-Type: image/png
Content-Length: 100
Cache-Control: max-age=600
}
//获取了响应头的HTTPS响应Entry应该是这样的
{
https://google.com/foo
GET
2
Accept-Language: fr-CA
Accept-Charset: UTF-8
HTTP/1.1 200 OK
3
Content-Type: image/png
Content-Length: 100
Cache-Control: max-age=600
AES_256_WITH_MD5
2
base64-encoded peerCertificate[0]
base64-encoded peerCertificate[1]
-1
TLSv1.2
}
第四点,通过reponse()来获取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();
}
主要就是拼接请求,然后是拼接响应结果,我们看一下body部分即可:
CacheResponseBody(final DiskLruCache.Snapshot snapshot,
String contentType, String contentLength) {
this.snapshot = snapshot;
this.contentType = contentType;
this.contentLength = contentLength;
Source source = snapshot.getSource(ENTRY_BODY);
bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
});
}
可以发现就是通过快照获取body的流对象(Source对象),然后通过Okio来读取。到这里我们就获取到了一个Response。
此时我们就从Cache中获取到了一个Response对象。在CacheInterceptor的intercept中,接下来就是把获取到的响应和请求一起传入到CacheStrategy的Factory的构造函数中,然后调用其get()函数获取CacheStrategy对象,我们看一下其实现。
/**
* 如果请求需要使用缓存的话,返回一个缓存策略实例
*/
public CacheStrategy get() {
//获取缓存实例
CacheStrategy candidate = getCandidate();
if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
// 网络和缓存都无法使用时返回CacheStrategy对象,其后续请求和缓存结果都为null,缓存适配器拿到之后,会直接返回504
return new CacheStrategy(null, null);
}
//返回缓存实例
return candidate;
}
CacheStrategy.Factory的构造函数,其实就是为参数赋值,我们就不看了,而get()函数的话会得到一个CacheStrategy对象,然后返回到intercept中,我接下来继续看。其实逻辑大部分在上一篇都讲了:
(1)如果CacheStrategy对象返回的新的请求和缓存结果都为null,说明失败了。直接返回响应,响应码为504
(2)后续请求为null,那么响应结果肯定不为null,不然不会走到这里,因为上一步已经对两个都为null的做了校验。此时说明不需要网络请求,直接返回缓存的响应结果
(3)后续请求不为null,调用下一个拦截器获取网络请求的对象
(4)在获取网络请求响应结果之后,如果缓存的响应也不为null,那么根据网络响应的响应码是否等于304走不同逻辑。等于说明数据未改变,会使用网络响应的头信息来刷新缓存的头信息,Body部分不变。变化的话就将缓存的响应关闭
(5)构建新的响应结果
(6)缓存对象cache不为null,并且有请求体,可缓存,那么就把响应结果添加到cache中。
(7)缓存对象cache不为null,但是是无效的缓存,移除请求
(8)返回结果
上面这8个步骤里面前面就不说了,重点在第6点,即响应和请求符合要求,将其添加到缓存中,我们看一下这里的代码:
@Nullable CacheRequest put(Response response) {
//获取请求的方法
String requestMethod = response.request().method();
//如果请求的方法是无效的,那么移除请求(POST,PATCH,PUT,DELETE,MOVE都是无效的请求方式)
if (HttpMethod.invalidatesCache(response.request().method())) {
try {
remove(response.request());
} catch (IOException ignored) {
// The cache cannot be written.
}
return null;
}
//如果请求方式不是GET,那么直接返回null,其实HEAD和部分POST请求其实也是可以缓存的,但是成本高,效益低,所以不考虑
if (!requestMethod.equals("GET")) {
return null;
}
if (HttpHeaders.hasVaryAll(response)) {
return null;
}
//使用响应结果构建Entry对象
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;
}
}
写入本地之后,就返回一个CacheRequest接口的实现类CacheRequestImpl对象,然后通过处理CacheRequestImpl对象和响应结果这两个参数,返回真正的响应结果。
到这里缓存的流程也分析完了。
有一点需要说明一下:有些人可能会觉得奇怪,明明是硬盘缓存,怎么内存里面还有一个LinkHashMap对象来缓存Entry对象,其实在添加缓存的时候我们通过Response对象实例化了一个Entry对象,在它的构造里面,我们发现它并没有为Body部分赋值,只是保存了头信息等元信息,很小。它主要的作用是起到一个类似索引表的功能,提高效率。