HTTP -- 缓存机制

一、概念

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);
        }
    };
}

相关链接

彻底弄懂HTTP缓存机制及原理
HTTP协议进阶之缓存

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335