OkHttp缓存优化你的应用

Author Avatar
weechan 11月 02, 2018

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("正在使用移动网络")
        }
    }
})

`