前言:
HTTP缓存策略,可以有效的减少重复请求,降低服务器后台压力,和减少客户端的流量带宽。
不了解HTTP缓存策略,建议先阅读Android HTTP 缓存策略(用于检查磁盘数据是否过期)。
服务器端返回的HTTP缓存策略
找到NetworkDispatcher类:查看执行HTTP请求后的解析,缓存操作。
public class NetworkDispatcher extends Thread {
@Override
public void run() {
//....省略部分代码
//在网络线程中指向解析响应的数据
Response<?> response = request.parseNetworkResponse(networkResponse);
//若是需要缓存,则将解析后数据写入缓存中。
//注意点:在304s情况下(即内容数据相同时),只会更新缓存的metadata, 不会更新内容数据。
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
}
}
}
从以上代码可以知道,执行Request的parseNetworkResponse()
解析网络数据,生成一个Cache.Entry,然后写入磁盘中。
先来了解Request的parseNetworkResponse()
.
这里,找到一个Request的子类StringRequest:
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
以上代码,是解析成指定返回类型的数据,和解析header。
找到HttpHeaderParser类,查看解析Header的方法:
public class HttpHeaderParser {
/**
* 从NetworkRespone 生成一个Cache.Entry,即将http缓存机制信息写入Entry中
*/
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
//当前时间
long now = System.currentTimeMillis();
//服务器返回的标头
Map<String, String> headers = response.headers;
long serverDate = 0;
long serverExpires = 0;
long softExpire = 0;
long maxAge = 0;
boolean hasCacheControl = false;
String serverEtag = null;
String headerValue;
//服务器响应的时间
headerValue = headers.get("Date");
if (headerValue != null) {
//获取到long类型的服务器响应时间
serverDate = parseDateAsEpoch(headerValue);
}
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.equals("no-cache") || token.equals("no-store")) {
//数据处于安全问题,不缓存磁盘
return null;
} else if (token.startsWith("max-age=")) {
try {
maxAge = Long.parseLong(token.substring(8));
} catch (Exception e) {
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
maxAge = 0;
}
}
}
//服务器返回的数据过期
headerValue = headers.get("Expires");
if (headerValue != null) {
//服务器返回的数据过期时间
serverExpires = parseDateAsEpoch(headerValue);
}
serverEtag = headers.get("ETag");
//Cache-Control优先于Expire
if (hasCacheControl) { //Cache-Control标头存在的情况
//过期时间=(当前时间+缓存的有效时间*1000)
softExpire = now + maxAge * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) { //Cache-Control标头不存在的情况
//在Http规范中Expire标头的语义是softExpire.
//过期时间=现在时间+(服务器返回数据的过期时间-服务器响应时间)
softExpire = now + (serverExpires - serverDate);
}
Cache.Entry entry = new Cache.Entry();
//Response中body信息
entry.data = response.data;
// eTag
entry.etag = serverEtag;
//缓存时间
entry.softTtl = softExpire;
//过期时间
entry.ttl = entry.softTtl;
//服务器响应时间
entry.serverDate = serverDate;
//Response中全部Header信息
entry.responseHeaders = headers;
return entry;
}
/**
* 按RFC1123格式解析时间,返回long类型的date
*/
public static long parseDateAsEpoch(String dateStr) {
try {
DateUtils.parseDate(dateStr).getTime();
} catch (DateParseException e) {
return 0;
}
}
}
从以上代码可知,解析HTTP缓存策略中的过期时间,缓存时间,etag,服务器响应时间等等。
找到Cache.Entry类: 用于保存Response数据
public static class Entry {
/** 从缓存中的数据 */
public byte[] data;
/** ETag 用于缓存一致性. */
public String etag;
/** 从服务器响应的时间 */
public long serverDate;
/** TTL 过期时间 */
public long ttl;
/** Soft TTL 缓存时间 */
public long softTtl;
/** 从服务器上响应的header,默认不为空 */
public Map<String, String> responseHeaders = Collections.emptyMap();
/**
* 返回true,实体已经过期,必须重新获取。
**/
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/**
* 返回true,数据不在缓存期间,任可以展示,但需要从原始数据源中刷新。
* */
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
用于判断是否请求最新数据的判断情况:
- 当前时间大于过期时间,需要重新从服务器获取新数据。
- 当前时间小于过期时间,且当前时间大于过期时间,可以加载缓存数据,但必须去请求最新的数据,最新数据可能发生改变。
- 当前时间小于缓存时间,本地数据可以直接使用。
检查HTTP缓存策略,是否在缓存期间,是否过期
找到CacheDispatcher类:
public class CacheDispatcher extends Thread {
@Override
public void run() {
//从磁盘中获取该请求需要的数据,若是没有则加入网络队列中,执行网络操作。
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
//磁盘中无缓存数据,添加到网络请求中,执行网络获取数据
mNetworkQueue.put(request);
continue;
}
//若是这缓存数据已经过期,则进行网络获取新数据
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
//确保缓存一致性,使用从缓存中读取到的entry.
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
//解析从磁盘中读取到数据
Response<?> response = request.parseNetworkResponse(new NetworkResponse(entry.data, entry.responseHeaders));
//数据是否需要刷新
if (!entry.refreshNeeded()) {
//数据还在缓存期间中,响应动作在ExecutorDelivery中,将结果回调到主线程中
mDelivery.postResponse(request, response);
} else {
/**
* 缓存数据过了缓存时间,但没有过期,则先回调加载磁盘数据
* ,然后执行网络操作,刷新最新数据,二次回调响应。
*/
request.setCacheEntry(entry);
//标记响应是中间,还会继续刷新
response.intermediate = true;
//传递一个中间的响应回调到监听器中,同时也会传递一个请求去执行网络工作。
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
}
}
});
}
}
从以上代码可知:
- 先从缓存队列中获取到请求
- 从磁盘中获取数据,若是为空,将该请求加入网络队列中,执行网络操作。
- 反之,获取缓存数据,若是过期,则将该请求加入网络队列中,执行网络操作。
- 反之,获取的缓存数据没有过期,若是该数据还在缓存期间,则传递响应数据。
- 反之,该数据不在缓存期间,则先传递响应数据,且开启网络线程去刷新,二次传递新数据。
客户端的HTTP缓存策略
找到BasicNetwork类:该类执行网络线程中,用于操作HTTP请求
public class BasicNetwork implements Network {
//....省略部分代码
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
// 创一个Map来,存储上一次相同缓存key所对应请求的标头
Map<String, String> headers = new HashMap<String, String>();
//因磁盘中缓存的数据,已经过期,需要重新执行网络数据,而添加磁盘中缓存数据的标头
addCacheHeaders(headers, request.getCacheEntry());
//在HurlStack 中,执行HttpURLConnection,返回响应数据
httpResponse = mHttpStack.performRequest(request, headers);
//....省略部分代码
}
/**
* 添加磁盘中缓存数据的一些标头,若是没有缓存,不需要任何操作。
* <p>
* 1. If-None-Match
* 2. If-Modified-Since
*/
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
//
if (entry == null) {
return;
}
if (entry.etag != null) {//添加是否匹配的标头
headers.put("If-None-Match", entry.etag);
}
if (entry.serverDate > 0) {//
Date refTime = new Date(entry.serverDate);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
}
从以上代码,可知将本地HTTP缓存策略的表头信息发送给服务器。
实战案例
自定义一个3分钟缓存,3小时过期的圆形图片的请求CircleBitmapImageRequest。
public class CircleBitmapImageRequest extends ImageRequest {
private static final android.graphics.Bitmap.Config bitmapConfig = Bitmap.Config.RGB_565;
private static final Handler mainHandler = new Handler(Looper.getMainLooper());
private final BitmapResultListener resultListener;
public CircleBitmapImageRequest(String url, ImageView imageView, BitmapResultListener resultListener) {
this(url, imageView.getWidth(), imageView.getHeight(), bitmapConfig, resultListener);
}
public CircleBitmapImageRequest(String url, int maxWidth, int maxHeight, BitmapResultListener resultListener) {
this(url, maxWidth, maxHeight, bitmapConfig, resultListener);
}
public CircleBitmapImageRequest(String url, int maxWidth, int maxHeight, Bitmap.Config decodeConfig, BitmapResultListener resultListener) {
super(url, resultListener, maxWidth, maxHeight, decodeConfig, resultListener);
this.resultListener = resultListener;
loadPreviewBitmap();
}
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
Response<Bitmap> bitmapResponse= super.parseNetworkResponse(response);
if (bitmapResponse.isSuccess()){
//重新添加具备缓存期间和过期时间的header
bitmapResponse =Response.success(bitmapResponse.result,HttpResponseHeaderParser.parseSpecifiedTimeCacheHeaders(response));
}
return bitmapResponse;
}
@Override
protected void deliverResponse(Bitmap response) {
// circle crop bitmap
Bitmap circleBitmap = BitmapUtils.circleCrop(response);
if (resultListener!=null){
resultListener.onResponse(circleBitmap);
}
}
private void loadPreviewBitmap() {
if (resultListener != null) {
mainHandler.post(new Runnable() {
@Override
public void run() {
resultListener.loadPreviewBitmap();
}
});
}
}
}
重写缓存策略:
public class HttpResponseHeaderParser extends HttpHeaderParser {
/**
* 忽视`Cache-control`表头,默认指定缓存时间,SoftTtl == 3 mins, ttl == 24 hours
*
* @param response
* @return
*/
public static Cache.Entry parseSpecifiedTimeCacheHeaders(NetworkResponse response) {
Cache.Entry entry = parseCacheHeaders(response);
long now = System.currentTimeMillis();
//3分钟内,会缓存中,过完会刷新
final long cacheHitButRefreshed = 3 * 60 * 1000;
// in 12 hours this cache entry expires completely
final long cacheExpired = 3 * 60 * 60 * 1000;
//截止的缓存时间
final long softExpire = now + cacheHitButRefreshed;
//过期时间
final long ttl = now + cacheExpired;
entry.softTtl = softExpire;
entry.ttl = ttl;
return entry;
}
}
资源参考: