原创生活

国内 商业 滚动

基金 金融 股票

期货金融

科技 行业 房产

银行 公司 消费

生活滚动

保险 海外 观察

财经 生活 期货

当前位置:滚动 >

Retrofit 和OkHttp 中使用网络缓存,提高访问效率

文章来源:财金网  发布时间: 2019-04-23 10:53:32  责任编辑:cfenews.com
+|-

【原标题:Retrofit 和OkHttp 中使用网络缓存,提高访问效率】财金网消息 OkHttp缓存原理

我们先从HTTP协议开始入手,关于缓存的HTTP请求/返回头有以下几个,我列了张表格一一解释.

请求头/返回头含义

Cache-Control这个字段用于指定所有缓存机制在整个请求/响应链中

必须服从的指令。

Pragma与Cache-Control一样,是兼容HTTP1.0的头部

Expires资源过期时间

Last-Modified资源最后修改的时间

If-Modified-Since在请求头中指定一个日期,若资源最后更新时间超过该日期,

则服务器接受请求,相反的头为If-Unmodified-Since

ETag识别内容版本的唯一字符串,与资源关联的记号

与缓存最相关的Cache-Control有多条指令,并且在请求或返回头中的效果不一样.

在请求头中Cache-Control的指令

指令参数说明

no-cache无缓存必须向服务器确认是否过期候才能使用,

即不接受过期缓存,并非不缓存

no-store无真正意义上的不缓存

max-age=[秒]必须响应的最大age值

max-stale=[秒]可忽略可接受的最大过期时间

min-fresh=[秒]必须询问再过[秒]时间后资源是否过期,若过期则不返回

only-if-cached无只获取缓存的资源而不联网获取

在返回头中Cache-Control的指令

指令参数说明

public无可向任意方提供响应的缓存

private无向特定用户提供响应缓存

no-cache可省略不缓存

no-store无不缓存

max-age=[秒]必须响应的最大age值

max-stale=[秒]可忽略可接受的最大过期时间

min-fresh=[秒]必须询问再过[秒]时间后资源是否过期,若过期则不返回

only-if-cached无只获取缓存的资源而不联网获取

假设Okhttp完全遵守HTTP协议(实际上应该也是),利用Cache-Control我们可以缓存某些必要的资源.

有网络的时候:短时间内频繁的请求,后面的请求使用缓存中的资源.

无网络的时候:获取之前缓存的数据进行暂时的页面显示,当网络更新时对当前activity的数据进行刷新,刷新界面,避免界面空白的场景.

编写OkHttp网络拦截器

class CacheNetworkInterceptor implements Interceptor {

public Response intercept(Interceptor.Chain chain) throws IOException {

//无缓存,进行缓存

return chain.proceed(chain.request()).newBuilder()

.removeHeader("Pragma")

//对请求进行最大60秒的缓存

.addHeader("Cache-Control", "max-age=60")

.build();

}

}

static class CacheInterceptor implements Interceptor {

public Response intercept(Interceptor.Chain chain) throws IOException {

Response resp;

Request req;

if (ok) {

//有网络,检查10秒内的缓存

req = chain.request()

.newBuilder()

.cacheControl(new CacheControl

.Builder()

.maxAge(10, TimeUnit.SECONDS)

.build())

.build();

} else {

//无网络,检查30天内的缓存,即使是过期的缓存

req = chain.request().newBuilder()

.cacheControl(new CacheControl.Builder()

.onlyIfCached()

.maxStale(30, TimeUnit.DAYS)

.build())

.build();

}

resp = chain.proceed(req);

return resp.newBuilder().build();

}

}

配置OKHTTP中的Cache

int cacheSize = 10 * 1024 * 1024; // 10 MiB

Cache cache = new Cache(httpCacheDirectory, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()

.cache(cache)

//加入拦截器,注意Network与非Network的区别

.addInterceptor(new CacheInterceptor())

.addNetworkInterceptor(new CacheNetworkInterceptor())

.connectTimeout(10, TimeUnit.SECONDS)

.readTimeout(10, TimeUnit.SECONDS)

.build();

//最后通过使用该HTTP Client进行网络请求, 就实现上述利用缓存优化应用的需求

在retrofit中使用只要将retrofit的okhttpclient换成这个带缓存的okhttpclient即可

private val okhttpClient = OkHttpClient.Builder()

.connectTimeout(timeout, TimeUnit.MILLISECONDS)

.readTimeout(timeout, TimeUnit.MILLISECONDS)

.writeTimeout(timeout, TimeUnit.MILLISECONDS)

.retryOnConnectionFailure(true)

.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))

.addInterceptor(CacheInterceptor())

.addNetworkInterceptor(CacheNetworkInterceptor())

.cache(Cache(File(App.app.externalCacheDir, "ok-cache"), 1024 * 1024 * 30L))

.build()

var retrofit2 = Retrofit.Builder().baseUrl(baseURL)

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

.addConverterFactory(GsonConverterFactory.create())

.client(okhttpClient)

.build()

解释一下上面的代码,CacheInterceptor主要的作用是判断当前网络是否有效,如果有效,则创建一个请求.

该请求能获取一个10秒内未过期的缓存,否则强制获取一个缓存(过期了30天也允许).而CacheNetworkInterceptor主要是在缓存没命中的情况下,请求网络后,修改返回头,加上Cache-Control,告知OKHTTP对该请求进行一个60秒的缓存.

因此,当频繁请求的时候,OKHTTP使用10秒之内的缓存而不重复请求网络.当没网络的时候,请求会获取30天内的缓存,避免界面白屏.

OkHttp关于Cache的源码分析

分析源码之前先看下Cache的策略

Cache.png

Cache.png

Response getResponseWithInterceptorChain() throws IOException {

// Okhttp获取Response的入口

// 采用责任链模式,一层层按顺序转交Request并处理Response

List interceptors = new ArrayList<>();

// 用户定义的拦截器

interceptors.addAll(client.interceptors());

interceptors.add(retryAndFollowUpInterceptor);

interceptors.add(new BridgeInterceptor(client.cookieJar()));

//CacheInterceptor主要用于做缓存控制

interceptors.add(new CacheInterceptor(client.internalCache()));

interceptors.add(new ConnectInterceptor(client));

if (!forWebSocket) {

//用户定义的Network拦截器

interceptors.addAll(client.networkInterceptors());

}

// 发起实际请求的拦截器

interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,

originalRequest, this, eventListener, client.connectTimeoutMillis(),

client.readTimeoutMillis(), client.writeTimeoutMillis());

return chain.proceed(originalRequest);

}

这里我们主要看CacheInterceptor的实现,CacheInterceptor代码比较长,我们分段来解释:

@Override public Response intercept(Chain chain) throws IOException {

Response cacheCandidate = cache != null

? cache.get(chain.request())

: null;

// 实际上是类似map,将返回内容的URL的MD5的值当key,返回内容当response

// 然后从cache文件里面查询是否存在该缓存

long now = System.currentTimeMillis();

//根据当前的时间,以及缓存策略,来获取response

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

Request networkRequest = strategy.networkRequest;

Response cacheResponse = strategy.cacheResponse;

// 根据策略得到cacheReposne 与 NetworkRequest

// 之后的代码就是根据这两个东西设置返回头

// 不进行网络请求,且缓存以及过期了,返回504错误

if (networkRequest == null && cacheResponse == null) {

return new Response.Builder()

.request(chain.request())

.protocol(Protocol.HTTP_1_1)

.code(504)

.message("Unsatisfiable Request (only-if-cached)")

.body(Util.EMPTY_RESPONSE)

.sentRequestAtMillis(-1L)

.receivedResponseAtMillis(System.currentTimeMillis())

.build();

}

// 不进行网络请求,此时缓存命中,直接返回缓存,后面的拦截器也不会调用了

if (networkRequest == null) {

return cacheResponse.newBuilder()

.cacheResponse(stripBody(cacheResponse))

.build();

}

// 否则需要请求网络,继续调用责任链后面的拦截器,请求网络并获取response

Response networkResponse = null;

try {

networkResponse = chain.proceed(networkRequest);

} finally {

// 请求异常,关闭缓存避免泄漏

if (networkResponse == null && cacheCandidate != null) {

closeQuietly(cacheCandidate.body());

}

}

// 请求了网络的同时,缓存其实也找到的情况

// (比如 需要向服务器确认缓存是否可用的情况)

if (cacheResponse != null) {

// 返回了304, 我们都知道304的返回时不带body的,此时必须向获取cache的body

if (networkResponse.code() == HTTP_NOT_MODIFIED) {

Response response = cacheResponse.newBuilder()

.headers(combine(cacheResponse.headers(), networkResponse.headers()))

.sentRequestAtMillis(networkResponse.sentRequestAtMillis())

.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())

.cacheResponse(stripBody(cacheResponse))

.networkResponse(stripBody(networkResponse))

.build();

networkResponse.body().close();

// Update the cache after combining headers but before stripping the

// Content-Encoding header (as performed by initContentStream()).

cache.trackConditionalCacheHit();

cache.update(cacheResponse, response);

return response;

} else {

closeQuietly(cacheResponse.body());

}

}

//省略---------

}

// 缓存策略CacheStrategy主要的策略写在该方法下

