OkHttp缓存使用

缓存可以不用每次请求时都去服务器拉取数据,可以快速响应数据,提升用户体验,并且还能节省流量。

相关知识

ETag

资源的特定版本的标识符。在ETag和If-Match头部的帮助下,可以检测到"空中碰撞"的编辑冲突。客户端将ETag作为If-None-Match字段的值一起发送给服务器并与服务器当前版本的资源的ETag进行比较,如果两个值匹配(即资源未更改),客户端继续使用缓存,如果两个值不匹配,则表示资源有更新,客户端需要更新缓存的资源。

  • ETag: W/"<etag_value>"
    ‘W/’(大小写敏感) 表示使用弱验证器。弱验证器容易生成,但不利于比较。强验证器是比较的理想选择,但很难有效地生成。相同资源的两个弱Etag值可能语义等同,但不是每个字节都相同
  • ETag: “<etag_value>”
    实体标签唯一地表示所请求的资源

Last-Modified

响应头部,其中包含服务器的资源修改的日期及时间。通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。由于精确度比 ETag 要低,所以这是一个备用机制。

  • Last-Modified: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
    • <day-name>
      “Mon”, “Tue”, “Wed”, “Thu”, “Fri”, “Sat” 或 “Sun” 之一 (区分大小写)
    • <day>
      两位数字表示的天数, 例如"04" or “23”
    • <month>
      “Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec” 之一(区分大小写)
    • <year>
      4位数字表示的年份, 例如 “1990” 或者"2016"
    • <hour>>
      两位数字表示的小时数, 例如 “09” 或者 “23”
    • <minute>
      两位数字表示的分钟数,例如"04" 或者 “59”
    • <second>
      两位数字表示的秒数,例如 “04” 或者 “59”
    • GMT
      国际标准时间。HTTP中的时间均用国际标准时间表示,不使用当地时间

If-None-Match

条件式请求头部。对于GET和HEAD请求,当且仅当服务器上没有任何资源的ETag属性值与这个头部中列出的值相匹配的时候,服务器才返回所请求的资源,响应码为 200 。对于其他请求方法,当且仅当最终确认没有已存在的资源的 ETag属性值与这个头部中所列出的相匹配的时候,才会对请求进行相应的处理。

ETag属性之间的比较采用的是弱比较算法,即两个文件除了每个比特都相同外,内容一致也可以认为是相同的。例如,如果两个页面仅仅在页脚的生成时间有所不同,就可以认为二者是相同的。

  • If-None-Match: <etag_value>
  • If-None-Match: <etag_value>, <etag_value>, …
  • If-None-Match: *
    星号是一个特殊值,可以代表任意资源。只用在进行资源上传时,通常是采用PUT方法,来检测拥有相同ID的资源是否已经上传过

Cache-Control

通用消息头字段,被用于在http请求和响应中,通过指定指令来实现缓存机制。缓存指令是单向的,这意味着在请求中设置的指令,不一定被包含在响应中。

请求指令
  • Cache-Control: max-age=<seconds>
    设置缓存到期时间(秒),超过时间客户端必须重新请求数据刷新缓存
  • Cache-Control: max-stale[=<seconds>]
    表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示缓存过期以后继续使用的时间不能超过该给定时间(也就是说整个缓存时间其实是max-age+max-stale)
  • Cache-Control: min-fresh=<seconds>
    表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应
  • Cache-control: no-cache
    可以进行缓存,但每次发请求时,都要向服务器进行验证,如果服务器允许,才能使用本地缓存。客户端都必须连接到服务器,并将缓存资源的 ETag 与服务器上的 ETag 进行比较。如果 ETag 相同,则向用户提供缓存。否则表示资源已经更新,客户端需要进行更新缓存
  • Cache-control: no-store
    缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存
  • Cache-control: no-transform
    不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等HTTP头不能由代理修改
  • Cache-control: only-if-cached
    客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝
响应指令
  • Cache-control: must-revalidate
    一旦资源过期(比如已经超过max-age),在通过服务器验证之前,缓存不能用
  • Cache-control: no-cache
  • Cache-control: no-store
  • Cache-control: no-transform
  • Cache-control: public
    表示请求返回的内容所经过的任何路径(包括:发送请求的客户端,代理服务器,等等)中都可以对响应进行缓存
  • Cache-control: private
    表示响应只能被发起请求的客户端缓存,不能作为共享缓存(即代理服务器不能缓存它)
  • Cache-control: proxy-revalidate
    与must-revalidate作用相同,但它只适用于共享缓存(例如代理服务器),并被private私有缓存忽略
  • Cache-Control: max-age=<seconds>
  • Cache-control: s-maxage=<seconds>
    覆盖max-age或者Expires头,但是只适用于共享缓存(比如各个代理),private私有缓存会忽略

If-Match

条件式请求头部。对于GET和HEAD请求,服务器只有在请求的资源满足此头部列出的ETag值时才会返回资源。而对于PUT或其他非安全请求方法来说,只有在满足条件的情况下才可以将资源上传。

ETag 之间的比较使用的是强比较算法,即只有在每一个字节都相同的情况下,才可以认为两个文件是相同的。在ETag前面添加W/ 前缀表示可以采用相对宽松的算法。

  • If-Match: <etag_value>
  • If-Match: <etag_value>, <etag_value>, …

Expires

响应头包含日期/时间, 超过该响应头给定的时间值以后,响应过期。如果Cache-Control响应头设置了max-age或者s-max-age指令,那么 Expires会被忽略

  • Expires: <http-date>

Pragma

  • Pragma: no-cache

与 Cache-Control: no-cache效果一致。强制要求缓存服务器在返回缓存的版本之前将请求提交到源头服务器进行验证。Pragma: no-cache兼容http 1.0 ,Cache-Control: no-cache是http 1.1提供的。

缓存实现

如果服务器支持缓存,则请求返回的响应头会有Cache-Control响应指令。直接给OkHttpClient设置缓存即可。

Cache cache = new Cache(context.getCacheDir(), 10 * 1024 * 1024);
OkHttpClient client = new OkHttpClient.Builder()
        .cache(cache)
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build();

如果服务器不支持缓存,则需要使用Interceptor来重写Respose的头部信息,从而让OkHttp支持缓存。

public class CacheInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        CacheControl cacheControl = new CacheControl.Builder()
                .maxAge(15, TimeUnit.MINUTES)
                .build();
                
        Response cacheResponse = response.newBuilder()
                .removeHeader("Pragma")
                .removeHeader("Cache-Control")
                .header("Cache-Control", cacheControl.toString())
                .build();
        return cacheResponse;
    }
}

然后将该拦截器添加到OkHttpClient的网络拦截器中。

Cache cache = new Cache(getCacheDir(), 10 * 1024 * 1024);
OkHttpClient client = new OkHttpClient.Builder()
        .addNetworkInterceptor(new CacheInterceptor())
        .cache(cache)
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .build();

这样就能让OkHttp支持缓存啦!

自定义缓存实现

上面的实现方式只是基础实现方式,这种实现方式是以URL生成Key来进行DiskLruCache的,而且只限定于Get请求。如果请求URL的参数中有时间戳或者与时间戳有关的参数的化,就会导致缓存失效。对于这种请求,需要自定义缓存实现。

final class CustomCache implements java.io.Closeable, java.io.Flushable {

    private static final int VERSION = 666888;
    private static final int ENTRY_BODY = 0;
    private static final int ENTRY_COUNT = 1;

    private DiskLruCache cache;

