Volley Q&A

1. Volley如何做内存优化的?

有两个很有意思的类ByteArrayPoolPoolingByteArrayOutputStream, 当从网络取得响应数据流后,将响应数据转换为字节数组存储到NetworkResponse中。Volley适合数据量小,通信频繁的网络操作,如果按传统方式申请内存则分配内存,然后等待垃圾收集器回收,必然会造成频繁GC, 导致内存抖动,性能下降。

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
/** com/android/volley/BasicNetwork.java */
/** Reads the contents of HttpEntity into a byte[]. */
private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
PoolingByteArrayOutputStream bytes =
new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
byte[] buffer = null;
try {
InputStream in = entity.getContent();
if (in == null) {
throw new ServerError();
}
buffer = mPool.getBuf(1024);
int count;
while ((count = in.read(buffer)) != -1) {
bytes.write(buffer, 0, count);
}
return bytes.toByteArray();
} finally {
try {
// Close the InputStream and release the resources by "consuming the content".
entity.consumeContent();
} catch (IOException e) {
// This can happen if there was an exception above that left the entity in
// an invalid state.
VolleyLog.v("Error occured when calling consumingContent");
}
mPool.returnBuf(buffer);
bytes.close();
}
}

一种好的使用场景是,像I/O系统那样使用大块临时的字节数组缓存复制数据。在这种情况下,用户通常希望申请一块尽量小的缓冲区来保证性能,但并不关心缓冲区是否比要读取的数据长度大。上面的代码从池中申请了一块1024字节长度的缓冲区用于输入流中的数据写入到buf中,使用完毕将这部分空间返回给池。同样地,PoolingByteArrayOutputStream用于保存写入字节的缓存是随着写入字节增长而增长的,在调用expand方法的时候会将原来的缓存空间加入到池中。同时定义了缓冲池的最大空间,以空间换性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* com/android/volley/PoolingByteArrayOutputStream.java
* Ensures there is enough space in the buffer for the given number of additional bytes.
*/
private void expand(int i) {
/* Can the buffer handle @i more bytes, if not expand it */
if (count + i <= buf.length) {
return;
}
byte[] newbuf = mPool.getBuf((count + i) * 2);
System.arraycopy(buf, 0, newbuf, 0, count);
mPool.returnBuf(buf);
buf = newbuf;
}

2.Volley的缓存机制

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
/**
* com/android/volley/RequestQueue.java
* Staging area for requests that already have a duplicate request in flight.
*
* <ul>
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
* key.</li>
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
* is <em>not</em> contained in that list. Is null if no requests are staged.</li>
* </ul>
*/
private final Map<String, Queue<Request<?>>> mWaitingRequests =
new HashMap<String, Queue<Request<?>>>();

/**
* The set of all requests currently being processed by this RequestQueue. A Request
* will be in this set if it is waiting in any queue or currently being processed by
* any dispatcher.
*/
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

/** The cache triage queue. */
private final PriorityBlockingQueue<Request<?>> mCacheQueue =
new PriorityBlockingQueue<Request<?>>();

/** The queue of requests that are actually going out to the network. */
private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
new PriorityBlockingQueue<Request<?>>();

创建一个RequestQueue的时候会创建一个缓存请求队列,一个网络请求队列,一个等待任务集合和一个当前正在处理的请求的集合。创建5个工作线程,其中一个是CacheDispather, 处理缓存队列中任务,4个是NetworkDispatcher, 处理网络请求队列中的任务。如果一个请求任务需要缓存,就把它加入到缓存请求队列,如果等待任务集合中已经有这个请求,就把它加入到这个集合中相同的等待队列中。当一个任务完成时,检查等待队列中是否有相同的请求,把这些请求加入缓存队列中。这样做的目的是对于连续的相同的请求,提高缓存命中率。加入到缓存队列中的请求,在CacheDispather线程处理时如果没有命中缓存,就将这个任务加入到网络请求队列中去处理。如果命中了缓存,但是缓存过期,就将这个任务加入到网络请求队列中。如果Soft Expire过期,表示快到过期时间了,那么先返回缓存,然后更新缓存内容。

3.如何判断缓存是否过期?

Cache-Control指令

指令 参数 说明
no_cache 可省略 缓存前必须先确认有效性
no_store 不缓存请求或响应的内容
max_age 必需 响应的最大Age值
stale_while_revalidate 允许先返回缓存,再重新刷新
must_revalidate 可缓存但必须向源服务器进行确认
proxy_revalidate 要求中间缓存服务器对缓存的响应有效性再进行确认

解析响应首部,通过指定首部字段Cache-Control, 就能操作缓存的工作机制,指令可选,通过多个”,”分隔,如果指令是no_cache或no_store, 那么不缓存;如果指令是max_age, 记录缓存最大age值,还有stale_while_revalidate, 允许先返回缓存,然后重新请求刷新,隐藏了延迟。此外must_revalidate, proxy_revalidate, 必须向源服务器进行确认。首部Expire获取过期时间,首部Last_Modified获取资源修改时间。优先使用Cache-Control来计算缓存的过期时间。如果一个请求命中了缓存,重新加入到请求队列中的请求就会加上原来请求的一些首部信息。If-None-Match用于指定字段值的实体标记ETag与请求资源的ETag不一致时,告诉服务器处理该请求。If-Modified-Since则会告诉服务器若该字段值早于资源的更新时间,就处理该请求,如果请求的资源没有更新过,返回304 Not Modified的响应。

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
/**
* com/android/volley/toolbox/HttpHeaderParser.java
* Extracts a {@link Cache.Entry} from a {@link NetworkResponse}.
*
* @param response The network response to parse headers from
* @return a cache entry for the given response, or null if the response is not cacheable.
*/
public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
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")) {
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;
}

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;
}

判断缓存是否需要更新,就是根据首部字段的信息判断的。相关方法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 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();
}

4.请求失败如何进行重试?响应状态码非200如何处理

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
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
...
try {
...
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

Entry entry = request.getCacheEntry();
if (entry == null) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
responseHeaders, true,
SystemClock.elapsedRealtime() - requestStart);
}

// A HTTP 304 response does not have all header fields. We
// have to use the header fields from the cache entry plus
// the new ones from the response.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
entry.responseHeaders.putAll(responseHeaders);
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
entry.responseHeaders, true,
SystemClock.elapsedRealtime() - requestStart);
}

// Handle moved resources
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
String newUrl = responseHeaders.get("Location");
request.setRedirectUrl(newUrl);
}

// Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
...
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
SystemClock.elapsedRealtime() - requestStart);
} catch (SocketTimeoutException e) {
attemptRetryOnException("socket", request, new TimeoutError());
} catch (ConnectTimeoutException e) {
attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
...
}
}
}

响应超时、连接超时、I/O异常、认证错误、资源重定向都需要重试,注意到performRequet方法是一个无限循环,用完重试次数仍然失败则抛出VolleyError, 结束循环。注意到针对几种状态码进行了特别处理:
1)304 Not Modified 表示客户端发送附带条件的请求时,服务器端允许访问资源,但因发送请求未满足条件,直接返回304(服务器端资源未改变,可直接使用客户端未过期的缓存)。
2)301 Moved Permanently, 302 Not Found 永久重定向和临时重定向,表示请求的资源以及分配了新的URI, 通过首部字段Location获取新的资源地址。通过设定请求的跳转地址redirectUrl, 下次重试时直接使用新地址访问资源。
3)204 No Content 表示服务端接受的请求已经重新处理,但返回的响应报文中不含实体的主体部分。在这种情况下返回空内容,客户端不做处理。
4)小于200和大于299的状态码均抛出异常。