一、概念
HTTP缓存(特指客户端缓存)是客户端向服务器发起HTTP网络请求时,客户端在本地磁盘内保存的资源副本,当客户端再次向服务器发起HTTP网络请求时,客户端可直接使用缓存,因而减少了客户端对服务器的访问次数,并节省了通信流量和通信时间。
二、缓存规则
在客户端已经存在缓存的情况下,根据客户端是否需要重新向服务器发起网络请求,可以将缓存分为两类:强制缓存与对比缓存。
强制缓存与对比缓存比较
- 强制缓存如果生效,不再需要和服务器发生交互,而对比缓存不管是否生效,都需要和服务端发生交互。
- 两类缓存可以同时存在,强制缓存优先级高于对比缓存,当执行强制缓存时,如果强制缓存生效,则直接使用缓存,不再执行对比缓存,如果强制缓存失效,则执行比较缓存。
三、与缓存相关的HTTP头字段
当客户端向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则包含在响应头字段中。
举例:
https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1558669494488&di=09aa7382a311f5edc8ef4bc8a712575b&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201512%2F19%2F20151219100834_aHVRh.jpeg
date: Fri, 24 May 2019 01:49:28
expires: Sun, 20 May 2029 09:17:37 GMT
cache-control: max-age=315360000
last-modified: Tue, 30 May 2017 17:07:11
etag: "1A0711C1D9AED7C0BED240AE6B8354D8“
…
与强制缓存相关的HTTP头字段
- 响应头字段expires
属于HTTP 1.0字段,指定的值为服务器返回的资源到期时间,即客户端下一次请求时,如果手机系统时间小于服务器返回的到期时间,直接使用缓存数据。 - 响应头字段cache-control
属于HTTP 1.1字段,常见用法如下:
cache-control: max-age=xxx,缓存数据将在xxx秒后失效,即客户端下一次请求时,如果手机系统时间小于(响应头字段date指定的值 + cache-control: max-age指定的值 - 响应头字段age指定的值(如果该响应头字段存在的话)),直接使用缓存数据。
cache-control: no-cache,客户端可以缓存数据,但是当客户端下一次请求时,需要先使用对比缓存来验证缓存数据的有效性。
cache-control: no-store,客户端不可以缓存数据。 - 请求头字段cache-control
属于HTTP 1.1字段,常见用法如下:
cache-control: max-age=xxx,指定的值为缓存的最大生存时间,只有当缓存的当前生存时间小于该值时,才可以使用缓存。缓存的当前生存时间计算方式:当前手机系统时间 - 响应头字段date指定的值 + 响应头字段age指定的值(如果该响应头字段存在的话)。
cache-control: no-cache,忽略客户端缓存,直接发起网络请求,获取响应后会将响应进行缓存。
cache-control: no-store,忽略客户端缓存,直接发起网络请求,获取响应后不会将响应进行缓存。
cache-control: max-stale,缓存过期后在指定时间内仍然可以直接使用。
cache-control: min-fresh,缓存过期前在指定时间内不可以直接使用。
cache-control: only-if-cached,只使用缓存,即使未命中也不发起网络请求。
与对比缓存相关的HTTP头字段
- 响应头字段last-modified
服务器响应请求时,告诉客户端资源的最后修改时间。当客户端执行对比缓存时,请求头需要带上if-modified-since字段,其指定的值为响应头返回的last-modified字段指定的值。 - 请求头字段if-modified-since
服务器接收到请求后发现有if-modified-since字段,则将该字段指定的值与被请求资源的最后修改时间进行比较,如果资源的最后修改时间大于该值,说明资源有被修改过,服务器返回新数据(状态码200),如果资源的最后修改时间小于等于该值,说明资源无被修改过,服务器不返回新数据(状态码304)。 - 响应头字段etag
服务器响应请求时,告诉客户端返回资源在服务器的唯一标识(生成规则由服务器决定)。当客户端执行对比缓存时,请求头需要带上if-none-match字段,其指定的值为响应头返回的etag字段指定的值。 - 请求头字段If-none-match
服务器接收到请求后发现有If-none-match字段,则将该字段指定的值与被请求资源的唯一标识进行比较,如果资源的唯一标识与该值不同,说明资源有被修改过,服务器返回新数据(状态码200),如果资源的唯一标识与该值不同,说明资源无被修改过,服务器不返回新数据(状态码304)。
四、HTTP缓存整体流程
五、OkHttp缓存应用
OkHttp简单使用
Cache cache = new Cache(cacheFile, cacheSize);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache) //开启缓存功能
.build();
Request request = new Request.Builder()
.url(url)
//.cacheControl(CacheControl.FORCE_CACHE)
//.cacheControl(CacheControl.FORCE_NETWORK)
.cacheControl(cacheControl)
.build();
Call call = okHttpClient.newCall(request);
Response response = call.execute();
CacheControl取值:
1. CacheControl.FORCE_NETWORK
new Builder().noCache().build(); //指定cache-control: no-cache
2. CacheControl.FORCE_CACHE
new Builder() //指定cache-control: max-stale=2147483647, only-if-cached
.onlyIfCached()
.maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
.build();
3.cacheControl
new CacheControl.Builder()
//.noCache() //指定cache-control: no-cache
//.noStore() //指定cache-control: no-store
//.maxStale(60, TimeUnit.SECONDS) //指定cache-control: max-stale=60
//.minFresh(60, TimeUnit.SECONDS) //指定cache-control: min-fresh=60
//.onlyIfCached() //指定cache-control: only-if-cached
.maxAge(360, TimeUnit.SECONDS) //指定cache-control: max-age=360
OkHttp测试代码
//1.Gradle
dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.2.0'
}
//2.AndroidManifest
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
//3.MainActivity
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private String mUrl = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1558669494488&di=09aa7382a311f5edc8ef4bc8a712575b&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201512%2F19%2F20151219100834_aHVRh.jpeg";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testCache();
}
private void testCache(){
Log.d(TAG, "zwm, testCache");
//缓存文件夹
File cacheFile = new File(getExternalCacheDir().toString(),"zwm");
//缓存大小为10M
int cacheSize = 10 * 1024 * 1024;
//创建缓存对象
final Cache cache = new Cache(cacheFile,cacheSize);
final CacheControl cacheControl = new CacheControl.Builder()
//.noCache() //请求头配置Cache-Control: no-cache,忽略客户端缓存,直接发起网络请求,获取响应后会将响应进行缓存
//.noStore() //请求头配置Cache-Control: no-store,忽略客户端缓存,直接发起网络请求,获取响应后不会将响应进行缓存
.maxAge(360, TimeUnit.SECONDS) //请求头配置Cache-Control: max-age,指定的值为缓存的最大生存时间,只有缓存的当前的生存时间小于该值时,才可以使用缓存。缓存的当前的生存时间计算方式:客户端的请求时间(例如手机系统时间)- 响应头字段date + 响应头字段age(如果该字段存在的话)
.build();
new Thread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "zwm, Thread run");
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new CacheInterceptor()) //添加网络拦截器
.cache(cache) //开启缓存功能
.build();
String url = mUrl;
Request request = new Request.Builder()
.url(url)
//.cacheControl(CacheControl.FORCE_CACHE) //强制使用缓存
//.cacheControl(CacheControl.FORCE_NETWORK) //强制使用网络
.cacheControl(cacheControl) //进行缓存控制
.build();
Log.d(TAG, "zwm, request headers: \n" + request.headers()); //打印请求头
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute(); //执行网络请求
Log.d(TAG, "zwm, cacheResponse: " + response.cacheResponse()); //判断响应是否来自缓存
Log.d(TAG, "zwm, networkResponse: " + response.networkResponse()); //判断响应是否来自网络
Log.d(TAG, "zwm, response headers: \n" + response.headers()); //打印响应头
response.body().close();
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, "zwm, IOException");
}
}
}).start();
}
private static class CacheInterceptor implements Interceptor { //定义网络拦截器
@Override
public Response intercept(Chain chain) throws IOException {
Log.d(TAG, "zwm, CacheInterceptor, intercept called");
Response originResponse = chain.proceed(chain.request());
Log.d(TAG, "zwm, CacheInterceptor, chain proceed done");
return originResponse.newBuilder()
//.header("cache-control", "no-cache") //客户端可以缓存,但是下一次请求时需要先校验缓存数据的有效性
//.header("cache-control", "no-store") //客户端不可以缓存
.removeHeader("pragma") //移除pragma字段。pragma字段的常用方法为pragma: no-cache,可以应用到http 1.0 和http 1.1,而cache-control: no-cache只能应用于http 1.1
.removeHeader("age") //移除age字段。age字段指定的值表示当前获取到响应的时间跟服务器创建该响应的时间的差值,即该响应已经生存的时间(该响应在客户端发起网络请求前已经存在于服务器中)
.header("cache-control", "max-age=3600") //指定缓存有效时间,以秒为单位,缓存到期时间计算方式:date + max-age。如果age字段存在,则cache-control: max-age指定的值必须大于age指定的值,才可以使用缓存,此时缓存到期时间计算方式:date + (max-age - age)
//.header("age", "300") //指定当前获取到响应时,该响应已经生存的时间(该响应在客户端发起网络请求前已经存在于服务器中)
.build();
}
}
}
//4.输出Log
//第一次网络请求
2019-05-24 18:50:44.435 10305-10305/com.tomorrow.testnetworkcache D/MainActivity: zwm, testCache
2019-05-24 18:50:44.452 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, Thread run
2019-05-24 18:50:44.728 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, request headers:
Cache-Control: max-age=360
2019-05-24 18:50:44.848 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, CacheInterceptor, intercept called
2019-05-24 18:50:44.877 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, CacheInterceptor, chain proceed done
2019-05-24 18:50:44.884 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, cacheResponse: null
2019-05-24 18:50:44.884 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, networkResponse: Response{protocol=h2, code=200, message=, url=https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1558669494488&di=09aa7382a311f5edc8ef4bc8a712575b&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201512%2F19%2F20151219100834_aHVRh.jpeg}
2019-05-24 18:50:44.885 10305-10324/com.tomorrow.testnetworkcache D/MainActivity: zwm, response headers:
server: JSP3/2.0.14
date: Fri, 24 May 2019 10:51:04 GMT
content-type: image/jpeg
content-length: 14581
etag: "1A0711C1D9AED7C0BED240AE6B8354D8"
last-modified: Tue, 30 May 2017 17:07:11 GMT
expires: Sun, 20 May 2029 09:17:37 GMT
accept-ranges: bytes
image-center-request-id: 14bdbc55fee422d93e3d8147f06b353d
x-img-generate-time: 1558603057
x-img-original-height: 660
x-img-original-size: 23721
x-img-original-width: 440
x-img-thumnail-height: 660
x-img-thumnail-size: 14581
x-img-thumnail-width: 440
ohc-response-time: 1 0 0 0 0 0
OkHttp-Sent-Millis: 1558695044848
OkHttp-Received-Millis: 1558695044876
cache-control: max-age=3600
//第二次网络请求
2019-05-24 18:51:35.610 10305-10305/com.tomorrow.testnetworkcache D/MainActivity: zwm, testCache
2019-05-24 18:51:35.613 10305-10531/com.tomorrow.testnetworkcache D/MainActivity: zwm, Thread run
2019-05-24 18:51:35.667 10305-10531/com.tomorrow.testnetworkcache D/MainActivity: zwm, request headers:
Cache-Control: max-age=360
2019-05-24 18:51:35.681 10305-10531/com.tomorrow.testnetworkcache D/MainActivity: zwm, cacheResponse: Response{protocol=http/1.1, code=200, message=, url=https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1558669494488&di=09aa7382a311f5edc8ef4bc8a712575b&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201512%2F19%2F20151219100834_aHVRh.jpeg}
2019-05-24 18:51:35.681 10305-10531/com.tomorrow.testnetworkcache D/MainActivity: zwm, networkResponse: null
2019-05-24 18:51:35.681 10305-10531/com.tomorrow.testnetworkcache D/MainActivity: zwm, response headers:
server: JSP3/2.0.14
date: Fri, 24 May 2019 10:51:04 GMT
content-type: image/jpeg
content-length: 14581
etag: "1A0711C1D9AED7C0BED240AE6B8354D8"
last-modified: Tue, 30 May 2017 17:07:11 GMT
expires: Sun, 20 May 2029 09:17:37 GMT
accept-ranges: bytes
image-center-request-id: 14bdbc55fee422d93e3d8147f06b353d
x-img-generate-time: 1558603057
x-img-original-height: 660
x-img-original-size: 23721
x-img-original-width: 440
x-img-thumnail-height: 660
x-img-thumnail-size: 14581
x-img-thumnail-width: 440
ohc-response-time: 1 0 0 0 0 0
OkHttp-Sent-Millis: 1558695044848
OkHttp-Received-Millis: 1558695044876
cache-control: max-age=3600
六、WebView缓存应用
public void loadUrl(String url)
作用:加载网页
加载模式:
- LOAD_DEFAULT
遵循HTTP缓存策略,根据响应头字段expires、cache-control、last-modified、etag等决定是否使用缓存。 - LOAD_NO_CACHE
设置请求头字段pragma: no-cache、cache-control: no-cache,即不使用缓存。 - LOAD_CACHE_ELSE_NETWORK
只要有缓存,无论是否过期,都使用缓存,如无缓存则发起网络请求。 - LOAD_CACHE_ONLY
只使用缓存,不发起网络请求,如无缓存则报net::ERR_CACHE_MISS。
public void reload()
作用:
如果之前调用了loadUrl(String url)加载网页,那么可以调用reload()执行对比缓存,进行刷新页面。
原理:
设置请求头字段cache-control: max-age=0、if-none-match、if-modified-since进行缓存有效性验证,如果缓存有效则返回状态码304,否则返回状态码200及新数据。
注意:
如果当前缓存模式为LOAD_CACHE_ONLY、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE,那么调用reload()方法将不能执行对比缓存,需要先设置缓存模式为LOAD_DEFAULT,然后调用reload()才能正常工作。
add header
使用:
Map<String, String> map = new HashMap<>();
map.put("cache-control", "max-age=0");
map.put("custom-header", "test");
mWebView.loadUrl(mUrl, map);
问题:
当添加cache-control: max-age=60时,发现在缓存的当前生存时间已经超过60秒的情况下仍然直接使用缓存,不执行对比缓存。
WebView测试代码
//1.AndroidManifest
<uses-permission android:name="android.permission.INTERNET"/>
//2.MainActivity
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private String mUrl = "http://s3.cn-north-1.amazonaws.com.cn/seb-prod-cn-samsungassistant.com/mall/1629_jPaQPo5gCV5IW1cy1551405310030.jpg";
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testWebView();
}
private void testWebView() {
mWebView = findViewById(R.id.webview);
WebSettings localWebSettings = mWebView.getSettings();
localWebSettings.setJavaScriptEnabled(true);
localWebSettings.setUseWideViewPort(true);
localWebSettings.setLoadWithOverviewMode(true);
localWebSettings.setGeolocationEnabled(true);
localWebSettings.setGeolocationDatabasePath(this.getFilesDir().getPath());
localWebSettings.setDatabaseEnabled(true);
localWebSettings.setDomStorageEnabled(true);
localWebSettings.setAllowContentAccess(true);
localWebSettings.setAllowFileAccess(true);
localWebSettings.setAllowFileAccessFromFileURLs(true);
localWebSettings.setAllowUniversalAccessFromFileURLs(true);
localWebSettings.setJavaScriptCanOpenWindowsAutomatically(true);
localWebSettings.setTextSize(WebSettings.TextSize.NORMAL);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, true);
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
localWebSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
String version = "";
try {
PackageManager packageManager = this.getPackageManager();
version = packageManager.getPackageInfo(this.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException ignored) {
}
String userAgent = localWebSettings.getUserAgentString() + " SamsungLifeService/" + version;
localWebSettings.setUserAgentString(userAgent);
mWebView.setWebViewClient(mWebViewClient);
mWebView.setWebChromeClient(mWebChromClient);
Log.d(TAG, "zwm, LOAD_DEFAULT: -1, LOAD_NO_CACHE: 2, LOAD_CACHE_ELSE_NETWORK: 1, LOAD_CACHE_ONLY: 3");
mWebView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
Log.d(TAG, "zwm, loadUrl, CacheMode: " + mWebView.getSettings().getCacheMode());
mWebView.loadUrl(mUrl);
//Map<String, String> map = new HashMap<>();
//map.put("cache-control", "max-age=0"); //添加请求头字段
//map.put("custom-header", "test"); //添加自定义请求头字段
//mWebView.loadUrl(mUrl, map);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mWebView.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT); //需要先设置缓存模式为LOAD_DEFAULT,然后调用reload()才能正常工作
Log.d(TAG, "zwm, reload, CacheMode: " + mWebView.getSettings().getCacheMode());
mWebView.reload();
}
}, 5000);
}
private WebViewClient mWebViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Log.d(TAG, "zwm, shouldOverrideUrlLoading, url: " + url);
return true;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Log.d(TAG, "zwm, shouldOverrideUrlLoading, url2: " + request.getUrl());
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.d(TAG, "zwm, onPageStarted, url: " + url);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
Log.d(TAG, "zwm, onPageFinished, url: " + url);
super.onPageFinished(view, url);
}
@Override
public void onLoadResource(WebView view, String url) {
Log.d(TAG, "zwm, onLoadResource, url: " + url);
super.onLoadResource(view, url);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
Log.d(TAG, "zwm, shouldInterceptRequest, url: " + request.getUrl());
return super.shouldInterceptRequest(view, request);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
Log.d(TAG, "zwm, onReceivedError, error: " + error.getDescription());
super.onReceivedError(view, request, error);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
Log.d(TAG, "zwm, onReceivedHttpError, errorResponse: " + errorResponse.getReasonPhrase());
super.onReceivedHttpError(view, request, errorResponse);
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {
Log.d(TAG, "zwm, doUpdateVisitedHistory, url: " + url + ", isReload: " + isReload);
super.doUpdateVisitedHistory(view, url, isReload);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
Log.d(TAG, "zwm, onReceivedSslError, error: " + error.getPrimaryError());
handler.proceed();
}
};
private WebChromeClient mWebChromClient = new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
Log.d(TAG, "zwm, onProgressChanged, newProgress: " + newProgress);
super.onProgressChanged(view, newProgress);
}
@Override
public void onReceivedTitle(WebView view, String title) {
Log.d(TAG, "zwm, onReceivedTitle, title: " + title);
super.onReceivedTitle(view, title);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
Log.d(TAG, "zwm, onJsAlert");
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
Log.d(TAG, "zwm, onJsConfirm");
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Log.d(TAG, "zwm, onJsPrompt");
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public void onConsoleMessage(String message, int lineNumber, String sourceID) {
Log.d(TAG, "zwm, onConsoleMessage, message: " + message);
super.onConsoleMessage(message, lineNumber, sourceID);
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
Log.d(TAG, "zwm, onConsoleMessage, consoleMessage: " + consoleMessage.message());
return super.onConsoleMessage(consoleMessage);
}
};
}