    public CustomCache(File file, long maxSize) {
        try {
            cache = DiskLruCache.open(file, VERSION, ENTRY_COUNT, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void put(Response response) {
        String requestMethod = response.request().method();
        if (!requestMethod.equals("GET")) {
            return;
        }

        DiskLruCache.Editor editor;
        try {
            String key = key(response.request().url());
            if (TextUtils.isEmpty(key)) {
                return;
            }

            editor = cache.edit(key);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        Buffer copyBuffer = null;
        try {
            BufferedSource source = response.body().source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.getBuffer();
            copyBuffer = buffer.clone();
            String body = copyBuffer.readUtf8();
            if (!TextUtils.isEmpty(body)) {
                editor.set(ENTRY_BODY, body);
                editor.commit();
            }
        } catch (IOException e) {
            abortQuietly(editor);
            e.printStackTrace();
        } finally {
            if (copyBuffer != null) {
                copyBuffer.close();
            }
        }
    }

    public String get(Request request) {
        try {
            String key = key(request.url());
            if (TextUtils.isEmpty(key)) {
                return null;
            }
            DiskLruCache.Value snapshot = cache.get(key);
            String body = null;
            if (snapshot != null) {
                body = snapshot.getString(ENTRY_BODY);
            }
            return body;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    public void remove(Request request) {
        try {
            String key = key(request.url());
            if (!TextUtils.isEmpty(key)) {
                cache.remove(key);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void remove(String url) {
        try {
            String key = key(url);
            if (!TextUtils.isEmpty(key)) {
                cache.remove(key);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void update(Response response) {
        String requestMethod = response.request().method();
        if (!requestMethod.equals("GET")) {
            return;
        }

        String key = key(response.request().url());
        if (TextUtils.isEmpty(key)) {
            return;
        }

        try {
            cache.remove(key);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        DiskLruCache.Editor editor;
        try {
            editor = cache.edit(key);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        Buffer copyBuffer = null;
        try {
            BufferedSource source = response.body().source();
            source.request(Long.MAX_VALUE);
            Buffer buffer = source.getBuffer();
            copyBuffer = buffer.clone();
            String body = copyBuffer.readUtf8();
            if (!TextUtils.isEmpty(body)) {
                editor.set(ENTRY_BODY, body);
                editor.commit();
            }
        } catch (IOException e) {
            abortQuietly(editor);
            e.printStackTrace();
        } finally {
            if (copyBuffer != null) {
                copyBuffer.close();
            }
        }
    }

    private void abortQuietly(@Nullable DiskLruCache.Editor editor) {
        try {
            if (editor != null) {
                editor.abort();
            }
        } catch (IOException ignored) {
            ignored.printStackTrace();
        }
    }

    public String key(HttpUrl httpUrl) {
        if (httpUrl == null) {
            return null;
        }

        HttpUrl.Builder builder = new HttpUrl.Builder();
        builder.scheme(httpUrl.scheme()).host(httpUrl.host());
        List<String> segments = httpUrl.pathSegments();
        if (segments != null) {
            for (String segment : segments) {
                builder.addPathSegment(segment);
            }
        }
        Set<String> names = httpUrl.queryParameterNames();
        List<String> nameList = new ArrayList<>(names);
        if (nameList.contains("app_version")) {
            nameList.remove("app_version");
        }
        if (nameList.contains("timestamp")) {
            nameList.remove("timestamp");
        }
        for (String name : nameList) {
            builder.addQueryParameter(name, httpUrl.queryParameter(name));
        }
        return ByteString.encodeUtf8(builder.build().toString()).md5().hex();
    }

    @Override
    public void close() throws IOException {
        cache.close();
    }

    @Override
    public void flush() throws IOException {
        cache.flush();
    }
}

上面是通过处理URL去掉可变参数以后剩下的链接部分作为键进行缓存的实现方式。那么该怎样应用到OKhttp中呢?

class CustomCacheInterceptor implements Interceptor {

    private static final String CONTENT_TYPE = "application/json; charset=utf-8";

    private Context context;
    private CustomCache cache;

    public CustomCacheInterceptor(Context context, CustomCache cache) {
        this.context = context;
        this.cache = cache;
    }

    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {
        Request request = chain.request();
        if (!NetworkUtils.checkNetwork(context)) {
            String cacheBody = cache.get(request);
            ResponseBody responseBody = !TextUtils.isEmpty(cacheBody) ? ResponseBody.create(cacheBody, MediaType.parse(CONTENT_TYPE)) : Util.EMPTY_RESPONSE;
            return new Response.Builder()
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_1)
                    .code(200)
                    .message("Return from cache")
                    .body(responseBody)
                    .sentRequestAtMillis(System.currentTimeMillis())
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();
        }

        Response networkResponse = null;
        try {
            networkResponse = chain.proceed(request);
        } finally {
        }

        long sentRequestAtMillis, receivedResponseAtMillis;
        if (networkResponse != null) {
            sentRequestAtMillis = networkResponse.sentRequestAtMillis();
            receivedResponseAtMillis = networkResponse.receivedResponseAtMillis();
            if (networkResponse.isSuccessful()) {
                cache.update(networkResponse);
                return networkResponse;
            }
        } else {
            sentRequestAtMillis = System.currentTimeMillis();
            receivedResponseAtMillis = sentRequestAtMillis;
        }

        String cacheBody = cache.get(request);
        ResponseBody responseBody = !TextUtils.isEmpty(cacheBody) ? ResponseBody.create(cacheBody, MediaType.parse(CONTENT_TYPE)) : Util.EMPTY_RESPONSE;
        return new Response.Builder()
                .request(chain.request())
                .protocol(Protocol.HTTP_1_1)
                .code(200)
                .message("Return from cache")
                .body(responseBody)
                .sentRequestAtMillis(sentRequestAtMillis)
                .receivedResponseAtMillis(receivedResponseAtMillis)
                .build();
    }
}

上面的拦截器只是简单的实现了断网或者网络请求失败的时候使用缓存的情况,可以根据需要自行修改。

最后在构建OkhttpClient的时候加入拦截器就可以实现自定义缓存啦。

CustomCache cache = new CustomCache(new File(getCacheDir(), "cache"), 10 * 1024 * 1024);  //最大缓存10MB
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(new CustomCacheInterceptor(getApplicationContext(), cache))
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(50, TimeUnit.SECONDS)
        .build();

感谢大家的支持,如有错误请指正,如需转载请标明原文出处!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值