Volley 相关ToolBox 工具类解析

Volley 的工具类解析

Network

ByteArrayPool

原文解释如下:

1
2
3
4
5
6
7
8
9
10
11
ByteArrayPool is a source and repository of <code>byte[]</code> objects. Its purpose is to supply those buffers to consumers who need to use them for a short period of time and then dispose of them. Simply creating and disposing such buffers in the conventional manner can considerable heap churn and garbage collection delays on Android, which lacks good management of short-lived heap objects. It may be advantageous to trade off some memory in the form of a permanently allocated pool of buffers in order to gain heap performance improvements; that is what this class does.
A good candidate user for this class is something like an I/O system that uses large temporary byte[] buffers to copy data around. In these use cases, often the consumer wants the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into account and also to maximize the odds of being able to reuse a recycled buffer, this class is free to return buffers larger than the requested size. The caller needs to be able to gracefully deal with getting buffers any size over the minimum.
If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this class will allocate a new buffer and return it.
This class has no special ownership of buffers it creates; the caller is free to take a buffer it receives from this pool, use it permanently, and never return it to the pool; additionally,it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there are no other lingering references to it.
This class ensures that the total size of the buffers in its recycling pool never exceeds a certain byte limit. When a buffer is returned that would cause the pool to exceed the limit,least-recently-used buffers are disposed.
// Google 翻译如下
ByteArrayPool是<code> byte [] </ code>对象的源和存储库。其目的是为那些需要在短时间内使用它们然后处理它们的消费者提供这些缓冲区。以常规方式简单地创建和处理这些缓冲区可能会在Android上产生相当大的堆积和垃圾收集延迟,这对于短期堆栈对象缺乏良好的管理。以永久分配的缓冲池的形式来交换一些存储器可能是有利的,以便获得堆性能改进;这就是这个类的作用。
   这个类就像一个使用大型临时byte []缓冲区来复制数据的I / O系统。在这些用例中,消费者通常希望缓冲区具有一定的最小尺寸以确保良好的性能(例如,当从数据流复制数据块时),但不介意缓冲区大于最小值。考虑到这一点,并且最大限度地提高了重新使用再循环缓冲区的可能性,该类可以自由地返回大于请求的大小的缓冲区。调用者需要能够优雅地处理任何尺寸的缓冲区。
  如果在请求缓冲区时,它的回收池中没有适当大小的缓冲区,则此类将分配一个新的缓冲区并返回。
  这个类对它创建的缓冲区没有特殊的所有权;调用者可以自由地从该池接收缓冲区,永久使用它,并且永远不会将其返回到池中;另外,如果没有其他逗号引用它,则将其分配给其他地方的缓冲区返回到此池是没有害处的。
  此类确保其回收池中的缓冲区的总大小永远不会超过某个字节限制。当返回的缓冲区将导致池超过限制时,最近最少使用的缓冲区被排除。

上面已经把 ByteArrayPool 的作用说明白了,下面我们看下代码:

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
public class ByteArrayPool {
/** The buffer pool, arranged both by last use and by buffer size */
private final List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();
private final List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);
/** The total size of the buffers in the pool */
private int mCurrentSize = 0;
/**
* The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay
* under this limit.
*/
private final int mSizeLimit;
/** Compares buffers by size */
protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
@Override
public int compare(byte[] lhs, byte[] rhs) {
return lhs.length - rhs.length;
}
};
/**
* @param sizeLimit the maximum size of the pool, in bytes
*/
public ByteArrayPool(int sizeLimit) {
mSizeLimit = sizeLimit;
}
/**
* Returns a buffer from the pool if one is available in the requested size, or allocates a new one if a pooled one is not available.
* @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be
* larger.
* @return a byte[] buffer is always returned.
*/
public synchronized byte[] getBuf(int len) {
for (int i = 0; i < mBuffersBySize.size(); i++) {
byte[] buf = mBuffersBySize.get(i);
if (buf.length >= len) {
mCurrentSize -= buf.length;
mBuffersBySize.remove(i);
mBuffersByLastUse.remove(buf);
return buf;
}
}
return new byte[len];
}
/**
* Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted size.
* @param buf the buffer to return to the pool.
*/
public synchronized void returnBuf(byte[] buf) {
if (buf == null || buf.length > mSizeLimit) {
return;
}
mBuffersByLastUse.add(buf);
int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
if (pos < 0) {
pos = -pos - 1;
}
mBuffersBySize.add(pos, buf);
mCurrentSize += buf.length;
trim();
}
/**
* Removes buffers from the pool until it is under its size limit.
*/
private synchronized void trim() {
while (mCurrentSize > mSizeLimit) {
byte[] buf = mBuffersByLastUse.remove(0);
mBuffersBySize.remove(buf);
mCurrentSize -= buf.length;
}
}
}

BasicNetwork

网络请求实现类,针对 HttpStack 网络请求做的一层封装,如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/**
* A network performing Volley requests over an {@link HttpStack}.
*/
public class BasicNetwork implements Network {
protected static final boolean DEBUG = VolleyLog.DEBUG;
private static final int SLOW_REQUEST_THRESHOLD_MS = 3000;
private static final int DEFAULT_POOL_SIZE = 4096;
protected final HttpStack mHttpStack;
protected final ByteArrayPool mPool;
/**
* @param httpStack HTTP stack to be used
*/
public BasicNetwork(HttpStack httpStack) {
// If a pool isn't passed in, then build a small default pool that will give us a lot of
// benefit and not use too much memory.
this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
}
/**
* @param httpStack HTTP stack to be used
* @param pool a buffer pool that improves GC performance in copy operations
*/
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
mHttpStack = httpStack;
mPool = pool;
}
@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());
}
}
}
}
/**
* Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete.
*/
private void logSlowRequests(long requestLifetime, Request<?> request,
byte[] responseContents, StatusLine statusLine) {
if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) {
VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " +
"[rc=%d], [retryCount=%s]", request, requestLifetime,
responseContents != null ? responseContents.length : "null",
statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount());
}
}
/**
* Attempts to prepare the request for a retry. If there are no more attempts remaining in the
* request's retry policy, a timeout exception is thrown.
* @param request The request to use.
*/
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));
}
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
// If there's no cache entry, we're done.
if (entry == null) {
return;
}
if (entry.etag != null) {
headers.put("If-None-Match", entry.etag);
}
if (entry.lastModified > 0) {
Date refTime = new Date(entry.lastModified);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
protected void logError(String what, String url, long start) {
long now = SystemClock.elapsedRealtime();
VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url);
}
/** 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 occurred when calling consumingContent");
}
mPool.returnBuf(buffer);
bytes.close();
}
}
/**
* Converts Headers[] to Map<String, String>.
*/
protected static Map<String, String> convertHeaders(Header[] headers) {
Map<String, String> result = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < headers.length; i++) {
result.put(headers[i].getName(), headers[i].getValue());
}
return result;
}
}

