一般来讲网络请求可以简化为下图:
客户端发出请求,等待服务端响应数据,整个流程看起来很简单,但实际应用过程中会碰到各种各样的问题,比如请求参数繁琐的配置,网络不稳定需要自动重连,URL变更重定向,缓存处理等等。试想如果每个请求我们都完完整整地处理这些异常流程,那么导致的结果就是代码冗余、难以扩展。所以对网络请求进行封装一直都是刚需,网上可以找到各式各样的网络请求框架,今天我们准备学习的是 Volley 的框架,鉴于其在 Android 圈名头太大就不做过多介绍,只需要知道它是 Google I/O 2013上发布的网络通信库。
Volley和餐饮店
在本文中,我打算按照自己的理解来梳理 Volley 的设计流程。为了加深印象,我们假设有这么一家小饭馆,有多小呢,整个人员配置只有一个厨师范师傅。
厨房点餐与请求封装
某一天,你迫于自己太懒所以决定去这家饭馆吃,由于饭馆人员机构过于扁平,你不得不跑去厨房跟厨师说:"范师傅,来个黄焖鸡米饭,记得多放盐,多放辣、不要葱花"。半个小时后,厨师说搞定,这份黄焖鸡就是你的了。过了一会儿,你还想吃啤酒鸭,没服务员呐,你又得去厨房来这么一遍。你稍微感觉有点不对劲,如果你还要一个点个汤呢,这也太麻烦了吧。
吃完饭你到隔壁麻将馆找饭店老板结账时稍微抱怨了一下,老板刚赢了钱很高兴,表示想听听你的看法。你面色一正,轻咳了两声说道:"首先当然要明确职责,厨师你就让好好炒菜,不要他跟顾客打交道分了心,顾客想吃什么写个订单给他好了"。老板一听,言之有理,说道:"这个是好办法,但我们范师傅老花眼,订单上的字得写大一点,这样吧,一个订单就代表一份菜。另外,我们现在主打东北菜,过段时间可能要尝试粤菜,你帮我想想怎么弄"。
网络请求跟点餐十分类似,服务器首先得了解你需要什么,这是可以订单来表示 ,现在我们设计订单样式。
Request源码
public abstract class Request<T> {
/** Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. */
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
/** Supported request methods. */
public interface Method {
int DEPRECATED_GET_OR_POST = -1;
int GET = 0;
int POST = 1;
int PUT = 2;
int DELETE = 3;
int HEAD = 4;
int OPTIONS = 5;
int TRACE = 6;
int PATCH = 7;
}
private final int mMethod;
/** URL of this request. */
private final String mUrl;
}
可以看到Request包含了网络请求的基本信息 URL 和 Method。现在厨师知道你想吃什么了,那么你知道厨师会用什么给你盛菜吗,锅?碗?瓢?盆?,所以我们还要规定一种饭盒来盛菜。
HttpResponse源码
public final class HttpResponse {
private final int mStatusCode;
private final List<Header> mHeaders;
private final int mContentLength;
private final InputStream mContent;
}
HttpResponse 包含了网络请求的响应码、响应报头、响应输入流。现在厨师也知道做好的菜要放在这个饭盒里给你了。最后来设计厨师类,现在厨师能看懂你的订单,也知道了要返回给你装满饭盒的菜,但老板说了以后要换菜系,因为每个菜系的做菜方法不尽相同,所以厨师的操作手法不要写死,要面向接口编程。
HttpStack源码
public interface HttpStack {
HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError;
}
HttpStack 接口只有一个方法,这个方法接受一张订单,一些备注,最终返回一个饭盒。现在让我们的范厨师来实现这个接口,以后范厨师接到的订单做出来的菜都是东北风味。
HurlStack源码
public class HurlStack extends BaseHttpStack {
private static final int HTTP_CONTINUE = 100;
@Override
public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<>();
map.putAll(additionalHeaders);
// Request.getHeaders() takes precedence over the given additional (cache) headers).
map.putAll(request.getHeaders());
...
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
boolean keepConnectionOpen = false;
try {
for (String headerName : map.keySet()) {
connection.setRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
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.");
}
if (!hasResponseBody(request.getMethod(), responseCode)) {
return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
}
// Need to keep the connection open until the stream is consumed by the caller. Wrap the
// stream such that close() will disconnect the connection.
keepConnectionOpen = true;
// 返回响应
return new HttpResponse(
responseCode,
convertHeaders(connection.getHeaderFields()),
connection.getContentLength(),
new UrlConnectionInputStream(connection));
} finally {
if (!keepConnectionOpen) {
connection.disconnect();
}
}
}
}
先不用去了解细节,里面封装的是标准的 HttpUrlConnection 请求,它能做到解析 Request 和 Headers,返回 HttpResponse。也就是说,给范厨师一张订单,写上一些备注,他能给你做出东北味的菜了,最后还装在饭盒里递给你。万一以后再来一个蔡师傅,实现这个接口后,就能做出淮扬菜了。
至此,设计工作初步完成,饭馆老板很满意,给你的饭钱算了八折。
Volley为了移除对 Apache HTTP 库的依赖问题和兼容原有设计,将接口方法下沉到抽象类BaseHttpStack中去,所以HurlStack实现的是父类的抽象方法,而不是我们定义的接口方法
主厨与重试机制
迫于懒惰,过了几天你又去这家饭馆,这次菜上得很快,但打开饭盒里面居然是空的,你强压怒火问怎么回事。范厨师抽了根烟说:"红烧排骨刚加完水,熄火了,后面还一大堆订单呢,所以你这个就不做了"。你正要跳起来跟范厨师理论的时候,饭馆老板凭空出现,打发范厨师先去做饭并拉着你的手说:"老弟,你帮我想个办法弄弄这事儿,我平时都忙着在隔壁博弈,实在没空管这个",你不为所动,老板试探性地问道:"给个八折?",这时你的脸才舒张开来,说道:"炒菜过程中可能遇到各种各样的问题,不能因为停水停电就撂挑子,你让范师傅多试几次,说不定水电早就好了呢。我们需要一个主厨来决定到底要不要重试",老板深以为然,然后让你展开讲讲。
这次不用老板说你就考虑到以后做大做强的问题了,厨师长这个类肯定不能写死,现在顾客和厨师之间隔着一个主厨,所以订单由主厨转交给厨师,成菜也由主厨转交给你。既然都有主厨了,再用饭盒也不合适,我们来定义一个餐盘类。
NetworkResponse源码
public class NetworkResponse {
/** The HTTP status code. */
public final int statusCode;
/** Raw data from this response. */
public final byte[] data;
/**
* Response headers.
*
* <p>This map is case-insensitive. It should not be mutated directly.
*
* <p>Note that if the server returns two headers with the same (case-insensitive) name, this
* map will only contain the last one. Use {@link #allHeaders} to inspect all headers returned
* by the server.
*/
public final Map<String, String> headers;
/** All response headers. Must not be mutated directly. */
public final List<Header> allHeaders;
/** True if the server returned a 304 (Not Modified). */
public final boolean notModified;
/** Network roundtrip time in milliseconds. */
public final long networkTimeMs;
}
可以看到,新的响应封装中,已经把数据响应流转换为数组了,这方便我们的后续处理。
那么再定义一个服务员接口如下:
Network源码
public interface Network {
NetworkResponse performRequest(Request<?> request) throws VolleyError;
}
主厨接收订单,返回餐盘,所以的重试判断都由它的实现类来处理。
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;
/**
* @deprecated Should never have been exposed in the API. This field may be removed in a future
* release of Volley.
*/
@Deprecated protected final HttpStack mHttpStack;
private final BaseHttpStack mBaseHttpStack;
protected final ByteArrayPool mPool;
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
List<Header> responseHeaders = Collections.emptyList();
try {
...
// 真正执行网络请求
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
...
// 返回 Response
return new NetworkResponse(
statusCode,
responseContents,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
} catch (SocketTimeoutException e) {
// 1. 纳尼,超时了,吃掉异常,进入下一个 while 循环
attemptRetryOnException("socket", request, new TimeoutError());
} catch (MalformedURLException e) {
// 2. 纳尼,url都拼错了,处理不了抛异常
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode;
if (httpResponse != null) {
statusCode = httpResponse.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,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
// 3. 纳尼,权限问题,吃掉异常,进入下一个 while 循环
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());
}
}
}
}
/**
* 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));
}
}
可以看到,主厨通过判断请求过程中的异常和响应的异常码来决定要不要重试。当然我们也不能一直重试,不然其他订单就只能永远等着,所以有一个重试策略存在,它由 RetryPolicy 来控制,此处暂且不表。
不同的主厨有不同的处理风格,所以客人可以选择将订单交给他们任意一个。
老板很满意,觉得这小馆子越来越正规了,按照约定给你打了八折。
服务员与请求队列
又有一天,你都下订单半天了还不见上菜,原来主厨处理完上一个订单就躲到一边抽烟去了。你跟老板说,这可不行,得赶紧找个服务员,在有新订单的时候通知主厨,老板说你看着办。
因为服务员要不停地检查是否还有订单需要处理,所以创建一个线程来做循环处理。
public class NetworkDispatcher extends Thread {
/** The queue of requests to service. */
private final BlockingQueue<Request<?>> mQueue;
/** The network interface for processing requests. */
private final Network mNetwork;
/** The cache to write to. */
private final Cache mCache;
/** For posting responses and errors. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/**
* Forces this dispatcher to quit immediately. If any requests are still in the queue, they are
* not guaranteed to be processed.
*/
public void quit() {
mQuit = true;
interrupt();
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
try {
processRequest();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
Thread.currentThread().interrupt();
return;
}
VolleyLog.e(
"Ignoring spurious interrupt of NetworkDispatcher thread; "
+ "use quit() to terminate it");
}
}
}
// Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
// This is needed to avoid keeping previous request references alive for an indeterminate amount
// of time. Update consumer-proguard-rules.pro when modifying this. See also
// https://github.com/google/volley/issues/114
private void processRequest() throws InterruptedException {
// Take a request from the queue.
Request<?> request = mQueue.take();
processRequest(request);
}
@VisibleForTesting
void processRequest(Request<?> request) {
long startTimeMs = SystemClock.elapsedRealtime();
try {
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
request.notifyListenerResponseNotUsable();
return;
}
addTrafficStatsTag(request);
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
request.notifyListenerResponseNotUsable();
return;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache if applicable.
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// Post the response back.
request.markDelivered();
mDelivery.postResponse(request, response);
request.notifyListenerResponseReceived(response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
request.notifyListenerResponseNotUsable();
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
request.notifyListenerResponseNotUsable();
}
}
}
可以看到我们的服务员一旦获取到订单就交给主厨(NetWork)处理,并得到成品,并查询是否还有未处理的订单,循环往复。
冷藏工与缓存处理
又有一天,你去店里点了个王八汤,等了两个小时问服务员怎么还没动静,服务员告诉你今天做十个做王八汤的,还排着队挨个做呢。你想,这不得做到天黑啊,赶紧去找老板。 你们不能一次性多做点冷藏起来吗,只要还是达标的就不用挨个去做了,热热不完了吗?老板义正辞严地说道,我们要搞诚信餐饮,我考虑考虑。第二天找了一个冷藏工,专门负责成品冷藏。
public class CacheDispatcher extends Thread {
private static final boolean DEBUG = VolleyLog.DEBUG;
/** The queue of requests coming in for triage. */
private final BlockingQueue<Request<?>> mCacheQueue;
/** The queue of requests going out to the network. */
private final BlockingQueue<Request<?>> mNetworkQueue;
/** The cache to read from. */
private final Cache mCache;
/** For posting responses. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/** Manage list of waiting requests and de-duplicate requests with same cache key. */
private final WaitingRequestManager mWaitingRequestManager;
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
processRequest();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
Thread.currentThread().interrupt();
return;
}
VolleyLog.e(
"Ignoring spurious interrupt of CacheDispatcher thread; "
+ "use quit() to terminate it");
}
}
}
// Extracted to its own method to ensure locals have a constrained liveness scope by the GC.
// This is needed to avoid keeping previous request references alive for an indeterminate amount
// of time. Update consumer-proguard-rules.pro when modifying this. See also
// https://github.com/google/volley/issues/114
private void processRequest() throws InterruptedException {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
processRequest(request);
}
@VisibleForTesting
void processRequest(final Request<?> request) throws InterruptedException {
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
return;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
mNetworkQueue.put(request);
}
return;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
mNetworkQueue.put(request);
}
return;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response =
request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(
request,
response,
new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
});
} else {
// request has been added to list of waiting requests
// to receive the network response from the first request once it returns.
mDelivery.postResponse(request, response);
}
}
}
}
前台与队列封装
现在服务员和冷藏工都可能出菜,我们还得找个前台来统筹安排。 如果顾客赶时间,订单上注明了只要食物还比较新鲜,不介意使用库存,那么前台就将订单交给冷藏工,否则交给服务员。如果被冷藏工告知食物过期了,那么就将订单转交给服务员。
设计一个前台
public class RequestQueue {
/** The cache triage queue. */
private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<>();
/** The queue of requests that are actually going out to the network. */
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<>();
/** Number of network request dispatcher threads to start. */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
/** Cache interface for retrieving and storing responses. */
private final Cache mCache;
/** Network interface for performing requests. */
private final Network mNetwork;
/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;
/** The network dispatchers. */
private final NetworkDispatcher[] mDispatchers;
/** The cache dispatcher. */
private CacheDispatcher mCacheDispatcher;
/** Starts the dispatchers in this queue. */
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher =
new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
/**
* Adds a Request to the dispatch queue.
*
* @param request The request to service
* @return The passed-in request
*/
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
mCacheQueue.add(request);
return request;
}
}
可以看到前台管理了 N个 服务员(NetworkDispatcher),一个冷藏工(CacheDispatcher),调用 start 方法两个线程就开始做无限循环。顾客订单通过 add 方法添加进来,如果请求注明了禁止缓存则直接交给服务员处理;否则交给冷藏工。
现在我们再梳理一下整个流程
- 顾客将订单交给前台;
- 前台根据订单备注来决定把订单交给服务员还是冷藏工;
- 如果交给冷藏工,冷藏工去仓库查询是否有可用库存。如果没有,交给服务员,进入下一步;如果有,交给顾客,终止。
- 如果交给服务员,服务员再转交给主厨,主厨再交给范师傅。主厨判定中途发生问题时是否重试,服务员负责将新的订单源源不断地传给主厨。
- 服务员拿到成品后,根据订单要求决定是否往冷库存储。交给顾客,终止。
细节分析
前文简单地将Volley的架子搭建好,下面继续看看里面的细节。
前台(RequestQueue)
RequestQueue#start方法
public void start() {
// 1. 先重置
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
// CacheDispatcher就代表冷藏工,它持有库存请求单、网络请求单、冷库、回调。
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
// start开始后,开始无限循环
mCacheDispatcher.start();
// NetworkDispatcher代表服务员,它持有网络请求单、请求封装类(主厨)、冷库、回调。
// start开始后,开始无限循环查询订单
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher =
new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
RequestQueue#stop方法
/** Stops the cache and network dispatchers. */
public void stop() {
if (mCacheDispatcher != null) {
mCacheDispatcher.quit();
}
for (final NetworkDispatcher mDispatcher : mDispatchers) {
if (mDispatcher != null) {
mDispatcher.quit();
}
}
}
// Dispatcher#quit方法,先将中止标志位置为 true,调用 interrupt 方法会抛出 InterruptedException ,在异常捕获中跳出 while 循环。
public void quit() {
mQuit = true;
interrupt();
}
RequestQueue#add方法
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
// 将 Request 标记属于当前队列,在特定时候需要通过当前对象移除 Request
request.setRequestQueue(this);
// 加锁添加
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// 如果请求不可缓存,请跳过缓存队列并直接进入网络。向队列添加 Request 完成后,服务员或冷藏工可以通过 while 循环得到它。
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
mCacheQueue.add(request);
return request;
}
RequestQueue#cancelAll方法
public void cancelAll(RequestFilter filter) {
synchronized (mCurrentRequests) {
for (Request<?> request : mCurrentRequests) {
// 当满足指定条件时,将 Request 标记为 cancel,后续处理过程中,会对这个状态进行判断。
if (filter.apply(request)) {
request.cancel();
}
}
}
}
冷藏工(CacheDispatcher)
CacheDispatcher#processRequest方法
void processRequest(final Request<?> request) th rows InterruptedException {
// 1. 标记 Request 来源
request.addMarker("cache-queue-take");
// 2. 如果 Request 已被标记移除,结束
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
return;
}
// 3. 尝试从冷库中检索出缓存,key 为字符串 [Request Method + URL]
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
// 4. 标记缓存不存在
request.addMarker("cache-miss");
// 5. 关于 mWaitingRequestManager 我们后面再看 TODO
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
// 6. 将该请求添加到网络请求队列中去,终止。
mNetworkQueue.put(request);
}
return;
}
if (entry.isExpired()) {
// 7. 已拿到缓存,但是过期了,标记
request.addMarker("cache-hit-expired");
// 8. Request 持有这个缓存
request.setCacheEntry(entry);
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
// 9. 将该请求添加到网络请求队列中去,终止。
mNetworkQueue.put(request);
}
return;
}
// 10. 拿到未过期的缓存,标记
request.addMarker("cache-hit");
// 11. 构造 Response,Response 的 Json转换放在这一步
Response<?> response =
request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// 12. 无需刷新数据,回调,终止。
mDelivery.postResponse(request, response);
} else {
// 13. 软过期缓存命中,我们可以直接响应缓存 Response 。但我们也需要将其扔进网络请求队列中刷新数据,标记
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// 14. 标记为软过期响应
response.intermediate = true;
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(
request,
response,
new Runnable() {
@Override
public void run() {
try {
// 添加到请求队列中去
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
});
} else {
// request has been added to list of waiting requests
// to receive the network response from the first request once it returns.
mDelivery.postResponse(request, response);
}
}
}
CacheDispatcher#WaitingRequestManager类
private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
/**
* Staging area for requests that already have a duplicate request in flight.
*
* <ul>
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given
* cache key.
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight
* request is <em>not</em> contained in that list. Is null if no requests are staged.
* </ul>
*/
private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();
private final CacheDispatcher mCacheDispatcher;
WaitingRequestManager(CacheDispatcher cacheDispatcher) {
mCacheDispatcher = cacheDispatcher;
}
/** Request received a valid response that can be used by other waiting requests. */
@Override
public void onResponseReceived(Request<?> request, Response<?> response) {
if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
onNoUsableResponseReceived(request);
return;
}
String cacheKey = request.getCacheKey();
List<Request<?>> waitingRequests;
synchronized (this) {
waitingRequests = mWaitingRequests.remove(cacheKey);
}
if (waitingRequests != null) {
if (VolleyLog.DEBUG) {
VolleyLog.v(
"Releasing %d waiting requests for cacheKey=%s.",
waitingRequests.size(), cacheKey);
}
// Process all queued up requests.
for (Request<?> waiting : waitingRequests) {
mCacheDispatcher.mDelivery.postResponse(waiting, response);
}
}
}
/** No valid response received from network, release waiting requests. */
@Override
public synchronized void onNoUsableResponseReceived(Request<?> request) {
String cacheKey = request.getCacheKey();
List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null && !waitingRequests.isEmpty()) {
if (VolleyLog.DEBUG) {
VolleyLog.v(
"%d waiting requests for cacheKey=%s; resend to network",
waitingRequests.size(), cacheKey);
}
Request<?> nextInLine = waitingRequests.remove(0);
mWaitingRequests.put(cacheKey, waitingRequests);
nextInLine.setNetworkRequestCompleteListener(this);
try {
mCacheDispatcher.mNetworkQueue.put(nextInLine);
} catch (InterruptedException iex) {
VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
// Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
Thread.currentThread().interrupt();
// Quit the current CacheDispatcher thread.
mCacheDispatcher.quit();
}
}
}
/**
* 如果相同的 Request 还是处理中,加入队列中等待之前的 Request 处理完成。
* @return 如果返回 false,我们应该继续处理请求;true,我们应该暂停处理,等待之前的 Request 处理完毕。
*/
private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
String cacheKey = request.getCacheKey();
// Insert request into stage if there's already a request with the same cache key
// in flight.
if (mWaitingRequests.containsKey(cacheKey)) {
// 1. 如果存在相同的 Request。
List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new ArrayList<>();
}
// 2. 标记
request.addMarker("waiting-for-response");
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
return true;
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
// 3. 没有待处理的相同的 Request
mWaitingRequests.put(cacheKey, null);
request.setNetworkRequestCompleteListener(this);
if (VolleyLog.DEBUG) {
VolleyLog.d("new request, sending to network %s", cacheKey);
}
return false;
}
}
}
也就是说每个 Request 我们都会去判断,此时是不是还有一个相同的 Request 尚在处理中。
最后再看看 mDelivery,它是一个名为 ResponseDelivery 的接口,主要作用是网络请求结果的回调以及线程切换
ResponseDelivery源码
public interface ResponseDelivery {
/** Parses a response from the network or cache and delivers it. */
void postResponse(Request<?> request, Response<?> response);
/**
* Parses a response from the network or cache and delivers it. The provided Runnable will be
* executed after delivery.
*/
void postResponse(Request<?> request, Response<?> response, Runnable runnable);
/** Posts an error for the given request. */
void postError(Request<?> request, VolleyError error);
}
`ExecutorDelivery源码`
简单的三个方法,分别用于处理响应成功和响应失败的情况。再看一看它的实现类
public class ExecutorDelivery implements ResponseDelivery {
/** Used for posting responses, typically to the main thread. */
private final Executor mResponsePoster;
/**
* Creates a new response delivery interface.
*
* @param handler {@link Handler} to post responses on
*/
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster =
new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
/**
* Creates a new response delivery interface, mockable version for testing.
*
* @param executor For running delivery tasks
*/
public ExecutorDelivery(Executor executor) {
mResponsePoster = executor;
}
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
@Override
public void postResponse Request<?> request, Response<?> response, Runnable runnable) {
// 1. 标记
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
@Override
public void postError(Request<?> request, VolleyError error) {
request.addMarker("post-error");
Response<?> response = Response.error(error);
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
}
/** A Runnable used for delivering network responses to a listener on the main thread. */
@SuppressWarnings("rawtypes")
private static class ResponseDeliveryRunnable implements Runnable {
private final Request mRequest;
private final Response mResponse;
private final Runnable mRunnable;
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
// NOTE: If cancel() is called off the thread that we're currently running in (by
// default, the main thread), we cannot guarantee that deliverResponse()/deliverError()
// won't be called, since it may be canceled after we check isCanceled() but before we
// deliver the response. Apps concerned about this guarantee must either call cancel()
// from the same thread or implement their own guarantee about not invoking their
// listener after cancel() has been called.
// If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
// 1. Request 被取消,标记
mRequest.finish("canceled-at-delivery");
return;
}
// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
// 2. 成功,回调
mRequest.deliverResponse(mResponse.result);
} else {
// 3. 报错,回调
mRequest.deliverError(mResponse.error);
}
// If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
// 4. 是否是软过期,标记
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
// 5. 处理其他逻辑
// If we have been provided a post-delivery runnable, run it.
if (mRunnable != null) {
mRunnable.run();
}
}
}
}
运行在哪个线程取决于 Handler,默认主线程。
服务员(NetworkDispatcher)
NetworkDispatcher源码
void processRequest(Request<?> request) {
long startTimeMs = SystemClock.elapsedRealtime();
try {
// 1. 标记 Request 来源
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
// 2. 已取消
request.finish("network-discard-cancelled");
request.notifyListenerResponseNotUsable();
return;
}
// 3. ???统计流量??
addTrafficStatsTag(request);
// 4. 让主厨完成请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// 5. 当服务器返回304表明已有未过期的缓存,并且已经回调过。终止
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
request.notifyListenerResponseNotUsable();
return;
}
// 6. 数据转换,如gson解析等
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// 7. 如果可用,写入缓存。备注,当返回304的时候只更新元数据。
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// 8. 标记数据已回传,并通过 Delivery 回传数据
request.markDelivered();
mDelivery.postResponse(request, response);
request.notifyListenerResponseReceived(response);
} catch (VolleyError volleyError) {
// 9. 异常处理
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
request.notifyListenerResponseNotUsable();
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
request.notifyListenerResponseNotUsable();
}
}
主厨(Network)
BasicNetwork#performRequest
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
List<Header> responseHeaders = Collections.emptyList();
try {
// 跟缓存有关的表头
Map<String, String> additionalRequestHeaders =
getCacheHeaders(request.getCacheEntry());
// 真正执行网络请求
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
// 获取响应码
int statusCode = httpResponse.getStatusCode();
// 响应报头
responseHeaders = httpResponse.getHeaders();
// Handle cache validation.
// 处理 body
InputStream inputStream = httpResponse.getContent();
if (inputStream != null) {
responseContents =
inputStreamToBytes(inputStream, httpResponse.getContentLength());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
if (statusCode < 200 || statusCode > 299) {
// 请求不成功,抛出异常
throw new IOException();
}
// 返回 Response
return new NetworkResponse(
statusCode,
responseContents,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
} catch (SocketTimeoutException e) {
// 1. 纳尼,超时了,吃掉异常,进入下一个 while 循环
attemptRetryOnException("socket", request, new TimeoutError());
} catch (MalformedURLException e) {
// 2. 纳尼,url都拼错了,处理不了抛异常
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode;
if (httpResponse != null) {
statusCode = httpResponse.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,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
// 3. 纳尼,权限问题,吃掉异常,进入下一个 while 循环
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());
}
}
}
}
BasicNetwork#inputStreamToBytes方法
/** Reads the contents of an InputStream into a byte[]. */
private byte[] inputStreamToBytes(InputStream in, int contentLength)
throws IOException, ServerError {
// mPool作用是对象复用
PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, contentLength);
byte[] buffer = null;
try {
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".
if (in != null) {
in.close();
}
} catch (IOException e) {
// This can happen if there was an exception above that left the stream in
// an invalid state.
VolleyLog.v("Error occurred when closing InputStream");
}
mPool.returnBuf(buffer);
bytes.close();
}
}
ByteArrayPool源码
这个类主要是主要是为了复用 byte[],I/O操作时经常创建大量临时 byte[] 用于复制数据
public class ByteArrayPool {
/** The buffer pool, arranged both by last use and by buffer size */
private final List<byte[]> mBuffersByLastUse = new ArrayList<>();
private final List<byte[]> mBuffersBySize = new ArrayList<>(64);
/** The total size of the buffers in the pool */
private int mCurrentSize = 0;
/**
* 数据池大小的上限,超过这个值将移除 old buffers
*/
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;
}
/**
* 从数据池中找一个超过指定大小的 byte[]返回(不存在就创建一个),从数据池移除这个byte[] 对象
*/
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];
}
/**
* return byte[]到 pool中,相当于添加。
*/
public synchronized void returnBuf(byte[] buf) {
if (buf == null || buf.length > mSizeLimit) {
// 如果直接超过 pool MaxSize,结束
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();
}
/** 保证 pool 满足 maxSize. */
private synchronized void trim() {
while (mCurrentSize > mSizeLimit) {
byte[] buf = mBuffersByLastUse.remove(0);
mBuffersBySize.remove(buf);
mCurrentSize -= buf.length;
}
}
}
图片加载 (ImageLoader)
官方介绍说Volley适合进行数据量不大但是通信频繁的网络操作,所以它及其适合图片加载。
图片加载实现了一个新的的 Request,它主要作用是将 InputStream 解析为 Bitmap.
ImageRequest#doParse方法
private Response<Bitmap> doParse(NetworkResponse response) {
byte[] data = response.data;
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
Bitmap bitmap = null;
if (mMaxWidth == 0 && mMaxHeight == 0) {
// 1. 未指定宽高,直接解码图片
decodeOptions.inPreferredConfig = mDecodeConfig;
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
} else {
// 2. 如果需要缩放图片,先获取图片宽高。
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);
// 3. 缩放
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));
}
}
ImageLoader#get方法
public ImageContainer get(
String requestUrl,
ImageListener imageListener,
int maxWidth,
int maxHeight,
ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
Threads.throwIfNotOnMainThread();
// 1. 获取请求的唯一标识
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// 2. 先查询缓存
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container =
new ImageContainer(
cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null);
// 3. 直接返回结果
imageListener.onResponse(container, true);
return container;
}
// 4. 未命中缓存,构建 Container 对象。Container和imageListener关联起来,最终会回调 Container#imageListener接口
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// 5. 通知调用者,此时可显示默认图片
imageListener.onResponse(imageContainer, true);
// 6. 检查请求是否已存在。有一个记录图片批量请求的类,将该请求放在里面,并返回
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// 7. 代表这是一个全新的请求
Request<Bitmap> newRequest =
makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
// 8. 添加到 Volley 的请求队列中。成功响应后,会回调 Request#Listener接口,这个接口的方法调用了ImageLoader#onGetImageSuccess方法。
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
ImageLoader#onGetImageSucces方法
protected void onGetImageSuccess(String cacheKey, Bitmap response) {
// 1. 先缓存一份
mCache.putBitmap(cacheKey, response);
// 2. 从 in-flight 列表移除
BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
if (request != null) {
// Update the response bitmap.
request.mResponseBitmap = response;
// 3. 在主线程中处理结果
batchResponse(cacheKey, request);
}
}
ImageLoader#batchResponse方法
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() {
// 1. mBatchedResponses对应多个请求 2. BatchedImageRequest表示某个请求对应多个观察者
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);
}
}
总结
粗糙地过了一遍 Volley,总的感觉是分层明显,可以作为面向接口编程的Demo。当然还有细节也值得探究,例如本地缓存、多线程处理等等,这个下次单独总结。