在Http协议中,缓存的控制是通过首部的Cache-Control来控制,通过对Cache-Control进行设置,即可实现不同的缓存策略。
Cache-Control和其他的首部字段一样,使用key:value结构,同时value可有多个值, 值之间以,分隔(具体参考HTTP详解)。Cache-Control是一个通用首部字段,在Http请求报文中可使用,也可在应答报文中使用。
max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;
must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。
更详细内容可参考:Http首部字段定义
了解了HTTP的理论知识,后面我们对OkHttp中的缓存进行简单的介绍。
OkHttp默认对Http缓存进行了支持,只要服务端返回的Response中含有缓存策略,OkHttp就会通过CacheInterceptor拦截器对其进行缓存。但是OkHttp默认情况下构造的HTTP请求中并没有加Cache-Control,即便服务器支持了,我们还是不能正常使用缓存数据。所以需要对OkHttp的缓存过程进行干预,使其满足我们的需求。
OkHttp的优雅之处就在于使用了责任链模式,将请求-应答过程中的每一步都通过一个拦截器来实现,并对此过程的头部和尾部都提供了扩展,这也为我们干预缓存过程提供了可能。所以在实现缓存之前,我们需要对OkHttp对拦截器的处理过程有个大概的了解。
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
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);
return chain.proceed(originalRequest);
}
以上代码就是整个拦截器的处理过程,具体的流程可参考源码,这里我们只说一下基本的流程:发起请求时,会按interceptors中加入的顺序依次执行,返回Response时按照逆序执行:
自定义拦截器 <-> 内置拦截器(retryAndFollowUpInterceptor...ConnectInterceptor)
<-> 网络拦截器 <-> CallServerInterceptor
其中CallServerInterceptor就是负责发送请求与接收应答的拦截器。由于我们关注的只是缓存,所以只考虑内置拦截器中的CacheInterceptor。那么流程可简化为:
Request <-> 自定义拦截器 <-> CacheInterceptor <-> 网络拦截器 <-> Response
从这个流程可以看出,如果服务端返回的Response中没有Cache-Control, 那么我们可通过添加网络拦截器来实现。同样,在访问缓存数据时,我们可通过添加自定义拦截器来实现。
在开始添加缓存策略之前,我们先了解一个完整的缓存策略:
整体来说,在有网络的情况下,使用缓存还是比较复杂,这里我们通过简化版的缓存策略(有网络时访问服务器,无网络时返回缓存数据)来演示OkHttp使用缓存的过程。
首先,我们通过定义一个网络拦截器来为Response添加缓存策略:
public class HttpCacheInterceptor implements Interceptor {
private Context context;
public HttpCacheInterceptor(Context context) {
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
return chain.proceed(chain.request()).newBuilder()
.request(newRequest)
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + 1)
.build();
return response;
}
}
其次,通过自定义拦截器设置Request使用缓存的策略:
public class BaseInterceptor implements Interceptor {
private Context mContext;
public BaseInterceptor(Context context) {
this.mContext = context;
}
@Override
public Response intercept(Chain chain) throws IOException {
if (NetworkUtil.isConnected(mContext)) {
return chain.proceed(chain.request());
} else { // 如果没有网络,则返回缓存未过期一个月的数据
Request newRequest = chain.request().newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
return chain.proceed(newRequest);
}
}
}
Pragma是Http/1.1之前版本遗留的字段,用于做版本兼容,但不同的平台对此有不同的实现,所以在使用缓存策略时需要将其屏蔽,避免对缓存策略造成影响。
将对修改Request和Response缓存策略的拦截器应用于OkHttp:
OkHttpClient httpClient = new OkHttpClient.Builder()
.addInterceptor(new BaseInterceptor(context))
.addNetworkInterceptor(new HttpCacheInterceptor(context))
.cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 设置缓存路径和缓存容量
.build();
接下来就可以在无网络的情况下愉快地使用缓存数据了。
如果觉得OkHttp的缓存太复杂,想自己来缓存数据怎么办呢?有两种方案来实现:
- 自定义拦截器,
- 监听OkHttp的请求过程,在请求完成时缓存数据;
这种方案首先需要考虑应使用普通的拦截器还是网络拦截器,上面我们已经了解了整个请求过程中拦截器的执行顺序,需要注意的是:在无网络的情况下,请求在执行到CacheIntercepter,如果没有缓存数据,将会直接返回,并不会执行到自定义的网络拦截器中,所以不适合在网络拦截器中缓存数据。那么我们可通过自定义普通拦截器来实现,基本的过程如下:
@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {
Response response = null;
if (NetworkUtil.isConnected(mContext)) {
response = chain.proceed(newRequest);
saveCacheData(response); // 保存缓存数据
} else { // 不执行chain.proceed会打断责任链,即后面的拦截器不会被执行
response = getCacheData(chain.request().url()); // 获取缓存数据
}
return response;
}
OkHttp: 使用这种方案你良心不会痛吗?
这种方案可以说摒弃了OkHttp扩展拦截器这一强大的功能,直接与请求和应答进行交互,基本的过程如下:
Request request = new Request.Builder()
.url(realUrl)
.build();
if (NetworkUtil.isConnected()) {
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
// 返回缓存数据
}
@Override
public void onResponse(Response response) throws IOException {
// 1. 缓存数据
// 2. 返回请求结果
}
});
} else {
// 返回缓存数据
}
这两种方案都抛弃了OkHttp自己实现的缓存策略,所以更加灵活,尤其是监听OkHttp请求过程这种方法。但也都有一个很大的缺点:需要实现一个缓存模块。在开发中具体使用哪种缓存策略,根据已有代码模块和需求衡量即可。
实际的开发过程中,我们在网络请求中会添加一些公共参数,对于一些可变的公共参数,在缓存数据和访问缓存数据的过程中需要删除,比如网络类型,有网络时其值为Wifi或4G等,无网络时可能为none, 这时访问缓存时就会因url不一致导致访问缓存失败。
@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {
// 添加公共参数
HttpUrl.Builder urlBuilder = chain.request().url().newBuilder()
.addQueryParameter("a", "a")
.addQueryParameter("b", "b");
Request.Builder requestBuilder = chain.request().newBuilder();
if (NetworkUtil.isConnected(mContext)) {
urlBuilder.addQueryParameter("network", NetworkUtil.getNetwokType(mContext));
} else { // 无网络时不添加可变的公共参数
requestBuilder.removeHeader("Pragma")
.header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
}
Request newRequest = requestBuilder
.url(urlBuilder.build())
.build();
return chain.proceed(newRequest);
}
@Override // HttpCacheInterceptor.java
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());
HttpUrl newUrl = chain.request().url().newBuilder()
.removeAllQueryParameters("network")
.build(); // 缓存数据前删除可变的公共参数
Request newRequest = chain.request().newBuilder()
.url(newUrl)
.build();
return response.newBuilder()
.request(newRequest)
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + 1)
.build();
}