源码分析,如需转载,请注明作者:Yuloran (t.cn/EGU6c76)
前言
最近在写一个开源项目,需要用到 Http 的缓存机制。由于项目所使用的 Http 客户端为 OkHttp,所以需要了解如何使用 OkHttp 来实现 Http 的缓存控制。很惭愧,这一块不太熟悉,所以就到网上 CV 了一下。虽然我知道网上很多博客不太靠谱,但是没想到,居然真掉坑里了。
错误示例
不点名了,网上很多:
public class CacheControlInterceptor implements Interceptor
{
@Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
if (!NetworkUtil.isNetworkConnected())
{
request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
}
Response.Builder builder = chain.proceed(request).newBuilder();
if (NetworkUtil.isNetworkConnected())
{
// 有网络时, 不缓存, 最大保存时长为1min
builder.header("Cache-Control", "public, max-age=60").removeHeader("Pragma");
} else
{
// 无网络时,设置超时为1周
long maxStale = 60 * 60 * 24 * 7;
builder.header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale).removeHeader("Pragma");
}
return builder.build();
}
}
// 省略...
builder.addNetworkInterceptor(new CacheControlInterceptor());
复制代码
这段代码的表现结果:请求成功后,断开网络,重新打开页面,1min 内可以看到数据,1min 后数据消失。
错误原因
在看了 OKHttp 拦截器调用源码以及 Http Cache-Control 后,发现上述代码可以说没有一行是正确的,也就是说逻辑完全不对:
-
没有网络时,修改请求头设为强制使用缓存的逻辑,应当置于普通拦截器(
addInterceptor
)中,而不是网络拦截器(addNetworkInterceptor
)。因为没有网络时,OkHttp 的ConnectInterceptor
会抛出UnKnownHostException
,终止执行后续拦截器。而networkInterceptors
正是位于ConnectInterceptor
之后; -
对于 OkHttp 来说,即使服务器没有设置
Cache-Control
响应头,客户端也不用额外设置。因为在开启OkHttpClient
的缓存功能后,GET
请求的响应报文会被自动缓存。若要禁止缓存,在接口上加上@Headers("Cache-Control: no-store")
注解即可; -
only-if-cached, max-stale
是请求头的属性,而非响应头。
错误证明
直接从关键点切入:
RealCall::execute()
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
eventListener.callStart(this);
try {
client.dispatcher().executed(this);
// 发起请求并获得响应
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} catch (IOException e) {
eventListener.callFailed(this, e);
throw e;
} finally {
client.dispatcher().finished(this);
}
}
复制代码
RealCall::getResponseWithInterceptorChain()
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
// 新建一个数组,并把所有拦截器都加进去。因为是数组,所以只能按照拦截器的添加顺序依次执行
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors()); // 1. 普通拦截器
interceptors.add(retryAndFollowUpInterceptor); // 2. 连接重试拦截器
interceptors.add(new BridgeInterceptor(client.cookieJar())); // 3. 请求头,响应头再加工拦截器
interceptors.add(new CacheInterceptor(client.internalCache())); // 4. 缓存保存与读取拦截器
interceptors.add(new ConnectInterceptor(client)); // 5. 创建连接拦截器
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors()); // 6. 网络拦截器
}
interceptors.add(new CallServerInterceptor(forWebSocket)); // 7. 接口请求拦截器
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
复制代码
从源码中可看出,所有拦截器都保存在同一个数组中,然后新建一个 chain
,并将该数组存储到这个 chain
中。这个 chain
,就是启动整个拦截器执行链的头结点。具体过程如下:
那么,为什么在网络拦截器中修改请求头为 FORCE_CACHE
没有用呢?因为在没有网络时,ConnectInterceptor
会直接抛出 UnKnownHostException
,终止执行链继续向下执行,所以位于其后面的网络拦截器不会被执行:
至于请求头与响应头,Cache-Control
如何设置才是正确的,Http Cache-Control 里有详细描述。
正确示例
无网时,强制使用缓存:
1. 创建请求头拦截器
public class RequestHeadersInterceptor implements Interceptor
{
private static final String TAG = "RequestHeadersInterceptor";
@Override
public Response intercept(Chain chain) throws IOException
{
Logger.debug(TAG, "RequestHeadersInterceptor.");
Request request = chain.request();
Request.Builder builder = request.newBuilder();
// builder.header("Content-Type", "application/json;charset=UTF-8")
// .header("Accept-Charset", "UTF-8");
if (!NetworkService.getInstance().getNetworkInfo().isConnected())
{
// 无网络时,强制使用缓存
Logger.debug(TAG, "network unavailable, force cache.");
builder.cacheControl(CacheControl.FORCE_CACHE);
}
return chain.proceed(builder.build());
}
}
复制代码
NetworkService
是我写的网络连接探测器,基于 API 21
,需要的可以自取:点我
2. 添加请求头拦截器
// 缓存大小 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addInterceptor(new RequestHeadersInterceptor());
...
复制代码
篡改服务器响应头
一般情况下,客户端不应该修改响应头。客户端使用什么样的缓存策略,应当由服务器兄弟确定。只有特殊情况下,才需要客户端额外配置。比如调用的是第三方服务器接口,其缓存策略不符合客户端的要求等。这里给出一个简单示例:
1. 创建响应头拦截器
public class CacheControlInterceptor implements Interceptor
{
private static final String TAG = "CacheControlInterceptor";
@Override
public Response intercept(Chain chain) throws IOException
{
Logger.debug(TAG, "CacheControlInterceptor.");
Response response = chain.proceed(chain.request());
String cacheControl = response.header("Cache-Control");
if (StringUtil.isEmpty(cacheControl))
{
Logger.debug(TAG, "'Cache-Control' not set by the backend, add it ourselves.");
return response.newBuilder().removeHeader("Pragma").header("Cache-Control", "public, max-age=60").build();
}
return response;
}
}
复制代码
2. 添加响应头拦截器
// 缓存大小 100M
int size = 100 * 1024 * 1024;
Cache cache = new Cache(cacheDir, size);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.cache(cache).addNetworkInterceptor(new CacheControlInterceptor ());
...
复制代码
结语
请求与响应的本质是不同主机利用各自的 IP 地址和端口号,通过 Socket 编程接口互相发送信息。为了约束数据交换格式,产生了 Http 协议。由于 Http 是明文传输,为了传输安全,又产生了 Https 协议。既然是协议,那么只有在双方都遵守的情况下才会生效。所以,在项目开发中,我们经常需要跟服务器兄弟进行接口联调,以保证约定被正确实现。OkHttp 扮演的角色类似于浏览器,共同点是都将请求与响应封装成了用户友好的形式,都支持错误重连、报文缓存等机制,不同的是浏览器还需要负责网页渲染等。
本文表面上描述的是如何利用 OkHttp 实现缓存控制,实则阐述了 OkHttp 的请求与响应的执行机制。所谓通则一通百通,利用 OKHttp 实现其它功能现在应该也不是问题了。比如实现一个加解密拦截器,对请求体进行加密,对响应报文进行解密,显然,这个拦截器,需要加到网络拦截器中。
OkHttp 的 Response 对象,是对真正响应报文(networkResponse 和 cacheResponse)的封装。所以,只要不在拦截器中调用 response.body()
方法,就不会导致请求阻塞,尤其是响应报文很大的时候,更不能调用。
最后,针对 Cahce-Control
有三点总结:
- 要正确理解 Http 协议的约定,MDN 是个优秀的网站
- 遇到问题多读源码,只有源码才不会骗人
- 实践是检验真理的唯一标准
所有评论(0)