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我们可以缓存某些必要的资源. 1.有网络的时候:短时间内频繁的请求,后面的请求使用缓存中的资源. 2.无网络的时候:获取之前缓存的数据进行暂时的页面显示,当网络更新时对当前activity的数据进行刷新,刷新界面,避免界面空白的场景.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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.SECONDS) .build()) .build(); } resp = chain.proceed(req); return resp.newBuilder().build(); } } 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进行网络请求, 就实现上述需求
OKHTTP关于Cache的源码分析如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Response getResponseWithInterceptorChain() throws IOException { // Okhttp获取Response的入口 // 采用责任链模式,一层层按顺序转交Request并处理Response List<Interceptor> 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代码比较长,我们分段来解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 @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()); } } //省略--------- }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 缓存策略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()); } }
借用一张图来说明http的整个工作流程
最后附上当网络可用的时候,自动重新请求的一个基于MVP模式的实现方案
NetStatusMonitor是一个单例,用于监听整个应用程序的网络状态 ActivityManager也是一个单例,用来管理应用程序的活动栈 基于MVP模式,给presenter的抽象基类定义一个refresh的方法
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("正在使用移动网络")
}
}
})
`