文章目录
1. OkHttp缓存机制部分源码查看
为了方便在AndroidStudio
中查看源码,这里更换4.9.0版本为:
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
如果需要其他的版本,可以在mvnrepository中搜索。
切换AndroidStudio
的为Project
视图,然后找到OkHttp
,可以看到:
简单看下这几个类:
1.1. CacheInterceptor缓存拦截器
CacheInterceptor
:缓存拦截器,负责拦截处理网络请求。注意到下面的注释:处理来自缓存的请求并将响应写入缓存。
/** Serves requests from the cache and writes responses to the cache. */
public final class CacheInterceptor implements Interceptor {
final @Nullable InternalCache cache;
@Override
public Response intercept(Chain chain) throws IOException {
// 如果本次请求缓存有数据,就直接取出
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
// 将请求传入到缓存策略,得到cacheResponse
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
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.
// 请求失败,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 we don't need the network, we're done.
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());
}
}
// If we have a cache response too, then we're doing a conditional get.
// 协商缓存,如果本地存在,且资源为修改,就直接返回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();
// 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());
}
}
// 请求数据,写入cache
...
return response;
}
}
1.2. CacheStrategy缓存策略类
CacheStrategy
:缓存策略类。主要用来判断当前缓存时候可用。在这个类的内部类Factory
的get
方法中,调用getCandidate
方法,这个方法会将请求进行包装,也即是在请求头部添加和缓存控制相关的头部信息,比如这里我截取部分:
// CacheStrategy.Factory->getCandidate方法
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
// 包装,添加头部信息
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
// 关联到请求的header
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
1.3. DiskLruCache磁盘缓存类
Google
提供了一套硬盘缓存的解决方案:DiskLruCache
(非Google
官方编写,但获得官方认证)。可以参考郭大佬的Android DiskLruCache完全解析,硬盘缓存的最佳方案来了解这个类。
在前面我们在OkHttpClient
创建的时候指定了cache
,如:
OkHttpClient client = new OkHttpClient.Builder()
.cache(new Cache(file, cacheSize))
// .addInterceptor(new MyInteceptor())
.build();
这里看下Cache
这个类:
public final class Cache implements Closeable, Flushable {
final DiskLruCache cache;
final InternalCache internalCache = new InternalCache() {
@Override
public @Nullable Response get(Request request) throws IOException {
return Cache.this.get(request);
}
@Override
public @Nullable CacheRequest put(Response response) throws IOException {
return Cache.this.put(response);
}
...
}
}
其实Cache
类底层也就是调用DiskLruCache
。
2. 缓存案例
在上文OkHttp的HTTP缓存使用中做了简单的使用,但是不难发现在上篇的案例中虽然能够使用HTTP
缓存,但是具有一定的局限性。
因为使用OkHttp
的HTTP
缓存时候需要确保服务端的响应是设置了对应的HTTP
缓存首部控制信息。这种双方编码人员需要协商。显然这种方式不够简洁。
所以可以尝试在OkHttp
客户端这边加入首部信息,而拦截器可以做这件事情。
2.1 场景:服务器响应首部没有HTTP缓存控制信息
自定义一个拦截器,添加对应请求缓存控制,也响应
class MyInteceptor implements Interceptor{
@Override
public Response intercept( Chain chain) throws IOException {
// 1. 请求头部设置缓存信息
Request request = chain.request().newBuilder()
.cacheControl(CacheControl.FORCE_CACHE) // 强制从缓存中读取
.build();
// todo
Log.e("TAG", "intercept: ");
// 2. 响应请求头添加
// 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
return chain.proceed(request).newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.addHeader("Cache-Control", "max-age=1800")
.build();
}
}
这里如果不适用强制从缓存读取,会导致虽然OkHttp
每次都写入了缓存数据,但是数据却没有从本地缓存中读取。
此时的缓存文件头部:
和上篇OkHttp的HTTP缓存使用中的截图做对比可以发现这里写入缓存的首部信息少了Cache-Control
和Last-Modified
。
但至于怎么让他可以有这两个字段,暂时我还不知道。
也就是暂时还不知道怎么做到理想情况:有缓存且没过期就从本地缓存中读取数据,没有缓存或者是已经过期就从服务器进行联网加载数据。
找到了解决办法。还是使用拦截器,只是在使用的时候使用的是addNetworkInterceptor
,而不是使用addInterceptor
。
File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
long cacheSize = 10 * 1024 * 1024L; // 10MB
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new MyInteceptor()) // 这里添加拦截器使用的是addNetworkInterceptor
.cache(new Cache(file, cacheSize))
.build();
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES) // 缓存一分钟内有效,直接加载缓存
.build();
Request request = new Request.Builder()
.cacheControl(cacheControl)
.url("http://192.168.1.110:90/test/1.0/users/1")
.build();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("数据请求失败。");
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
int code = response.code();
String string = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(code+"\n"+string);
}
});
}
});
}
}
);
// MyInteceptor.java
class MyInteceptor implements Interceptor{
@Override
public Response intercept( Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.build();
// todo
Log.e("TAG", "intercept: ");
// 响应请求头添加
//设置缓存时间为60秒,并移除了pragma消息头
return chain.proceed(request).newBuilder()
.removeHeader("Pragma")
.addHeader("Cache-Control", "max-age=1800")
.addHeader("Last-Modified", getTime())
.build();
}
private String getTime(){
ZonedDateTime zonedDateTime = ZonedDateTime.now().with(LocalTime.MAX);
return zonedDateTime.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}
就可以做到在一分钟内的请求,会直接从缓存中读取,而不会请求后台服务器数据。此时再次来看下缓存的数据头部信息:
可以发现确实将Cache-Control
和Last-Modified
这两个字段添加到了本地缓存头部文件中。也证实了前面的分析确实是正确的。所以这里从缓存中拿取数据才会成功。
对于addNetworkInterceptor和addInterceptor这两个方法做一个区分:
// OkHttpClient.java
public static final class Builder {
final List<Interceptor> interceptors = new ArrayList<>();
final List<Interceptor> networkInterceptors = new ArrayList<>();
...
}
这两者都是List
列表存储的拦截器对象。这里看不出有什么区别。不妨来看下官网的API
文档:here
但是其实也没有得到解释。不过网上有很多解释,这里做一个笔记:
addInterceptor
是添加应用拦截器,而addNetworkInterceptor
是网络拦截器。
应用拦截器不关心OkHttp注入的头信息,所以在这里注册的拦截器并不会添加自己添加的响应头部信息到磁盘缓存中;
网络拦截器能够操作中间过程的响应,根据上面的案例我们知道使用这种方式可以使用本地未过期的磁盘缓存。
2.2 场景:断网强制使用缓存(服务器响应首部没有HTTP缓存控制信息)
这里就直接设置请求头的缓存控制为强制从缓存读取数据。
ps:这么做有个问题,那就是如果将缓存目录文件删除了,或者数据更新了,这里是不知道的。因为除了第一次获取数据后,强制缓存就根本不联网。
所以可以将强制读取用在断网的时候,强制从缓存中加载。其余的时候就不适用强制缓存。
File file = new File(Environment.getExternalStorageDirectory(), "DCIM");
long cacheSize = 10 * 1024 * 1024L; // 10MB
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new MyInteceptor()) // 添加请求拦截器,以添加响应首部缓存信息
.cache(new Cache(file, cacheSize))
.build();
CacheControl cacheControl = new CacheControl.Builder()
.maxAge(1, TimeUnit.MINUTES)
.build();
Request request = new Request.Builder()
.cacheControl(cacheControl)
.url("http://192.168.1.102:90/test/1.0/users/1")
.build();
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText("数据请求失败。");
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
int code = response.code();
String string = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
textView.setText(code+"\n"+string);
}
});
}
});
}
}
);
// MyInteceptor.java
class MyInteceptor implements Interceptor{
@Override
public Response intercept( Chain chain) throws IOException {
// 1. 请求头部设置缓存信息
Request request = chain.request().newBuilder()
.cacheControl(CacheControl.FORCE_CACHE) // 强制从缓存中读取
.build();
// todo
Log.e("TAG", "intercept: ");
// 2. 响应请求头添加
// 移除了pragma消息头,移除它的原因是因为pragma也是控制缓存的一个消息头属性
return chain.proceed(request).newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
.addHeader("Cache-Control", "max-age=1800")
.build();
}
}
注意添加权限。
References