最近在写一个开源项目,需要用到 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
是请求头的属性,而非响应头。
直接从关键点切入:
@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); } } 复制代码
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 里有详细描述。
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
,需要的可以自取: 点我
// 缓存大小 100M int size = 100 * 1024 * 1024; Cache cache = new Cache(cacheDir, size); OkHttpClient.Builder builder = new OkHttpClient.Builder(); builder.cache(cache).addInterceptor(new RequestHeadersInterceptor()); ... 复制代码
一般情况下,客户端不应该修改响应头。客户端使用什么样的缓存策略,应当由服务器兄弟确定。只有特殊情况下,才需要客户端额外配置。比如调用的是第三方服务器接口,其缓存策略不符合客户端的要求等。这里给出一个简单示例:
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; } } 复制代码
// 缓存大小 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
有三点总结: