前言
安卓技术学习图谱(持续更新中,欢迎关注)
https://github.com/Sakuragi/android-summary
实现需求
无网的时候读取本地缓存,有网的情况下更新缓存。
注:本文只是给了post的缓存的一种解决思路,具体的本地持久化策略还需读者自己实现
解决方案
默认情况下,一般认为使用GET通过HTTP访问的请求可以缓存,也应当缓存。使用HTTPS或POST访问的请求不应当缓存。OKHttp也是这样设计的,OKHttp默认不支持Post缓存。那么我们该如何来设计使OKHttp支持POST缓存呢?
大致有这么几种持久化方式:
- Sqlite :网络正常时缓存响应信息到数据库,在没有网络的时候读出数据。
- DiskLruCache :通过文件缓存到本地。
结合OKHttp的源码,我们发现OKHttp也是使用了DiskLruCache进行GET请求缓存的。我们首先来看看OkHttp是怎么进行缓存的。
当OKHttp执行excute时:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
通过源码可以看到它通过getResponseWithInterceptorChain()方法得到响应,这个方法是通过一个一个拦截器,以责任链模式发起请求获得响应,来进一步看这个方法:
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);
return chain.proceed(originalRequest);
}
在这里面我们可以看到添加了一个缓存拦截器interceptors.add(new CacheInterceptor(client.internalCache())),继续跟踪进去:
public final class CacheInterceptor implements Interceptor {
final InternalCache cache;
public CacheInterceptor(InternalCache cache) {
this.cache = cache;
}
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
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.
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.
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());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
private static Response stripBody(Response response) {
return response != null && response.body() != null
? response.newBuilder().body(null).build()
: response;
}
/**
* Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
* consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
* may never exhaust the source stream and therefore not complete the cached response.
*/
private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
throws IOException {
// Some apps return a null body; for compatibility we treat that like a null cache request.
if (cacheRequest == null) return response;
Sink cacheBodyUnbuffered = cacheRequest.body();
if (cacheBodyUnbuffered == null) return response;
final BufferedSource source = response.body().source();
final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
Source cacheWritingSource = new Source() {
boolean cacheRequestClosed;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead;
try {
bytesRead = source.read(sink, byteCount);
} catch (IOException e) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheRequest.abort(); // Failed to write a complete cache response.
}
throw e;
}
if (bytesRead == -1) {
if (!cacheRequestClosed) {
cacheRequestClosed = true;
cacheBody.close(); // The cache response is complete!
}
return -1;
}
sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
cacheBody.emitCompleteSegments();
return bytesRead;
}
@Override public Timeout timeout() {
return source.timeout();
}
@Override public void close() throws IOException {
if (!cacheRequestClosed
&& !discard(this, HttpCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
source.close();
}
};
return response.newBuilder()
.body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
.build();
}
/** Combines cached headers with a network headers as defined by RFC 2616, 13.5.3. */
private static Headers combine(Headers cachedHeaders, Headers networkHeaders) {
Headers.Builder result = new Headers.Builder();
for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
String fieldName = cachedHeaders.name(i);
String value = cachedHeaders.value(i);
if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
continue; // Drop 100-level freshness warnings.
}
if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
Internal.instance.addLenient(result, fieldName, value);
}
}
for (int i = 0, size = networkHeaders.size(); i < size; i++) {
String fieldName = networkHeaders.name(i);
if ("Content-Length".equalsIgnoreCase(fieldName)) {
continue; // Ignore content-length headers of validating responses.
}
if (isEndToEnd(fieldName)) {
Internal.instance.addLenient(result, fieldName, networkHeaders.value(i));
}
}
return result.build();
}
/**
* Returns true if {@code fieldName} is an end-to-end HTTP header, as defined by RFC 2616,
* 13.5.1.
*/
static boolean isEndToEnd(String fieldName) {
return !"Connection".equalsIgnoreCase(fieldName)
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
&& !"TE".equalsIgnoreCase(fieldName)
&& !"Trailers".equalsIgnoreCase(fieldName)
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
}
我们来看这个缓存拦截器的实现方式:主要分三步:
第一步:首先判断用户是否设置缓存,如果有的话读出缓存
第二部:根据CacheStrategy缓存策略,判断是否需要网络请求,如果networkRequest为空则说明不需要网络请求,直接返回当前缓存
第三步:获取网络请求,如果缓存不为空,调用validate进行验证,是否需要更新缓存,如果缓存为空,则保存当前缓存。
根据这个实现步骤,我们可以模仿拦截器实现网络缓存:
public class PostCacheInterceptor implements Interceptor {
final DiskLruCacheHelper cache;
public PostCacheInterceptor(DiskLruCacheHelper cache) {
this.cache = cache;
}
private final int REQUEST_URL = 0;
private final int REQUEST_METHOD = 1;
private final int REQUESTCONTENTTYPE = 2;
private final int PROTOCAL = 3;
private final int CODE = 4;
private final int MESSAGE = 5;
private final int REPONSE_BODY = 6;
private final int MEDIA_TYPE = 7;
private final int SETN_REQUEST_AT_MILLIS = 8;
private final int RECEIVE_REPONSE_AT_MILLIS = 9;
private final int CACHE_LENGTH = 10;
@Override
public Response intercept(Interceptor.Chain chain) throws IOException {
if (!isNeedCache(chain.request().url().toString())) {
return chain.proceed(chain.request());
}
//获取缓存
String key = createKey(chain.request());
LogUtil.d("cache key: " + key);
Response cacheResponse = null;
String cacheRes = cache != null&&key!=null
? cache.getAsString(key)
: null;
if (!TextUtils.isEmpty(cacheRes)) {
LogUtil.d("cacheRes: " + cacheRes);
cacheResponse = combineCacheToResponse(cacheRes);
}
//没有网络连接的时候读取缓存
if (!NetStateUtils.isNetWorkConnected()) {
LogUtil.d("no network connected jujge cache available");
if (cacheResponse != null) {
LogUtil.d("no network connected, return cache: " + cacheResponse);
return cacheResponse;
}
}
LogUtil.d("waiting for network response...");
//获取网络响应
Request netWorkRequest = chain.request();
Response networkResponse = null;
try {
networkResponse = chain.proceed(netWorkRequest);
} finally {
if (networkResponse == null) {
LogUtil.d("close cache response...");
if (cacheResponse!=null&&HttpHeaders.hasBody(cacheResponse)){
closeQuietly(cacheResponse.body());
}
return chain.proceed(netWorkRequest);
}
}
LogUtil.d("prepare update cache response...");
//更新缓存
if (cacheResponse != null) {
Response response = null;
response = networkResponse.newBuilder()
.request(new Request.Builder()
.method("GET", null)
.url(netWorkRequest.url())
.headers(netWorkRequest.headers())
.tag(netWorkRequest.tag())
.build())
.build();
LogUtil.d("update cache response");
if (key!=null){
cache.put(key, createCache(response));
}
if (cacheResponse!=null&&HttpHeaders.hasBody(cacheResponse)){
closeQuietly(cacheResponse.body());
}
return networkResponse;
}
Request newRequest = new Request.Builder()
.method("GET", null)
.url(netWorkRequest.url())
.headers(netWorkRequest.headers())
.tag(netWorkRequest.tag())
.build();
Response newResponse = networkResponse.newBuilder()
.request(newRequest)
.build();
LogUtil.d("init cache response");
//放入缓存
if (cache != null) {
LogUtil.d("url: " + netWorkRequest.url().toString());
if (HttpHeaders.hasBody(newResponse)) {
try {
LogUtil.d("chain request url: " + newResponse.request().url());
if (key!=null){
cache.put(key, createCache(newResponse));
LogUtil.d("put cache response key: " + key);
}
// String resp1 = cache.getAsString(key);
// LogUtil.d("resp1: " + resp1);
return networkResponse;
} catch (Exception e) {
LogUtil.d("put cache exception: " + e);
}finally {
if (cacheResponse != null && HttpHeaders.hasBody(cacheResponse)) {
closeQuietly(cacheResponse.body());
}
}
}
}
return networkResponse;
}
private String createKey(Request request) {
RequestBody requestBody = request.body();
Charset charset = Charset.forName("UTF-8");
String url = request.url().toString();
StringBuilder sb = new StringBuilder();
sb.append(url + "&");
MediaType type = requestBody.contentType();
if (type != null) {
charset = type.charset() == null ? charset : type.charset();
}
Buffer buffer = new Buffer();
try {
requestBody.writeTo(buffer);
sb.append(buffer.readString(charset));
} catch (Exception e) {
LogUtil.d("read request error: " + e);
} finally {
buffer.close();
}
if (url.startsWith(BuildConfig.SERVER_URL + "your own url")) {
return //这里可以根据url来定制化key
}
return sb.toString();
}
//根据键返回索引
private int[] getIndexofKeyValue(String str, String originStr) {
int[] indexs = new int[2];
indexs[0] = originStr.indexOf(str);
indexs[1] = originStr.indexOf("&", indexs[0]) >= 0 ? originStr.indexOf("&", indexs[0]) : originStr.length();
LogUtil.d("index0: " + indexs[0] + " index1: " + indexs[1]);
return indexs;
}
private boolean isNeedCache(String url) {
//这里可以根据Url来判断是否需要缓存
}
private Response combineCacheToResponse(String cache) {
String[] caches = cache.split("");
if (caches == null || caches.length <= 0) {
return null;
}
Request request = new Request.Builder()
.url(caches[REQUEST_URL])
.method(caches[REQUEST_METHOD], null)
.build();
Response.Builder builder = new Response.Builder();
try {
builder.protocol(Protocol.get(caches[PROTOCAL]));
} catch (IOException e) {
e.printStackTrace();
}
return builder.message(caches[MESSAGE])
.code(Integer.valueOf(caches[CODE]))
.request(request)
.receivedResponseAtMillis(Long.valueOf(caches[RECEIVE_REPONSE_AT_MILLIS]))
.sentRequestAtMillis(Long.valueOf(caches[SETN_REQUEST_AT_MILLIS]))
.body(ResponseBody.create(MediaType.parse(caches[MEDIA_TYPE]), caches[REPONSE_BODY]))
.build();
}
private String createCache(Response response) {
String[] caches = new String[CACHE_LENGTH];
caches[REQUEST_URL] = response.request().url().toString();
caches[REQUEST_METHOD] = response.request().method();
if (response.request().body() != null && response.request().body().contentType() != null) {
caches[REQUESTCONTENTTYPE] = response.request().body().contentType().toString();
} else {
caches[REQUESTCONTENTTYPE] = "application/x-www-form-urlencoded";
}
caches[PROTOCAL] = response.protocol().toString();
caches[CODE] = response.code() + "";
caches[MESSAGE] = response.message();
if (response.body() != null && response.body().contentType() != null) {
caches[MEDIA_TYPE] = response.body().contentType().toString();
} else {
caches[MEDIA_TYPE] = "application/x-www-form-urlencoded";
}
caches[SETN_REQUEST_AT_MILLIS] = response.sentRequestAtMillis() + "";
caches[RECEIVE_REPONSE_AT_MILLIS] = response.receivedResponseAtMillis() + "";
if (HttpHeaders.hasBody(response)) {
BufferedSource source = response.body().source();
Buffer buffer = null;
try {
source.request(Long.MAX_VALUE);
buffer = source.buffer();
Charset charset = response.body().contentType().charset();
if (charset == null) {
charset = Charset.forName("UTF-8");
}
caches[REPONSE_BODY] = buffer.clone().readString(charset);
} catch (IOException e) {
e.printStackTrace();
}finally {
// closeQuietly(response.body());
}
}
String cache = "";
for (String str : caches) {
cache += str + "";
}
return cache;
}
static boolean isEndToEnd(String fieldName) {
return !"Connection".equalsIgnoreCase(fieldName)
&& !"Keep-Alive".equalsIgnoreCase(fieldName)
&& !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
&& !"Proxy-Authorization".equalsIgnoreCase(fieldName)
&& !"TE".equalsIgnoreCase(fieldName)
&& !"Trailers".equalsIgnoreCase(fieldName)
&& !"Transfer-Encoding".equalsIgnoreCase(fieldName)
&& !"Upgrade".equalsIgnoreCase(fieldName);
}
private String subString(String str, int[] index) {
if (index == null || index.length < 2) {
return null;
}
if (index[0] < 0 || index[1] < 0) {
return null;
}
return str.substring(index[0], index[1]);
}
}