private CacheStrategy getCandidate() {

// 没有缓存!

if (cacheResponse == null) {

return new CacheStrategy(request, null);

}

// 当请求的协议是https的时候,如果cache没有hansake就丢弃缓存

if (request.isHttps() && cacheResponse.handshake() == null) {

return new CacheStrategy(request, null);

}

/// -- 省略一些代码

// 根据缓存的缓存时间,缓存可接受最大过期时间等等HTTP协议上的规范

// 来判断缓存是否可用,

if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {

Response.Builder builder = cacheResponse.newBuilder();

if (ageMillis + minFreshMillis >= freshMillis) {

builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");

}

long oneDayMillis = 24 * 60 * 60 * 1000L;

if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {

builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");

}

return new CacheStrategy(null, builder.build());

}

}

// 请求条件, 当etag,lastModified,servedDate这三种属性存在时

//需要向服务器确认缓存的有效性

String conditionName;

String conditionValue;

if (etag != null) {

conditionName = "If-None-Match";

conditionValue = etag;

} else if (lastModified != null) {

conditionName = "If-Modified-Since";

conditionValue = lastModifiedString;

} else if (servedDate != null) {

conditionName = "If-Modified-Since";

conditionValue = servedDateString;

} else {

return new CacheStrategy(request, null); // 不存在的时候,按流程进行请求

}

Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();

Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

// 构造一个请求询问服务器资源是否过期

Request conditionalRequest = request.newBuilder()

.headers(conditionalRequestHeaders.build())

.build();

return new CacheStrategy(conditionalRequest, cacheResponse);

借用一张图来说明http的整个工作流程

流程也很清晰明了了,简单的说及时通过Request创建RealCall对象,经过层层interceptor之后最终产生一个response.

不过值得注意的是,当CacheInterceptor命中缓存之后, 后面的拦截器将不再执行.

这也是addInterceptor 与 addNetworkInterceptor之间的区别.

最后附上当网络可用的时候,自动重新请求的一个基于MVP模式的实现方案

NetStatusMonitor是一个单例,用于监听整个应用程序的网络状态.

ActivityManager也是一个单例,用来管理应用程序的活动栈,原理Application注册关于活动的生命周期监听.

基于MVP模式,给presenter的抽象基类定义一个refresh的方法,当断网时间超过XX秒的时候,调用在栈顶的activity的presenter进行刷新页面.

如有不足请各位大佬指正:

NetStatusMonitor.setNetStatusListener(object: NetStatusMonitor.Listener {

var lostTime = 0L

override fun onLost() {

lostTime = System.currentTimeMillis()

}

override fun onAvailable() {

with(ActivityManager.peek() as BaseView<*>){

//当栈顶活动位于前台

if(this.lifecycle.currentState == Lifecycle.State.RESUMED){

// 获取ForegroundActivity进行刷新

// 断线时间超过30秒重连再刷新一次

if(System.currentTimeMillis() - lostTime > 1000 * 30){

// 通知presenter刷新数据

this.presenter.refresh()

}

}

}

}

override fun onNetStateChange(oldState: Int, newState: Int) {

if(newState == NetStatusMonitor.MOBILE){

showToast("正在使用移动网络")

}

}

})

object NetStatusMonitor {

interface Listener{

fun onLost()

fun onAvailable()

fun onNetStateChange(oldState: Int, newState: Int)

}

val WIFI = 1;

val MOBILE = 2;

val WIFI_MOBILE = 3;

val UNKNOW = 0

var available = false

var netState: Int by Delegates.observable(UNKNOW) { property, oldValue, newValue ->

listener?.onNetStateChange(oldValue, newValue)

}

private var listener : Listener? = null

fun setNetStatusListener(listener: Listener){

this.listener = listener

}

init {

val cm = Utils.app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

fun setType() {

val activeNetwork = cm.activeNetworkInfo

val isMobile = activeNetwork.type == ConnectivityManager.TYPE_MOBILE

val isWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isAvailable

if (isWifi && isMobile)

netState = WIFI_MOBILE

else if (isWifi && !isMobile)

netState = WIFI

else if (isMobile && !isWifi)

netState = MOBILE

else

netState = UNKNOW

}

cm.requestNetwork(NetworkRequest.Builder().build(), object : ConnectivityManager.NetworkCallback() {

override fun onAvailable(network: Network?) {

available = true

setType()

listener?.onAvailable()

}

override fun onLost(network: Network?) {

available = false

listener?.onLost()

}

})

}

}

专题首页|财金网首页

原创
新闻

精彩
互动

独家
观察

京ICP备2021034106号-38   营业执照公示信息  财金网  版权所有  cfenews.com  投稿邮箱:362293157@qq.com  业务QQ:362293157立即发帖