资讯专栏INFORMATION COLUMN

OkHttp缓存使用指南

lemanli / 1440人阅读

摘要:如表示可接受过期小时内的数据表示指定时间内的缓存数据仍有效,与缓存是否过期无关。所以需要对的缓存过程进行干预,使其满足我们的需求。将对修改和缓存策略的拦截器应用于设置缓存路径和缓存容量接下来就可以在无网络的情况下愉快地使用缓存数据了。

HTTP缓存

在Http协议中,缓存的控制是通过首部的Cache-Control来控制,通过对Cache-Control进行设置,即可实现不同的缓存策略。

Cache-Control和其他的首部字段一样,使用key:value结构,同时value可有多个值, 值之间以,分隔(具体参考HTTP详解)。Cache-Control是一个通用首部字段,在Http请求报文中可使用,也可在应答报文中使用。

请求指令集(在请求报文中的取值):

no-cache: 不要缓存数据,直接从源服务器获取数据;

no-store: 不缓存请求或响应的任何内容;

max-age: 表示可接受过期过久的缓存数据,同指定了参数的max-stale;

max-stale: 表示接收过期的缓存,如后面未指定参数,则表示永远接收缓存数据。如max-stale: 3600, 表示可接受过期1小时内的数据;

min-fresh: 表示指定时间内的缓存数据仍有效,与缓存是否过期无关。如min-fresh: 60, 表示60s内的缓存数据都有效,60s之后的缓存数据将无效。

only-if-cache: 表示直接获取缓存数据,若没有数据返回,则返回504(Gateway Timeout)

应答指令集(在应答报文中的取值):

public: 可向任一方提供缓存数据;

private: 只向指定用户提供缓存数据;

no-cache: 缓存前需确认其有效性;

no-store: 不缓存请求或响应的任何内容;

max-age: 表示缓存的最大时间,在此时间范围内,访问该资源时,直接返回缓存数据。不需要对资源的有效性进行确认;

must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。

更详细内容可参考:Http首部字段定义

了解了HTTP的理论知识,后面我们对OkHttp中的缓存进行简单的介绍。

OkHttp拦截器

OkHttp默认对Http缓存进行了支持,只要服务端返回的Response中含有缓存策略,OkHttp就会通过CacheInterceptor拦截器对其进行缓存。但是OkHttp默认情况下构造的HTTP请求中并没有加Cache-Control,即便服务器支持了,我们还是不能正常使用缓存数据。所以需要对OkHttp的缓存过程进行干预,使其满足我们的需求。

OkHttp的优雅之处就在于使用了责任链模式,将请求-应答过程中的每一步都通过一个拦截器来实现,并对此过程的头部和尾部都提供了扩展,这也为我们干预缓存过程提供了可能。所以在实现缓存之前,我们需要对OkHttp对拦截器的处理过程有个大概的了解。

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest, this, eventListener);
    return chain.proceed(originalRequest);
}

以上代码就是整个拦截器的处理过程,具体的流程可参考源码,这里我们只说一下基本的流程:发起请求时,会按interceptors中加入的顺序依次执行,返回Response时按照逆序执行:

自定义拦截器 <-> 内置拦截器(retryAndFollowUpInterceptor...ConnectInterceptor)
<-> 网络拦截器 <-> CallServerInterceptor

其中CallServerInterceptor就是负责发送请求与接收应答的拦截器。由于我们关注的只是缓存,所以只考虑内置拦截器中的CacheInterceptor。那么流程可简化为:

Request <-> 自定义拦截器 <-> CacheInterceptor <-> 网络拦截器 <-> Response

从这个流程可以看出,如果服务端返回的Response中没有Cache-Control, 那么我们可通过添加网络拦截器来实现。同样,在访问缓存数据时,我们可通过添加自定义拦截器来实现。

使用OkHttp缓存

在开始添加缓存策略之前,我们先了解一个完整的缓存策略:

整体来说,在有网络的情况下,使用缓存还是比较复杂,这里我们通过简化版的缓存策略(有网络时访问服务器,无网络时返回缓存数据)来演示OkHttp使用缓存的过程。

首先,我们通过定义一个网络拦截器来为Response添加缓存策略:

public class HttpCacheInterceptor implements Interceptor {

    private Context context;

    public HttpCacheInterceptor(Context context) {
        this.context = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        return chain.proceed(chain.request()).newBuilder()
            .request(newRequest)
            .removeHeader("Pragma")
            .header("Cache-Control", "public, max-age=" + 1)
            .build();

        return response;
    }
}

其次,通过自定义拦截器设置Request使用缓存的策略:

public class BaseInterceptor implements Interceptor {

    private Context mContext;

    public BaseInterceptor(Context context) {
        this.mContext = context;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
    
        if (NetworkUtil.isConnected(mContext)) {
            return chain.proceed(chain.request());    
        } else { // 如果没有网络,则返回缓存未过期一个月的数据
            Request newRequest = chain.request().newBuilder()
                    .removeHeader("Pragma")
                .header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
            return chain.proceed(newRequest);    
        }
    }
}
Pragma是Http/1.1之前版本遗留的字段,用于做版本兼容,但不同的平台对此有不同的实现,所以在使用缓存策略时需要将其屏蔽,避免对缓存策略造成影响。