HttpStack 是一个接口,里面只有一个 performRequest() 方法,HurlStack 和 HttpClientStack 都继承自这个接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* An HTTP stack abstraction.
*/
public interface HttpStack {
/**
* Performs an HTTP request with the given parameters.
*
* <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise,
* and the Content-Type header is set to request.getPostBodyContentType().</p>
*
* @param request the request to perform
* @param additionalHeaders additional headers to be sent together with
* {@link Request#getHeaders()}
* @return the HTTP response
*/
HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
}

HurlStack 是 基于 HttpURLConnection 的,如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
**
* An {@link HttpStack} based on {@link HttpURLConnection}.
*/
public class HurlStack implements HttpStack {
private static final String HEADER_CONTENT_TYPE = "Content-Type";
/**
* An interface for transforming URLs before use.
*/
public interface UrlRewriter {
/**
* Returns a URL to use instead of the provided one, or null to indicate
* this URL should not be used at all.
*/
String rewriteUrl(String originalUrl);
}
private final UrlRewriter mUrlRewriter;
private final SSLSocketFactory mSslSocketFactory;
public HurlStack() {
this(null);
}
/**
* @param urlRewriter Rewriter to use for request URLs
*/
public HurlStack(UrlRewriter urlRewriter) {
this(urlRewriter, null);
}
/**
* @param urlRewriter Rewriter to use for request URLs
* @param sslSocketFactory SSL factory to use for HTTPS connections
*/
public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
mUrlRewriter = urlRewriter;
mSslSocketFactory = sslSocketFactory;
}
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<String, String>();
map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
url = rewritten;
}
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
for (String headerName : map.keySet()) {
connection.addRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// -1 is returned by getResponseCode() if the response code could not be retrieved.
// Signal to the caller that something was wrong with the connection.
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}
StatusLine responseStatus = new BasicStatusLine(protocolVersion,
connection.getResponseCode(), connection.getResponseMessage());
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
response.setEntity(entityFromConnection(connection));
}
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
if (header.getKey() != null) {
Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
response.addHeader(h);
}
}
return response;
}
/**
* Checks if a response message contains a body.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a>
* @param requestMethod request method
* @param responseCode response status code
* @return whether the response has a body
*/
private static boolean hasResponseBody(int requestMethod, int responseCode) {
return requestMethod != Request.Method.HEAD
&& !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK)
&& responseCode != HttpStatus.SC_NO_CONTENT
&& responseCode != HttpStatus.SC_NOT_MODIFIED;
}
/**
* Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
* @param connection
* @return an HttpEntity populated with data from <code>connection</code>.
*/
private static HttpEntity entityFromConnection(HttpURLConnection connection) {
BasicHttpEntity entity = new BasicHttpEntity();
InputStream inputStream;
try {
inputStream = connection.getInputStream();
} catch (IOException ioe) {
inputStream = connection.getErrorStream();
}
entity.setContent(inputStream);
entity.setContentLength(connection.getContentLength());
entity.setContentEncoding(connection.getContentEncoding());
entity.setContentType(connection.getContentType());
return entity;
}
/**
* Create an {@link HttpURLConnection} for the specified {@code url}.
*/
protected HttpURLConnection createConnection(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Workaround for the M release HttpURLConnection not observing the
// HttpURLConnection.setFollowRedirects() property.
// https://code.google.com/p/android/issues/detail?id=194495
connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects());
return connection;
}
/**
* Opens an {@link HttpURLConnection} with parameters.
* @param url
* @return an open connection
* @throws IOException
*/
private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
HttpURLConnection connection = createConnection(url);
int timeoutMs = request.getTimeoutMs();
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setDoInput(true);
// use caller-provided custom SslSocketFactory, if any, for HTTPS
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
}
return connection;
}
@SuppressWarnings("deprecation")
/* package */ static void setConnectionParametersForRequest(HttpURLConnection connection,
Request<?> request) throws IOException, AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST:
// This is the deprecated way that needs to be handled for backwards compatibility.
// If the request's post body is null, then the assumption is that the request is
// GET. Otherwise, it is assumed that the request is a POST.
byte[] postBody = request.getPostBody();
if (postBody != null) {
// Prepare output. There is no need to set Content-Length explicitly,
// since this is handled by HttpURLConnection using the size of the prepared
// output stream.
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.addRequestProperty(HEADER_CONTENT_TYPE,
request.getPostBodyContentType());
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(postBody);
out.close();
}
break;
case Method.GET:
// Not necessary to set the request method because connection defaults to GET but
// being explicit here.
connection.setRequestMethod("GET");
break;
case Method.DELETE:
connection.setRequestMethod("DELETE");
break;
case Method.POST:
connection.setRequestMethod("POST");
addBodyIfExists(connection, request);
break;
case Method.PUT:
connection.setRequestMethod("PUT");
addBodyIfExists(connection, request);
break;
case Method.HEAD:
connection.setRequestMethod("HEAD");
break;
case Method.OPTIONS:
connection.setRequestMethod("OPTIONS");
break;
case Method.TRACE:
connection.setRequestMethod("TRACE");
break;
case Method.PATCH:
connection.setRequestMethod("PATCH");
addBodyIfExists(connection, request);
break;
default:
throw new IllegalStateException("Unknown method type.");
}
}
private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
throws IOException, AuthFailureError {
byte[] body = request.getBody();
if (body != null) {
connection.setDoOutput(true);
connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(body);
out.close();
}
}
}

HttpClientStack 是基于 HttpClient 的,如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
* An HttpStack that performs request over an {@link HttpClient}.
*/
public class HttpClientStack implements HttpStack {
protected final HttpClient mClient;
private final static String HEADER_CONTENT_TYPE = "Content-Type";
public HttpClientStack(HttpClient client) {
mClient = client;
}
private static void addHeaders(HttpUriRequest httpRequest, Map<String, String> headers) {
for (String key : headers.keySet()) {
httpRequest.setHeader(key, headers.get(key));
}
}
@SuppressWarnings("unused")
private static List<NameValuePair> getPostParameterPairs(Map<String, String> postParams) {
List<NameValuePair> result = new ArrayList<NameValuePair>(postParams.size());
for (String key : postParams.keySet()) {
result.add(new BasicNameValuePair(key, postParams.get(key)));
}
return result;
}
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
addHeaders(httpRequest, additionalHeaders);
addHeaders(httpRequest, request.getHeaders());
onPrepareRequest(httpRequest);
HttpParams httpParams = httpRequest.getParams();
int timeoutMs = request.getTimeoutMs();
// TODO: Reevaluate this connection timeout based on more wide-scale
// data collection and possibly different for wifi vs. 3G.
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
return mClient.execute(httpRequest);
}
/**
* Creates the appropriate subclass of HttpUriRequest for passed in request.
*/
@SuppressWarnings("deprecation")
/* protected */ static HttpUriRequest createHttpRequest(Request<?> request,
Map<String, String> additionalHeaders) throws AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST: {
// This is the deprecated way that needs to be handled for backwards compatibility.
// If the request's post body is null, then the assumption is that the request is
// GET. Otherwise, it is assumed that the request is a POST.
byte[] postBody = request.getPostBody();
if (postBody != null) {
HttpPost postRequest = new HttpPost(request.getUrl());
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
HttpEntity entity;
entity = new ByteArrayEntity(postBody);
postRequest.setEntity(entity);
return postRequest;
} else {
return new HttpGet(request.getUrl());
}
}
case Method.GET:
return new HttpGet(request.getUrl());
case Method.DELETE:
return new HttpDelete(request.getUrl());
case Method.POST: {
HttpPost postRequest = new HttpPost(request.getUrl());
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(postRequest, request);
return postRequest;
}
case Method.PUT: {
HttpPut putRequest = new HttpPut(request.getUrl());
putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(putRequest, request);
return putRequest;
}
case Method.HEAD:
return new HttpHead(request.getUrl());
case Method.OPTIONS:
return new HttpOptions(request.getUrl());
case Method.TRACE:
return new HttpTrace(request.getUrl());
case Method.PATCH: {
HttpPatch patchRequest = new HttpPatch(request.getUrl());
patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(patchRequest, request);
return patchRequest;
}
default:
throw new IllegalStateException("Unknown request method.");
}
}
private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest,
Request<?> request) throws AuthFailureError {
byte[] body = request.getBody();
if (body != null) {
HttpEntity entity = new ByteArrayEntity(body);
httpRequest.setEntity(entity);
}
}
/**
* Called before the request is executed using the underlying HttpClient.
*
* <p>Overwrite in subclasses to augment the request.</p>
*/
protected void onPrepareRequest(HttpUriRequest request) throws IOException {
// Nothing.
}
/**
* The HttpPatch class does not exist in the Android framework, so this has been defined here.
*/
public static final class HttpPatch extends HttpEntityEnclosingRequestBase {
public final static String METHOD_NAME = "PATCH";
public HttpPatch() {
super();
}
public HttpPatch(final URI uri) {
super();
setURI(uri);
}
/**
* @throws IllegalArgumentException if the uri is invalid.
*/
public HttpPatch(final String uri) {
super();
setURI(URI.create(uri));
}
@Override
public String getMethod() {
return METHOD_NAME;
}
}
}

Request

StringRequest

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
/**
* A canned request for retrieving the response body at a given URL as a String.
*/
public class StringRequest extends Request<String> {
private final Listener<String> mListener;
/**
* Creates a new request with the given method.
*
* @param method the request {@link Method} to use
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
/**
* Creates a new GET request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}

JsonRequest

JsonRequest
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
/**
* A request for retrieving a T type response body at a given URL that also
* optionally sends along a JSON body in the request specified.
*
* @param <T> JSON type of response expected
*/
public abstract class JsonRequest<T> extends Request<T> {
/** Default charset for JSON request. */
protected static final String PROTOCOL_CHARSET = "utf-8";
/** Content type for request. */
private static final String PROTOCOL_CONTENT_TYPE =
String.format("application/json; charset=%s", PROTOCOL_CHARSET);
private final Listener<T> mListener;
private final String mRequestBody;
/**
* Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
* or {@link #getPostParams()} is overridden (which defaults to POST).
*
* @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
*/
@Deprecated
public JsonRequest(String url, String requestBody, Listener<T> listener,
ErrorListener errorListener) {
this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
}
public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
mRequestBody = requestBody;
}
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);
}
@Override
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
/**
* @deprecated Use {@link #getBodyContentType()}.
*/
@Deprecated
@Override
public String getPostBodyContentType() {
return getBodyContentType();
}
/**
* @deprecated Use {@link #getBody()}.
*/
@Deprecated
@Override
public byte[] getPostBody() {
return getBody();
}
@Override
public String getBodyContentType() {
return PROTOCOL_CONTENT_TYPE;
}
@Override
public byte[] getBody() {
try {
return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
} catch (UnsupportedEncodingException uee) {
VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
mRequestBody, PROTOCOL_CHARSET);
return null;
}
}
}
JsonObjectRequest
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
/**
* A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
* optional {@link JSONObject} to be passed in as part of the request body.
*/
public class JsonObjectRequest extends JsonRequest<JSONObject> {
/**
* Creates a new request.
* @param method the HTTP method to use
* @param url URL to fetch the JSON from
* @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and
* indicates no parameters will be posted along with request.
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonObjectRequest(int method, String url, JSONObject jsonRequest,
Listener<JSONObject> listener, ErrorListener errorListener) {
super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener,
errorListener);
}
/**
* Constructor which defaults to <code>GET</code> if <code>jsonRequest</code> is
* <code>null</code>, <code>POST</code> otherwise.
*
* @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
*/
public JsonObjectRequest(String url, JSONObject jsonRequest, Listener<JSONObject> listener,
ErrorListener errorListener) {
this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
listener, errorListener);
}
@Override
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
return Response.success(new JSONObject(jsonString),
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (JSONException je) {
return Response.error(new ParseError(je));
}
}
}
JsonArrayRequest
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
/**
* A request for retrieving a {@link JSONArray} response body at a given URL.
*/
public class JsonArrayRequest extends JsonRequest<JSONArray> {
/**
* Creates a new request.
* @param url URL to fetch the JSON from
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonArrayRequest(String url, Listener<JSONArray> listener, ErrorListener errorListener) {
super(Method.GET, url, null, listener, errorListener);
}
/**
* Creates a new request.
* @param method the HTTP method to use
* @param url URL to fetch the JSON from
* @param jsonRequest A {@link JSONArray} to post with the request. Null is allowed and
* indicates no parameters will be posted along with request.
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonArrayRequest(int method, String url, JSONArray jsonRequest,
Listener<JSONArray> listener, ErrorListener errorListener) {
super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener,
errorListener);
}
@Override
protected Response<JSONArray> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
return Response.success(new JSONArray(jsonString),
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (JSONException je) {
return Response.error(new ParseError(je));
}
}
}

ImageRequest

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/**
* A canned request for getting an image at a given URL and calling
* back with a decoded Bitmap.
*/
public class ImageRequest extends Request<Bitmap> {
/** Socket timeout in milliseconds for image requests */
public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000;
/** Default number of retries for image requests */
public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;
/** Default backoff multiplier for image requests */
public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f;
private final Response.Listener<Bitmap> mListener;
private final Config mDecodeConfig;
private final int mMaxWidth;
private final int mMaxHeight;
private final ScaleType mScaleType;
/** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */
private static final Object sDecodeLock = new Object();
/**
* Creates a new image request, decoding to a maximum specified width and
* height. If both width and height are zero, the image will be decoded to
* its natural size. If one of the two is nonzero, that dimension will be
* clamped and the other one will be set to preserve the image's aspect
* ratio. If both width and height are nonzero, the image will be decoded to
* be fit in the rectangle of dimensions width x height while keeping its
* aspect ratio.
*
* @param url URL of the image
* @param listener Listener to receive the decoded bitmap
* @param maxWidth Maximum width to decode this bitmap to, or zero for none
* @param maxHeight Maximum height to decode this bitmap to, or zero for
* none
* @param scaleType The ImageViews ScaleType used to calculate the needed image size.
* @param decodeConfig Format to decode the bitmap to
* @param errorListener Error listener, or null to ignore errors
*/
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) {
super(Method.GET, url, errorListener);
setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES,
DEFAULT_IMAGE_BACKOFF_MULT));
mListener = listener;
mDecodeConfig = decodeConfig;
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mScaleType = scaleType;
}
/**
* For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to
* the normal constructor with {@code ScaleType.CENTER_INSIDE}.
*/
@Deprecated
public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
Config decodeConfig, Response.ErrorListener errorListener) {
this(url, listener, maxWidth, maxHeight,
ScaleType.CENTER_INSIDE, decodeConfig, errorListener);
}
@Override
public Priority getPriority() {
return Priority.LOW;
}
/**
* Scales one side of a rectangle to fit aspect ratio.
*
* @param maxPrimary Maximum size of the primary dimension (i.e. width for
* max width), or zero to maintain aspect ratio with secondary
* dimension
* @param maxSecondary Maximum size of the secondary dimension, or zero to
* maintain aspect ratio with primary dimension
* @param actualPrimary Actual size of the primary dimension
* @param actualSecondary Actual size of the secondary dimension
* @param scaleType The ScaleType used to calculate the needed image size.
*/
private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
int actualSecondary, ScaleType scaleType) {
// If no dominant value at all, just return the actual.
if ((maxPrimary == 0) && (maxSecondary == 0)) {
return actualPrimary;
}
// If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
if (scaleType == ScaleType.FIT_XY) {
if (maxPrimary == 0) {
return actualPrimary;
}
return maxPrimary;
}
// If primary is unspecified, scale primary to match secondary's scaling ratio.
if (maxPrimary == 0) {
double ratio = (double) maxSecondary / (double) actualSecondary;
return (int) (actualPrimary * ratio);
}
if (maxSecondary == 0) {
return maxPrimary;
}
double ratio = (double) actualSecondary / (double) actualPrimary;
int resized = maxPrimary;
// If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
if (scaleType == ScaleType.CENTER_CROP) {
if ((resized * ratio) < maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
if ((resized * ratio) > maxSecondary) {
resized = (int) (maxSecondary / ratio);
}
return resized;
}
@Override
protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
// Serialize all decode on a global lock to reduce concurrent heap usage.
synchronized (sDecodeLock) {
try {
return doParse(response);
} catch (OutOfMemoryError e) {
VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
return Response.error(new ParseError(e));
}
}
}
/**
* The real guts of parseNetworkResponse. Broken out for readability.
*/
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
if (mMaxWidth == 0 && mMaxHeight == 0) {
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
// If we have to resize this image, first get the natural bounds.
decodeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
int actualWidth = decodeOptions.outWidth;
int actualHeight = decodeOptions.outHeight;
// Then compute the dimensions we would ideally like to decode to.
int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
actualWidth, actualHeight, mScaleType);
int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
actualHeight, actualWidth, mScaleType);
// Decode to the nearest power of two scaling factor.
decodeOptions.inJustDecodeBounds = false;
// TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
// decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
decodeOptions.inSampleSize =
findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
Bitmap tempBitmap =
BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
// If necessary, scale down to the maximal acceptable size.
if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
tempBitmap.getHeight() > desiredHeight)) {
bitmap = Bitmap.createScaledBitmap(tempBitmap,
desiredWidth, desiredHeight, true);
tempBitmap.recycle();
} else {
bitmap = tempBitmap;
}
}
if (bitmap == null) {
return Response.error(new ParseError(response));
} else {
return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
}
}
@Override
protected void deliverResponse(Bitmap response) {
mListener.onResponse(response);
}
/**
* Returns the largest power-of-two divisor for use in downscaling a bitmap
* that will not result in the scaling past the desired dimensions.
*
* @param actualWidth Actual width of the bitmap
* @param actualHeight Actual height of the bitmap
* @param desiredWidth Desired width of the bitmap
* @param desiredHeight Desired height of the bitmap
*/
// Visible for testing.
static int findBestSampleSize(
int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
double wr = (double) actualWidth / desiredWidth;
double hr = (double) actualHeight / desiredHeight;
double ratio = Math.min(wr, hr);
float n = 1.0f;
while ((n * 2) <= ratio) {
n *= 2;
}
return (int) n;
}
}
  • ImageLoader

    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
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    /**
    * Helper that handles loading and caching images from remote URLs.
    *
    * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
    * and to pass in the default image listener provided by
    * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
    * this class must be made from the main thead, and all responses will be delivered to the main
    * thread as well.
    */
    public class ImageLoader {
    /** RequestQueue for dispatching ImageRequests onto. */
    private final RequestQueue mRequestQueue;
    /** Amount of time to wait after first response arrives before delivering all responses. */
    private int mBatchResponseDelayMs = 100;
    /** The cache implementation to be used as an L1 cache before calling into volley. */
    private final ImageCache mCache;
    /**
    * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
    * that we can coalesce multiple requests to the same URL into a single network request.
    */
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
    new HashMap<String, BatchedImageRequest>();
    /** HashMap of the currently pending responses (waiting to be delivered). */
    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
    new HashMap<String, BatchedImageRequest>();
    /** Handler to the main thread. */
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    /** Runnable for in-flight response delivery. */
    private Runnable mRunnable;
    /**
    * Simple cache adapter interface. If provided to the ImageLoader, it
    * will be used as an L1 cache before dispatch to Volley. Implementations
    * must not block. Implementation with an LruCache is recommended.
    */
    public interface ImageCache {
    Bitmap getBitmap(String url);
    void putBitmap(String url, Bitmap bitmap);
    }
    /**
    * Constructs a new ImageLoader.
    * @param queue The RequestQueue to use for making image requests.
    * @param imageCache The cache to use as an L1 cache.
    */
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
    mRequestQueue = queue;
    mCache = imageCache;
    }
    /**
    * The default implementation of ImageListener which handles basic functionality
    * of showing a default image until the network response is received, at which point
    * it will switch to either the actual image or the error image.
    * @param view The imageView that the listener is associated with.
    * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
    * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
    */
    public static ImageListener getImageListener(final ImageView view,
    final int defaultImageResId, final int errorImageResId) {
    return new ImageListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
    if (errorImageResId != 0) {
    view.setImageResource(errorImageResId);
    }
    }
    @Override
    public void onResponse(ImageContainer response, boolean isImmediate) {
    if (response.getBitmap() != null) {
    view.setImageBitmap(response.getBitmap());
    } else if (defaultImageResId != 0) {
    view.setImageResource(defaultImageResId);
    }
    }
    };
    }
    /**
    * Interface for the response handlers on image requests.
    *
    * The call flow is this:
    * 1. Upon being attached to a request, onResponse(response, true) will
    * be invoked to reflect any cached data that was already available. If the
    * data was available, response.getBitmap() will be non-null.
    *
    * 2. After a network response returns, only one of the following cases will happen:
    * - onResponse(response, false) will be called if the image was loaded.
    * or
    * - onErrorResponse will be called if there was an error loading the image.
    */
    public interface ImageListener extends ErrorListener {
    /**
    * Listens for non-error changes to the loading of the image request.
    *
    * @param response Holds all information pertaining to the request, as well
    * as the bitmap (if it is loaded).
    * @param isImmediate True if this was called during ImageLoader.get() variants.
    * This can be used to differentiate between a cached image loading and a network
    * image loading in order to, for example, run an animation to fade in network loaded
    * images.
    */
    void onResponse(ImageContainer response, boolean isImmediate);
    }
    /**
    * Checks if the item is available in the cache.
    * @param requestUrl The url of the remote image
    * @param maxWidth The maximum width of the returned image.
    * @param maxHeight The maximum height of the returned image.
    * @return True if the item exists in cache, false otherwise.
    */
    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
    return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
    }
    /**
    * Checks if the item is available in the cache.
    *
    * @param requestUrl The url of the remote image
    * @param maxWidth The maximum width of the returned image.
    * @param maxHeight The maximum height of the returned image.
    * @param scaleType The scaleType of the imageView.
    * @return True if the item exists in cache, false otherwise.
    */
    public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
    throwIfNotOnMainThread();
    String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    return mCache.getBitmap(cacheKey) != null;
    }
    /**
    * Returns an ImageContainer for the requested URL.
    *
    * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
    * If the default was returned, the {@link ImageLoader} will be invoked when the
    * request is fulfilled.
    *
    * @param requestUrl The URL of the image to be loaded.
    */
    public ImageContainer get(String requestUrl, final ImageListener listener) {
    return get(requestUrl, listener, 0, 0);
    }
    /**
    * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
    * {@code Scaletype == ScaleType.CENTER_INSIDE}.
    */
    public ImageContainer get(String requestUrl, ImageListener imageListener,
    int maxWidth, int maxHeight) {
    return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
    }
    /**
    * Issues a bitmap request with the given URL if that image is not available
    * in the cache, and returns a bitmap container that contains all of the data
    * relating to the request (as well as the default image if the requested
    * image is not available).
    * @param requestUrl The url of the remote image
    * @param imageListener The listener to call when the remote image is loaded
    * @param maxWidth The maximum width of the returned image.
    * @param maxHeight The maximum height of the returned image.
    * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
    * @return A container object that contains all of the properties of the request, as well as
    * the currently available image (default if remote is not loaded).
    */
    public ImageContainer get(String requestUrl, ImageListener imageListener,
    int maxWidth, int maxHeight, ScaleType scaleType) {
    // only fulfill requests that were initiated from the main thread.
    throwIfNotOnMainThread();
    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
    // Try to look up the request in the cache of remote images.
    Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
    if (cachedBitmap != null) {
    // Return the cached bitmap.
    ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
    imageListener.onResponse(container, true);
    return container;
    }
    // The bitmap did not exist in the cache, fetch it!
    ImageContainer imageContainer =
    new ImageContainer(null, requestUrl, cacheKey, imageListener);
    // Update the caller to let them know that they should use the default bitmap.
    imageListener.onResponse(imageContainer, true);
    // Check to see if a request is already in-flight.
    BatchedImageRequest request = mInFlightRequests.get(cacheKey);
    if (request != null) {
    // If it is, add this request to the list of listeners.
    request.addContainer(imageContainer);
    return imageContainer;
    }
    // The request is not already in flight. Send the new request to the network and
    // track it.
    Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
    cacheKey);
    mRequestQueue.add(newRequest);
    mInFlightRequests.put(cacheKey,
    new BatchedImageRequest(newRequest, imageContainer));
    return imageContainer;
    }
    protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
    ScaleType scaleType, final String cacheKey) {
    return new ImageRequest(requestUrl, new Listener<Bitmap>() {
    @Override
    public void onResponse(Bitmap response) {
    onGetImageSuccess(cacheKey, response);
    }
    }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
    onGetImageError(cacheKey, error);
    }
    });
    }
    /**
    * Sets the amount of time to wait after the first response arrives before delivering all
    * responses. Batching can be disabled entirely by passing in 0.
    * @param newBatchedResponseDelayMs The time in milliseconds to wait.
    */
    public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
    mBatchResponseDelayMs = newBatchedResponseDelayMs;
    }
    /**
    * Handler for when an image was successfully loaded.
    * @param cacheKey The cache key that is associated with the image request.
    * @param response The bitmap that was returned from the network.
    */
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
    // cache the image that was fetched.
    mCache.putBitmap(cacheKey, response);
    // remove the request from the list of in-flight requests.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
    // Update the response bitmap.
    request.mResponseBitmap = response;
    // Send the batched response
    batchResponse(cacheKey, request);
    }
    }
    /**
    * Handler for when an image failed to load.
    * @param cacheKey The cache key that is associated with the image request.
    */
    protected void onGetImageError(String cacheKey, VolleyError error) {
    // Notify the requesters that something failed via a null result.
    // Remove this request from the list of in-flight requests.
    BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
    if (request != null) {
    // Set the error for this request
    request.setError(error);
    // Send the batched response
    batchResponse(cacheKey, request);
    }
    }
    /**
    * Container object for all of the data surrounding an image request.
    */
    public class ImageContainer {
    /**
    * The most relevant bitmap for the container. If the image was in cache, the
    * Holder to use for the final bitmap (the one that pairs to the requested URL).
    */
    private Bitmap mBitmap;
    private final ImageListener mListener;
    /** The cache key that was associated with the request */
    private final String mCacheKey;
    /** The request URL that was specified */
    private final String mRequestUrl;
    /**
    * Constructs a BitmapContainer object.
    * @param bitmap The final bitmap (if it exists).
    * @param requestUrl The requested URL for this container.
    * @param cacheKey The cache key that identifies the requested URL for this container.
    */
    public ImageContainer(Bitmap bitmap, String requestUrl,
    String cacheKey, ImageListener listener) {
    mBitmap = bitmap;
    mRequestUrl = requestUrl;
    mCacheKey = cacheKey;
    mListener = listener;
    }
    /**
    * Releases interest in the in-flight request (and cancels it if no one else is listening).
    */
    public void cancelRequest() {
    if (mListener == null) {
    return;
    }
    BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
    if (request != null) {
    boolean canceled = request.removeContainerAndCancelIfNecessary(this);
    if (canceled) {
    mInFlightRequests.remove(mCacheKey);
    }
    } else {
    // check to see if it is already batched for delivery.
    request = mBatchedResponses.get(mCacheKey);
    if (request != null) {
    request.removeContainerAndCancelIfNecessary(this);
    if (request.mContainers.size() == 0) {
    mBatchedResponses.remove(mCacheKey);
    }
    }
    }
    }
    /**
    * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
    */
    public Bitmap getBitmap() {
    return mBitmap;
    }
    /**
    * Returns the requested URL for this container.
    */
    public String getRequestUrl() {
    return mRequestUrl;
    }
    }
    /**
    * Wrapper class used to map a Request to the set of active ImageContainer objects that are
    * interested in its results.
    */
    private class BatchedImageRequest {
    /** The request being tracked */
    private final Request<?> mRequest;
    /** The result of the request being tracked by this item */
    private Bitmap mResponseBitmap;
    /** Error if one occurred for this response */
    private VolleyError mError;
    /** List of all of the active ImageContainers that are interested in the request */
    private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
    /**
    * Constructs a new BatchedImageRequest object
    * @param request The request being tracked
    * @param container The ImageContainer of the person who initiated the request.
    */
    public BatchedImageRequest(Request<?> request, ImageContainer container) {
    mRequest = request;
    mContainers.add(container);
    }
    /**
    * Set the error for this response
    */
    public void setError(VolleyError error) {
    mError = error;
    }
    /**
    * Get the error for this response
    */
    public VolleyError getError() {
    return mError;
    }
    /**
    * Adds another ImageContainer to the list of those interested in the results of
    * the request.
    */
    public void addContainer(ImageContainer container) {
    mContainers.add(container);
    }
    /**
    * Detatches the bitmap container from the request and cancels the request if no one is
    * left listening.
    * @param container The container to remove from the list
    * @return True if the request was canceled, false otherwise.
    */
    public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
    mContainers.remove(container);
    if (mContainers.size() == 0) {
    mRequest.cancel();
    return true;
    }
    return false;
    }
    }
    /**
    * Starts the runnable for batched delivery of responses if it is not already started.
    * @param cacheKey The cacheKey of the response being delivered.
    * @param request The BatchedImageRequest to be delivered.
    */
    private void batchResponse(String cacheKey, BatchedImageRequest request) {
    mBatchedResponses.put(cacheKey, request);
    // If we don't already have a batch delivery runnable in flight, make a new one.
    // Note that this will be used to deliver responses to all callers in mBatchedResponses.
    if (mRunnable == null) {
    mRunnable = new Runnable() {
    @Override
    public void run() {
    for (BatchedImageRequest bir : mBatchedResponses.values()) {
    for (ImageContainer container : bir.mContainers) {
    // If one of the callers in the batched request canceled the request
    // after the response was received but before it was delivered,
    // skip them.
    if (container.mListener == null) {
    continue;
    }
    if (bir.getError() == null) {
    container.mBitmap = bir.mResponseBitmap;
    container.mListener.onResponse(container, false);
    } else {
    container.mListener.onErrorResponse(bir.getError());
    }
    }
    }
    mBatchedResponses.clear();
    mRunnable = null;
    }
    };
    // Post the runnable.
    mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
    }
    }
    private void throwIfNotOnMainThread() {
    if (Looper.myLooper() != Looper.getMainLooper()) {
    throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
    }
    }
    /**
    * Creates a cache key for use with the L1 cache.
    * @param url The URL of the request.
    * @param maxWidth The max-width of the output.
    * @param maxHeight The max-height of the output.
    * @param scaleType The scaleType of the imageView.
    */
    private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
    return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
    .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
    .toString();
    }
    }
  • NetworkImageView

    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
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    /**
    * Handles fetching an image from a URL as well as the life-cycle of the
    * associated request.
    */
    public class NetworkImageView extends ImageView {
    /** The URL of the network image to load */
    private String mUrl;
    /**
    * Resource ID of the image to be used as a placeholder until the network image is loaded.
    */
    private int mDefaultImageId;
    /**
    * Resource ID of the image to be used if the network response fails.
    */
    private int mErrorImageId;
    /** Local copy of the ImageLoader. */
    private ImageLoader mImageLoader;
    /** Current ImageContainer. (either in-flight or finished) */
    private ImageContainer mImageContainer;
    public NetworkImageView(Context context) {
    this(context, null);
    }
    public NetworkImageView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }
    public NetworkImageView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    }
    /**
    * Sets URL of the image that should be loaded into this view. Note that calling this will
    * immediately either set the cached image (if available) or the default image specified by
    * {@link NetworkImageView#setDefaultImageResId(int)} on the view.
    *
    * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and
    * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling
    * this function.
    *
    * @param url The URL that should be loaded into this ImageView.
    * @param imageLoader ImageLoader that will be used to make the request.
    */
    public void setImageUrl(String url, ImageLoader imageLoader) {
    mUrl = url;
    mImageLoader = imageLoader;
    // The URL has potentially changed. See if we need to load it.
    loadImageIfNecessary(false);
    }
    /**
    * Sets the default image resource ID to be used for this view until the attempt to load it
    * completes.
    */
    public void setDefaultImageResId(int defaultImage) {
    mDefaultImageId = defaultImage;
    }
    /**
    * Sets the error image resource ID to be used for this view in the event that the image
    * requested fails to load.
    */
    public void setErrorImageResId(int errorImage) {
    mErrorImageId = errorImage;
    }
    /**
    * Loads the image for the view if it isn't already loaded.
    * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise.
    */
    void loadImageIfNecessary(final boolean isInLayoutPass) {
    int width = getWidth();
    int height = getHeight();
    ScaleType scaleType = getScaleType();
    boolean wrapWidth = false, wrapHeight = false;
    if (getLayoutParams() != null) {
    wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT;
    wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT;
    }
    // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
    // view, hold off on loading the image.
    boolean isFullyWrapContent = wrapWidth && wrapHeight;
    if (width == 0 && height == 0 && !isFullyWrapContent) {
    return;
    }
    // if the URL to be loaded in this view is empty, cancel any old requests and clear the
    // currently loaded image.
    if (TextUtils.isEmpty(mUrl)) {
    if (mImageContainer != null) {
    mImageContainer.cancelRequest();
    mImageContainer = null;
    }
    setDefaultImageOrNull();
    return;
    }
    // if there was an old request in this view, check if it needs to be canceled.
    if (mImageContainer != null && mImageContainer.getRequestUrl() != null) {
    if (mImageContainer.getRequestUrl().equals(mUrl)) {
    // if the request is from the same URL, return.
    return;
    } else {
    // if there is a pre-existing request, cancel it if it's fetching a different URL.
    mImageContainer.cancelRequest();
    setDefaultImageOrNull();
    }
    }
    // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens.
    int maxWidth = wrapWidth ? 0 : width;
    int maxHeight = wrapHeight ? 0 : height;
    // The pre-existing content of this view didn't match the current URL. Load the new image
    // from the network.
    // update the ImageContainer to be the new bitmap container.
    mImageContainer = mImageLoader.get(mUrl,
    new ImageListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
    if (mErrorImageId != 0) {
    setImageResource(mErrorImageId);
    }
    }
    @Override
    public void onResponse(final ImageContainer response, boolean isImmediate) {
    // If this was an immediate response that was delivered inside of a layout
    // pass do not set the image immediately as it will trigger a requestLayout
    // inside of a layout. Instead, defer setting the image by posting back to
    // the main thread.
    if (isImmediate && isInLayoutPass) {
    post(new Runnable() {
    @Override
    public void run() {
    onResponse(response, false);
    }
    });
    return;
    }
    if (response.getBitmap() != null) {
    setImageBitmap(response.getBitmap());
    } else if (mDefaultImageId != 0) {
    setImageResource(mDefaultImageId);
    }
    }
    }, maxWidth, maxHeight, scaleType);
    }
    private void setDefaultImageOrNull() {
    if(mDefaultImageId != 0) {
    setImageResource(mDefaultImageId);
    }
    else {
    setImageBitmap(null);
    }
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    loadImageIfNecessary(true);
    }
    @Override
    protected void onDetachedFromWindow() {
    if (mImageContainer != null) {
    // If the view was bound to an image request, cancel it and clear
    // out the image from the view.
    mImageContainer.cancelRequest();
    setImageBitmap(null);
    // also clear out the container so we can reload the image if necessary.
    mImageContainer = null;
    }
    super.onDetachedFromWindow();
    }
    @Override
    protected void drawableStateChanged() {
    super.drawableStateChanged();
    invalidate();
    }
    }

