Volley默认请求多次原因解析

今天在提交数据到后台接口时遇到一个奇怪问题,服务端同学通过后台log发现偶尔会打印两次一模一样的请求,时间间隔一般只有几秒钟。刚开始我感觉应该是在用户点击完Button之后没有把Button的状态置为false,在等待服务端返回数据期间用户又点击了一次Button,后来查看代码,发现在onClick()里已经调用了setEnabled(false),并且在收到服务器响应后就会立即跳转Fragment,所以个人感觉不存在多次点击Button的情况。为了定位问题,在onClick()里生成了一个唯一的UUID,当做请求的参数带上,问题复现后,查看服务端log发现接收到的UUID竟然是一样的,这就验证了自己的想法,即:多次请求并不是多次点击Button导致的。那么就只有一个可能了,就是一个请求被发送了两次。我们用的网络请求库是Volley,为了追踪问题,想起了那句经典的RTFSC(read the fucking source code)。

Volley的网络请求都是调用的BasicNetwork的performRequest()方法获取服务器响应。

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
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = Collections.emptyMap();
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
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);
}
// 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 the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
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) {
int statusCode;
if (httpResponse != null) {
statusCode = httpResponse.getStatusLine().getStatusCode();
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
NetworkResponse networkResponse;
if (responseContents != null) {
networkResponse = new NetworkResponse(statusCode, responseContents,
responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else if (statusCode >= 400 && statusCode <= 499) {
// Don't retry other client errors.
throw new ClientError(networkResponse);
} else if (statusCode >= 500 && statusCode <= 599) {
if (request.shouldRetryServerErrors()) {
attemptRetryOnException("server",
request, new ServerError(networkResponse));
} else {
throw new ServerError(networkResponse);
}
} else {
// 3xx? No reason to retry.
throw new ServerError(networkResponse);
}
} else {
attemptRetryOnException("network", request, new NetworkError());
}
}
}
}

代码有点长,但文档注释很详细,逻辑很清晰。大致的过程为:添加header到request–>发送http–>解析response–>返回。这个请求是循环执行的,直到收到服务端响应,或者抛出异常。在异常处理中,看到有一个attemptRetryOnException()方法,突然想起了以前阅读Volley源码时依稀记得Volley默认有重试的机制,顿时感觉豁然开朗,赶紧看一下这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void attemptRetryOnException(String logPrefix, Request<?> request,
VolleyError exception) throws VolleyError {
RetryPolicy retryPolicy = request.getRetryPolicy();
int oldTimeout = request.getTimeoutMs();
try {
retryPolicy.retry(exception);
} catch (VolleyError e) {
request.addMarker(
String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
throw e;
}
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
}

方法很短,从Request中取出RetryPolicy对象,然后调用它的retry()方法,如果该方法抛出异常,performRequest()方法会终止执行且向上抛出异常,如果retry()没抛异常,则performRequest()方法继续while(true)的死循环,所以问题的关键就是分析RetryPolicy的retry()方法了。那这个RetryPolicy是什么时候设置到Request的呢?看一下setRetryPolicy()的调用。

1
2
3
4
5
6
7
8
public Request(int method, String url, Response.Listener listener) {
mMethod = method;
mUrl = url;
mListener = listener;
setRetryPolicy(new DefaultRetryPolicy());
mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
}

在Request的构造里设置了一个DefaultRetryPolicy对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** The default socket timeout in milliseconds */
public static final int DEFAULT_TIMEOUT_MS = 2500;
/** The default number of retries */
public static final int DEFAULT_MAX_RETRIES = 1;
/** The default backoff multiplier */
public static final float DEFAULT_BACKOFF_MULT = 1f;
/**
* Constructs a new retry policy using the default timeouts.
*/
public DefaultRetryPolicy() {
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
}

那么看一下它的retry()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++;
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (!hasAttemptRemaining()) {
throw error;
}
}
/**
* Returns true if this policy has attempts remaining, false otherwise.
*/
protected boolean hasAttemptRemaining() {
return mCurrentRetryCount <= mMaxNumRetries;
}

关键逻辑hasAttempRemaining()方法,先把当前请求次数mCurrentRetryCount++,然后和设置的最大请求次数mMaxNumRetries比较,意思是当前尝试请求的次数大于最大请求数就抛出异常。而最大请求次数的默认值DEFAULT_MAX_RETRIES = 1。

到这里,一切就清晰了。由于Volley默认设置的超时时间比较短,只有2.5秒,这在天朝这种复杂的网络环境下简直是不可想象的,请求发送的数据量稍微大一点,或者2G、移动3G这种龟速网下,或者服务器处理速度慢一点,很容易发生TimeOut。当请求发出后,2.5秒没收到服务器的响应(有可能服务器已经收到了请求,在做逻辑处理,还没返回结果),则客户端抛出TimeOut异常,失败一次后,进入DefaultRetryPolicy的retry(),判断当前请求的次数(1) <= 默认的最大请求次数(1),所以不抛出异常,回到BasicNetwork的performRequest()方法继续执行while(true)循环,即再发一次request请求。

知道了原因,修复起来就很简单了,增加默认的超时时间或者设置本次提交数据的请求不再使用重试策略。

1
request.setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));