将对修改Request和Response缓存策略的拦截器应用于OkHttp:

OkHttpClient httpClient = new OkHttpClient.Builder()
    .addInterceptor(new BaseInterceptor(context))
    .addNetworkInterceptor(new HttpCacheInterceptor(context))
    .cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 设置缓存路径和缓存容量
    .build();
    

接下来就可以在无网络的情况下愉快地使用缓存数据了。

不使用OkHttp的缓存

如果觉得OkHttp的缓存太复杂,想自己来缓存数据怎么办呢?有两种方案来实现:

自定义拦截器,

监听OkHttp的请求过程,在请求完成时缓存数据;

自定义拦截器

这种方案首先需要考虑应使用普通的拦截器还是网络拦截器,上面我们已经了解了整个请求过程中拦截器的执行顺序,需要注意的是:在无网络的情况下,请求在执行到CacheIntercepter,如果没有缓存数据,将会直接返回,并不会执行到自定义的网络拦截器中,所以不适合在网络拦截器中缓存数据。那么我们可通过自定义普通拦截器来实现,基本的过程如下:

@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {

    Response response = null;
    if (NetworkUtil.isConnected(mContext)) {
        response = chain.proceed(newRequest);
        saveCacheData(response); // 保存缓存数据
    } else { // 不执行chain.proceed会打断责任链,即后面的拦截器不会被执行
        response = getCacheData(chain.request().url()); // 获取缓存数据
    }
    
    return response;
}
监听OkHttp的请求过程

OkHttp: 使用这种方案你良心不会痛吗?

这种方案可以说摒弃了OkHttp扩展拦截器这一强大的功能,直接与请求和应答进行交互,基本的过程如下:

Request request = new Request.Builder()
    .url(realUrl)
    .build();
if (NetworkUtil.isConnected()) {
    httpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
            // 返回缓存数据
        }

        @Override
        public void onResponse(Response response) throws IOException {
            // 1. 缓存数据
            // 2. 返回请求结果
        }
    });
} else {
    // 返回缓存数据
}
优缺点比较

这两种方案都抛弃了OkHttp自己实现的缓存策略,所以更加灵活,尤其是监听OkHttp请求过程这种方法。但也都有一个很大的缺点:需要实现一个缓存模块。在开发中具体使用哪种缓存策略,根据已有代码模块和需求衡量即可。

注意点

对Response的缓存策略进行修改的拦截器一定要应用于网络拦截器,否则无法缓存数据,因为在Response返回的过程中,普通的拦截器在内置的CacheInterceptor之后执行;

修改Response的Cache-Control时,max-Age不能太大,否则你将在指定的max-Age时间内访问的始终是缓存数据(即便是有网的情况下);

实际的开发过程中,我们在网络请求中会添加一些公共参数,对于一些可变的公共参数,在缓存数据和访问缓存数据的过程中需要删除,比如网络类型,有网络时其值为Wifi或4G等,无网络时可能为none, 这时访问缓存时就会因url不一致导致访问缓存失败。

@Override // BaseInterceptor.java
public Response intercept(Chain chain) throws IOException {
    // 添加公共参数
    HttpUrl.Builder urlBuilder = chain.request().url().newBuilder()
            .addQueryParameter("a", "a")
            .addQueryParameter("b", "b");
    Request.Builder requestBuilder = chain.request().newBuilder();
    if (NetworkUtil.isConnected(mContext)) {
        urlBuilder.addQueryParameter("network", NetworkUtil.getNetwokType(mContext));
    } else { // 无网络时不添加可变的公共参数
        requestBuilder.removeHeader("Pragma")
                .header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
    }
    Request newRequest = requestBuilder
            .url(urlBuilder.build())
            .build();
            
    return chain.proceed(newRequest);
}


@Override // HttpCacheInterceptor.java
public Response intercept(Chain chain) throws IOException {

    Response response = chain.proceed(chain.request());
    HttpUrl newUrl = chain.request().url().newBuilder()
                .removeAllQueryParameters("network")
                .build(); // 缓存数据前删除可变的公共参数
    Request newRequest = chain.request().newBuilder()
            .url(newUrl)
            .build();
    return response.newBuilder()
            .request(newRequest)
            .removeHeader("Pragma")
            .header("Cache-Control", "public, max-age=" + 1)
            .build();
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/61922.html

相关文章

  • 2019最新Android面试题

    摘要:若拦截事件返回为,表示拦截,事件不会向下层的或者传递,表示不拦截,继续分发事件。五注册反注册未成对使用引起的内存泄漏。七集合对象没有及时清理引起的内存泄漏。 原文链接:https://blog.csdn.net/wen_hah... 版权声明:本文为博主原创文章,转载请附上博文链接! 前言 金三银四到来了,找工作的好时候到了,小伙伴们是不是都在忙着找工作呢,小弟前一阵也是忙着在找工作,...

    plus2047 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<