一步一步扩展 Volley (二),缓存生存时间 TTL

之前根据 Volley 的工作流程大致解析了它的框架,接下来继续分析 Volley 的缓存机制,下面我在解析其缓存机制的同时也讲述一下个人对缓存生存时间的扩展.

缓存调度器的处理流程

要明白 Volley 的缓存机制,关键在于分析缓存调度器处理请求的过程,下面是我画的流程图:

这张流程图主要对应 CacheDispatcher 的 run 方法,首先根据 cachekey 查找缓存,如果没有缓存就把请求放到网络等待队列中,缓存调度器继续处理下一个请求;如果有缓存,但是缓存已经过期了,也将请求放到网络等待队列中并处理下一个请求,如果缓存没有过期,继续判断缓存是否需要刷新;如果不需要更新缓存,则直接传递解析后的缓存数据到主线程,如果需要更新缓存,则在传递解析后的缓存数据到主线程之后还会把请求添加到网络等待队列。

下面再看下流程中的两个关键的方法的源码:

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
/**
* Data and metadata for an entry returned by the cache.
*/
public static class Entry {
/** The data returned from cache. */
public byte[] data;
/** ETag for cache coherency. */
public String etag;
/** Date of this response as reported by the server. */
public long serverDate;
/** The last modified date for the requested object. */
public long lastModified;
/** TTL for this record. */
public long ttl;
/** Soft TTL for this record. */
public long softTtl;
/** Immutable response headers as received from server; must be non-null. */
public Map<String, String> responseHeaders = Collections.emptyMap();
/** True if the entry is expired. */
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/** True if a refresh is needed from the original data source. */
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}

从上面的代码中可以看出 ttl 就是缓存的生存时间,如果当前时间大于这个时间,缓存就过期,需要重新从网络请求数据。而 softTtl 代表软生存时间,过了这个时间缓存没有完全过期,可以返回给主线程,但是需要从网络请求新的数据刷新缓存.

Response 数据转换为缓存的逻辑

网络调度器成功请求到服务器数据后,会调用HttpHeaderParser的静态方法parseCacheHeaders解析成缓存。大致的逻辑如下:

a. 如果 Header 的 Cache-Control 字段中有no-cacheno-store字段,则返回 null

b. 否则根据 Cache-Control 和 Expires 首部,计算 ttl 和 softTtl

c. 根据 Date 首部,获得服务器响应时间

d. 根据其他信息,生成缓存其他信息

缓存逻辑的扩展

上面解析 response 数据为缓存在请求的shouldCache为 false 的时候是可以不需要转换的,但是网络调度器现在的逻辑是先解析 response 数据并转换缓存,再判断shouldCache和缓存是否为空,若需要缓存且缓存不为空才会写入缓存。所以这个地方可以做个小小的优化.

另外在使用 volley 的过程中,例如图片请求,如果服务器返回的 Header 的 Cache-Control 字段中有no-cacheno-store字段,或者没有 Expires 首部,就是出现缓存为空的或者 ttl 为 0 的情况。这样的话每次都要去重新请求图片,但是实际上这是不必要的,另外有些服务器返回的 Header 不规范也会出现这个问题。所以我觉得应该在这个时候使用默认的缓存生存时间,这样就可以避免这个问题,也方便客户端自己做一些缓存的控制。

扩展方法是在Request类中加上 defaultTtl 和 defaultSoftTtl 两个属性,并改写解析成缓存的方法,下面是我改写后的parseCacheHeaders方法:

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
*
* @param response The network response to parse headers from
*
* @param defaultTtl The TTL of the cache, it is useful only when {value > 0} and finalExpire = 0
* @param defaultSoftTtl The Soft TTL of the cache, it is useful only when {value > 0} and softExpire = 0
* @return a cache entry for the given response, or null if the response is not cacheable.
*/
public static Cache.Entry parseCacheHeaders(NetworkResponse response, boolean shouldCache, long defaultTtl, long defaultSoftTtl) {
// return null if shouldCache is false, whether response has Cache-Control or not.
if(!shouldCache) {
return null;
}
long now = System.currentTimeMillis();
Map<String, String> headers = response.headers;
long serverDate = 0;
long lastModified = 0;
long serverExpires = 0;
long softExpire = 0;
long finalExpire = 0;
long maxAge = 0;
long staleWhileRevalidate = 0;
boolean hasCacheControl = false;
boolean mustRevalidate = false;
String serverEtag = null;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = parseDateAsEpoch(headerValue);
}
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.equals("no-cache") || token.equals("no-store")) {
// Use ttl as cache's expire time when Cache-Control is no-cache.
if(defaultTtl > 0) {
break;
}else {
return null;
}
} else if (token.startsWith("max-age=")) {
try {
maxAge = Long.parseLong(token.substring(8));
} catch (Exception e) {
}
} else if (token.startsWith("stale-while-revalidate=")) {
try {
staleWhileRevalidate = Long.parseLong(token.substring(23));
} catch (Exception e) {
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
mustRevalidate = true;
}
}
}
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
headerValue = headers.get("Last-Modified");
if (headerValue != null) {
lastModified = parseDateAsEpoch(headerValue);
}
serverEtag = headers.get("ETag");
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
// is more restrictive.
if (hasCacheControl) {
softExpire = now + maxAge * 1000;
finalExpire = mustRevalidate
? softExpire
: softExpire + staleWhileRevalidate * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
// Default semantic for Expire header in HTTP specification is softExpire.
softExpire = now + (serverExpires - serverDate);
finalExpire = softExpire;
} else if (defaultTtl > 0) {
// There not exist expire header in HTTP specification, so use ttl param.
softExpire = defaultSoftTtl > 0 ? (now + defaultSoftTtl) : softExpire;
finalExpire = now + defaultTtl;
}
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
entry.ttl = finalExpire;
entry.serverDate = serverDate;
entry.lastModified = lastModified;
entry.responseHeaders = headers;
return entry;
}

这篇文章就分析缓存的一些处理逻辑,下篇文章讲述一下使用DiskLruCache作为磁盘缓存.

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!