ClearCacheRequest

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
/**
* A synthetic request used for clearing the cache.
*/
public class ClearCacheRequest extends Request<Object> {
private final Cache mCache;
private final Runnable mCallback;
/**
* Creates a synthetic request for clearing the cache.
* @param cache Cache to clear
* @param callback Callback to make on the main thread once the cache is clear,
* or null for none
*/
public ClearCacheRequest(Cache cache, Runnable callback) {
super(Method.GET, null, null);
mCache = cache;
mCallback = callback;
}
@Override
public boolean isCanceled() {
// This is a little bit of a hack, but hey, why not.
mCache.clear();
if (mCallback != null) {
Handler handler = new Handler(Looper.getMainLooper());
handler.postAtFrontOfQueue(mCallback);
}
return true;
}
@Override
public Priority getPriority() {
return Priority.IMMEDIATE;
}
@Override
protected Response<Object> parseNetworkResponse(NetworkResponse response) {
return null;
}
@Override
protected void deliverResponse(Object response) {
}
}

Cache

DiskBasedCache

默认缓存路径为硬盘缓存,如下:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
/**
* Cache implementation that caches files directly onto the hard disk in the specified
* directory. The default disk usage size is 5MB, but is configurable.
*/
public class DiskBasedCache implements Cache {
/** Map of the Key, CacheHeader pairs */
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<String, CacheHeader>(16, .75f, true);
/** Total amount of space currently used by the cache in bytes. */
private long mTotalSize = 0;
/** The root directory to use for the cache. */
private final File mRootDirectory;
/** The maximum size of the cache in bytes. */
private final int mMaxCacheSizeInBytes;
/** Default maximum disk usage in bytes. */
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
/** High water mark percentage for the cache */
private static final float HYSTERESIS_FACTOR = 0.9f;
/** Magic number for current version of cache file format. */
private static final int CACHE_MAGIC = 0x20150306;
/**
* Constructs an instance of the DiskBasedCache at the specified directory.
* @param rootDirectory The root directory of the cache.
* @param maxCacheSizeInBytes The maximum size of the cache in bytes.
*/
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
/**
* Constructs an instance of the DiskBasedCache at the specified directory using
* the default maximum cache size of 5MB.
* @param rootDirectory The root directory of the cache.
*/
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
/**
* Clears the cache. Deletes all cached files from disk.
*/
@Override
public synchronized void clear() {
File[] files = mRootDirectory.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
mEntries.clear();
mTotalSize = 0;
VolleyLog.d("Cache cleared.");
}
/**
* Returns the cache entry with the specified key if it exists, null otherwise.
*/
@Override
public synchronized Entry get(String key) {
CacheHeader entry = mEntries.get(key);
// if the entry does not exist, return.
if (entry == null) {
return null;
}
File file = getFileForKey(key);
CountingInputStream cis = null;
try {
cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
CacheHeader.readHeader(cis); // eat header
byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
return entry.toCacheEntry(data);
} catch (IOException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} catch (NegativeArraySizeException e) {
VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
remove(key);
return null;
} finally {
if (cis != null) {
try {
cis.close();
} catch (IOException ioe) {
return null;
}
}
}
}
/**
* Initializes the DiskBasedCache by scanning for all files currently in the
* specified root directory. Creates the root directory if necessary.
*/
@Override
public synchronized void initialize() {
if (!mRootDirectory.exists()) {
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
putEntry(entry.key, entry);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
/**
* Invalidates an entry in the cache.
* @param key Cache key
* @param fullExpire True to fully expire the entry, false to soft expire
*/
@Override
public synchronized void invalidate(String key, boolean fullExpire) {
Entry entry = get(key);
if (entry != null) {
entry.softTtl = 0;
if (fullExpire) {
entry.ttl = 0;
}
put(key, entry);
}
}
/**
* Puts the entry with the specified key into the cache.
*/
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
/**
* Removes the specified key from the cache if it exists.
*/
@Override
public synchronized void remove(String key) {
boolean deleted = getFileForKey(key).delete();
removeEntry(key);
if (!deleted) {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
key, getFilenameForKey(key));
}
}
/**
* Creates a pseudo-unique filename for the specified cache key.
* @param key The key to generate a file name for.
* @return A pseudo-unique filename.
*/
private String getFilenameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
/**
* Returns a file object for the given cache key.
*/
public File getFileForKey(String key) {
return new File(mRootDirectory, getFilenameForKey(key));
}
/**
* Prunes the cache to fit the amount of bytes specified.
* @param neededSpace The amount of bytes we are trying to fit into the cache.
*/
private void pruneIfNeeded(int neededSpace) {
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
if (VolleyLog.DEBUG) {
VolleyLog.v("Pruning old cache entries.");
}
long before = mTotalSize;
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
} else {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
e.key, getFilenameForKey(e.key));
}
iterator.remove();
prunedFiles++;
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
if (VolleyLog.DEBUG) {
VolleyLog.v("pruned %d files, %d bytes, %d ms",
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
}
}
/**
* Puts the entry with the specified key into the cache.
* @param key The key to identify the entry by.
* @param entry The entry to cache.
*/
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
}
/**
* Removes the entry identified by 'key' from the cache.
*/
private void removeEntry(String key) {
CacheHeader entry = mEntries.get(key);
if (entry != null) {
mTotalSize -= entry.size;
mEntries.remove(key);
}
}
/**
* Reads the contents of an InputStream into a byte[].
* */
private static byte[] streamToBytes(InputStream in, int length) throws IOException {
byte[] bytes = new byte[length];
int count;
int pos = 0;
while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
pos += count;
}
if (pos != length) {
throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
}
return bytes;
}
/**
* Handles holding onto the cache headers for an entry.
*/
// Visible for testing.
static class CacheHeader {
/** The size of the data identified by this CacheHeader. (This is not
* serialized to disk. */
public long size;
/** The key that identifies the cache entry. */
public String key;
/** ETag for cache coherence. */
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;
/** Headers from the response resulting in this cache entry. */
public Map<String, String> responseHeaders;
private CacheHeader() { }
/**
* Instantiates a new CacheHeader object
* @param key The key that identifies the cache entry
* @param entry The cache entry.
*/
public CacheHeader(String key, Entry entry) {
this.key = key;
this.size = entry.data.length;
this.etag = entry.etag;
this.serverDate = entry.serverDate;
this.lastModified = entry.lastModified;
this.ttl = entry.ttl;
this.softTtl = entry.softTtl;
this.responseHeaders = entry.responseHeaders;
}
/**
* Reads the header off of an InputStream and returns a CacheHeader object.
* @param is The InputStream to read from.
* @throws IOException
*/
public static CacheHeader readHeader(InputStream is) throws IOException {
CacheHeader entry = new CacheHeader();
int magic = readInt(is);
if (magic != CACHE_MAGIC) {
// don't bother deleting, it'll get pruned eventually
throw new IOException();
}
entry.key = readString(is);
entry.etag = readString(is);
if (entry.etag.equals("")) {
entry.etag = null;
}
entry.serverDate = readLong(is);
entry.lastModified = readLong(is);
entry.ttl = readLong(is);
entry.softTtl = readLong(is);
entry.responseHeaders = readStringStringMap(is);
return entry;
}
/**
* Creates a cache entry for the specified data.
*/
public Entry toCacheEntry(byte[] data) {
Entry e = new Entry();
e.data = data;
e.etag = etag;
e.serverDate = serverDate;
e.lastModified = lastModified;
e.ttl = ttl;
e.softTtl = softTtl;
e.responseHeaders = responseHeaders;
return e;
}
/**
* Writes the contents of this CacheHeader to the specified OutputStream.
*/
public boolean writeHeader(OutputStream os) {
try {
writeInt(os, CACHE_MAGIC);
writeString(os, key);
writeString(os, etag == null ? "" : etag);
writeLong(os, serverDate);
writeLong(os, lastModified);
writeLong(os, ttl);
writeLong(os, softTtl);
writeStringStringMap(responseHeaders, os);
os.flush();
return true;
} catch (IOException e) {
VolleyLog.d("%s", e.toString());
return false;
}
}
}
private static class CountingInputStream extends FilterInputStream {
private int bytesRead = 0;
private CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int result = super.read();
if (result != -1) {
bytesRead++;
}
return result;
}
@Override
public int read(byte[] buffer, int offset, int count) throws IOException {
int result = super.read(buffer, offset, count);
if (result != -1) {
bytesRead += result;
}
return result;
}
}
/*
* Homebrewed simple serialization system used for reading and writing cache
* headers on disk. Once upon a time, this used the standard Java
* Object{Input,Output}Stream, but the default implementation relies heavily
* on reflection (even for standard types) and generates a ton of garbage.
*/
/**
* Simple wrapper around {@link InputStream#read()} that throws EOFException
* instead of returning -1.
*/
private static int read(InputStream is) throws IOException {
int b = is.read();
if (b == -1) {
throw new EOFException();
}
return b;
}
static void writeInt(OutputStream os, int n) throws IOException {
os.write((n >> 0) & 0xff);
os.write((n >> 8) & 0xff);
os.write((n >> 16) & 0xff);
os.write((n >> 24) & 0xff);
}
static int readInt(InputStream is) throws IOException {
int n = 0;
n |= (read(is) << 0);
n |= (read(is) << 8);
n |= (read(is) << 16);
n |= (read(is) << 24);
return n;
}
static void writeLong(OutputStream os, long n) throws IOException {
os.write((byte)(n >>> 0));
os.write((byte)(n >>> 8));
os.write((byte)(n >>> 16));
os.write((byte)(n >>> 24));
os.write((byte)(n >>> 32));
os.write((byte)(n >>> 40));
os.write((byte)(n >>> 48));
os.write((byte)(n >>> 56));
}
static long readLong(InputStream is) throws IOException {
long n = 0;
n |= ((read(is) & 0xFFL) << 0);
n |= ((read(is) & 0xFFL) << 8);
n |= ((read(is) & 0xFFL) << 16);
n |= ((read(is) & 0xFFL) << 24);
n |= ((read(is) & 0xFFL) << 32);
n |= ((read(is) & 0xFFL) << 40);
n |= ((read(is) & 0xFFL) << 48);
n |= ((read(is) & 0xFFL) << 56);
return n;
}
static void writeString(OutputStream os, String s) throws IOException {
byte[] b = s.getBytes("UTF-8");
writeLong(os, b.length);
os.write(b, 0, b.length);
}
static String readString(InputStream is) throws IOException {
int n = (int) readLong(is);
byte[] b = streamToBytes(is, n);
return new String(b, "UTF-8");
}
static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
if (map != null) {
writeInt(os, map.size());
for (Map.Entry<String, String> entry : map.entrySet()) {
writeString(os, entry.getKey());
writeString(os, entry.getValue());
}
} else {
writeInt(os, 0);
}
}
static Map<String, String> readStringStringMap(InputStream is) throws IOException {
int size = readInt(is);
Map<String, String> result = (size == 0)
? Collections.<String, String>emptyMap()
: new HashMap<String, String>(size);
for (int i = 0; i < size; i++) {
String key = readString(is).intern();
String value = readString(is).intern();
result.put(key, value);
}
return result;
}
}

####