okhttp 缓存实践

以下内容基于 okhttp:3.10.0 版本

在开发中,由于不同业务场景解,我们需要将接口返回的数据缓存到本地,以实现复用。例如,接口数据每间隔一定时间才会更新,在时间间隔内就没必要重复的向服务器请求数据,直接使用缓存即可;当 app 无法访问网络时,也可以使用缓存的接口数据,避免缺省页等等。所以使用缓存也是好处多多:节省流量、提高响应速度、增强用户体验......

okhttp 的缓存功能使用起来也比较简单,我们一步步来看:

1、配置缓存

配置缓存首先要指定缓存目录和缓存大小,这两个可以根据项目的需求来确定,然后使用 OkHttpClient..Builder()cache()方法来配置缓存对象。这里的OkHttpClient是一个单例,保证了只有一个缓存缓存目录的入口。配置代码如下:

public class OkHttpManager {
    private OkHttpClient client;

    private OkHttpManager() {
        // 缓存目录
        File file = new File(Environment.getExternalStorageDirectory(), "a_cache");
        // 缓存大小
        int cacheSize = 10 * 1024 * 1024;
        client = new OkHttpClient.Builder()
                .cache(new Cache(file, cacheSize)) // 配置缓存
                .build();
    }

    public static OkHttpManager getInstance() {
        return OkHttpHolder.instance;
    }

    private static class OkHttpHolder {
        private static final OkHttpManager instance = new OkHttpManager();
    }
    ......
}

到这里就完成了基本配置工作,不要忘了处理权限问题,因为缓存功能需要存储空间的读写权限

如果客户端和服务端已经协商好了,在接口的响应包含合适的Cache-Control响应头,表示缓存的策略,例如Cache-Control:max-age=60,表示缓存的有效期为60秒。

这个响应头是实现缓存的一个重点,如果包含合适的Cache-Control响应头,在无论网络连接是否正常的情况下请求接口数据,如果在缓存有效期内则直接从缓存读取数据,超过有效期会重新请求接口数据。

列举几个常用的Cache-control响应头的可选值:

  • must-revalidate,一旦缓存过期,必须向服务器重新请求,不得使用过期内容
  • no-cache,不使用缓存
  • no-store,不缓存请求的响应
  • no-transform,不得对响应进行转换或转变
  • public,任何响应都可以被缓存,即使响应默认是不可缓存或仅私有缓存可存的
  • private,表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)
  • proxy-revalidate,与must-revalidate类似,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。
  • max-age,缓存的有效时间
  • s-maxage,指定响应在公共缓存中的最大存活时间,它覆盖max-age和expires字段。

所以目前的问题是,如果响应不包含合适的Cache-Control响应头,该如何处理,这也是接下来主要讨论的问题。

2、拦截器

由于客户端和服务端不是同一团队,或者客户端使用了第三方接口等原因,无法进行协商,导致接口的响应没有合适的Cache-Control响应头,或者缓存已被禁用。这种情况下要让缓存功能正常工作,就需要使用自定义拦截器了,通过拦截器给请求的响应(Response)添加合适的Cache-Control响应头即可,这样问题就得到了解决!

不了解 okhttp 拦截器的可以先看官网的文档,很详细:https://github.com/square/okhttp/wiki/Interceptors

看一下拦截器如何实现:

public class NetCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response originResponse = chain.proceed(request);

        //设置响应的缓存时间为60秒,即设置Cache-Control头,并移除pragma消息头,因为pragma也是控制缓存的一个消息头属性
        originResponse = originResponse.newBuilder()
                .removeHeader("pragma")
                .header("Cache-Control", "max-age=60")
                .build();

        return originResponse;
    }

拦截请求的响应,先移除pragma,然后手动设置Cache-Control响应头。

把定义好的拦截器添加到OkHttpClient中:

client = new OkHttpClient.Builder()
                .cache(new Cache(file, cacheSize))
                .addNetworkInterceptor(new NetCacheInterceptor())
                .build();

3、测试

封装一个asyncGet()方法来实现异步get请求,http://publicobject.com/helloworld.txt是官方的一个实例地址:

public class OkHttpManager {
    private OkHttpClient client;
    ......
    public void asyncGet(Callback callback) {
        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        client.newCall(request).enqueue(callback);
    }
}

以下是发起请求、以及回调的代码:

OkHttpManager.getInstance().asyncGet(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    Log.e("failure", e.toString());
                }

                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    if (response.isSuccessful()) {
                        if (response.networkResponse() != null) {
                            Log.e("network", response.body().string().length() + "");
                        } else if (response.cacheResponse() != null) {
                            if (Utils.isNetworkAvailable(context)) {
                                Log.e("cache", response.body().string().length() + "");
                            } else {
                                Log.e("cache(no network)", response.body().string().length() + "");
                            }
                        }
                    }
                }
            });

如果响应是从网络请求得到的,那么response.networkResponse()不为空,如果是从缓存中得到的response.cacheResponse()不为空,以此来打印不同 log 观察缓存功能是否能正常工作,这里打印了响应的 boay 长度。

下边是在60秒内发起了若干次请求,即便断开网络连接也能正常的从缓存读取数据,超过60秒会重新请求数据,这也验证了我们的缓存功能可以正常工作了:


缓存功能的具体实现是通过 DiskLruCache 完成的,在之前配置的缓存目录可以找到对应的缓存文件:

4、okhttp 缓存策略

上边在拦截器中统一设置了响应的缓存时间,导致所有的接口数据都会缓存,且时间相同。这样问题就来了,可能不同接口对数据的缓存时间要求不同,或者有些接口并不需要缓存数据。要解决这个问题可以在拦截器中根据请求的地址(request.url().toString())来决定如何设置响应的缓存时间,但不够优雅!除此之外可以使用 okhttp 的缓存策略类CacheControl来处理。

CacheControl类提供了如下两个默认的缓存策略:

  • CacheControl.FORCE_NETWORK,即强制使用网络请求
  • CacheControl.FORCE_CACHE,即强制使用本地缓存,如果无可用缓存则返回一个code为504的响应

根据默认缓存策略的实现方式,我们可以通过CacheControl.Builder()定制自己的缓存策略,可选的设置方法如下:

  • noCache(),不使用缓存,使用网络请求
  • noStore(),不使用缓存也不存储缓存数据
  • maxAge(),缓存的有效时间,超过该时间会重新请求数据
  • maxStale(),超过缓存有效时间后,可继续使用旧缓存的时间,之后需要重新请求数据
  • minFresh(),增加额外的缓存的有效时间,之后需要重新请求数据
  • onlyIfCached(),使用缓存,不使用网络请求
  • noTransform(),不接受经过转码的响应
  • immutable(),缓存有效时间内,响应不会变化,避免服务器处理304响应

了解了这些配置方法后,修改之前的asyncGet()方法,创建一个CacheControl,并添加到Request

public void asyncGet(Callback callback) {
        CacheControl cacheControl = new CacheControl.Builder()
                .maxStale(10, TimeUnit.SECONDS)
                .maxAge(10, TimeUnit.SECONDS)
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .cacheControl(cacheControl)
                .build();

        client.newCall(request).enqueue(callback);
    }

Request添加CacheControl配置,就相当于给给Request添加了对应的Cache-Control请求头!!!

我们设置maxAge为10秒、maxStale为10秒,此时拦截器中设置的Cache-Control响应头还是60秒,测试下效果:


可以看出,当时间间隔大于20秒会重新请求数据,即超过maxAge时间+maxStale时间

我们修改maxAge为100秒再测试下效果:


可以看出,当时间间隔大于70秒会重新请求数据,即Cache-Control响应头时间+maxStale时间

所以当通过CacheControl类设置的缓存时间大于Cache-Control响应头时间,缓存有效时间为Cache-Control响应头时间,否则为CacheControl类设置的缓存时间。

所以我们可以给有需要的接口请求通过CacheControl类设置缓存策略,然后在拦截器中判断请求是否包含Cache-Control请求头,如果有就把Cache-Control请求头添加到响应中去,这样问题就解决了,修改后的拦截器如下:

public class NetCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response originResponse = chain.proceed(request);

        if (!TextUtils.isEmpty(request.header("Cache-Control"))){
            originResponse = originResponse.newBuilder()
                    .removeHeader("pragma")
                    .header("Cache-Control", request.header("Cache-Control"))
                    .build();
        }

        return originResponse;
    }
}

内容就这些了,不合理的地方还望指出。

测试代码地址:https://github.com/SheHuan/SomeTest

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

推荐阅读更多精彩内容