您的位置:首页 > 移动开发 > Android开发

Android Volley完全解析,带你从源码的角度理解Volley

2017-01-19 15:48 375 查看
经过前三篇文章的学习,Volley的用法我们已经掌握的差不多了,但是对于Volley的工作原理,恐怕有很多朋友还不是很清楚。因此,本篇文章中我们就来一起阅读一下Volley的源码,将它的工作流程整体地梳理一遍。同时,这也是Volley系列的最后一篇文章了。

其实,Volley的官方文档中本身就附有了一张Volley的工作流程图,如下图所示。

多数朋友突然看到一张这样的图,应该会和我一样,感觉一头雾水吧?没错,目前我们对Volley背后的工作原理还没有一个概念性的理解,直接就来看这张图自然会有些吃力。不过没关系,下面我们就去分析一下Volley的源码,之后再重新来看这张图就会好理解多了。

说起分析源码,那么应该从哪儿开始看起呢?这就要回顾一下Volley的用法了,还记得吗,使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:

1. public static RequestQueue newRequestQueue(Context context) {

2. return newRequestQueue(context, null);
3. }

这个方法仅仅只有一行代码,只是调用了newRequestQueue()的方法重载,并给第二个参数传入null。那我们看下带有两个参数的newRequestQueue()方法中的代码,如下所示:

1. public static RequestQueue newRequestQueue(Context context, HttpStack stack) {

2. File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
3. String userAgent = "volley/0";

4. try {
5. String packageName = context.getPackageName();

6. PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
7. userAgent = packageName + "/" + info.versionCode;

8. } catch (NameNotFoundException e) {
9. }

10. if (stack == null) {
11. if (Build.VERSION.SDK_INT >= 9) {

12. stack = new HurlStack();
13. } else {

14. stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
15. }

16. }
17. Network network = new BasicNetwork(stack);

18. RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
19. queue.start();

20. return queue;
21. }

可以看到,这里在第10行判断如果stack是等于null的,则去创建一个HttpStack对象,这里会判断如果手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例。实际上HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的,这里为什么这样选择呢?可以参考我之前翻译的一篇文章Android访问网络,使用HttpURLConnection还是HttpClient?

创建好了HttpStack之后,接下来又创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的,紧接着new出一个RequestQueue对象,并调用它的start()方法进行启动,然后将RequestQueue返回,这样newRequestQueue()的方法就执行结束了。

那么RequestQueue的start()方法内部到底执行了什么东西呢?我们跟进去瞧一瞧:

1. public void start() {

2. stop(); // Make sure any currently running dispatchers are stopped.
3. // Create the cache dispatcher and start it.

4. mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
5. mCacheDispatcher.start();

6. // Create network dispatchers (and corresponding threads) up to the pool size.
7. for (int i = 0; i < mDispatchers.length; i++) {

8. NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
9. mCache, mDelivery);

10. mDispatchers[i] = networkDispatcher;
11. networkDispatcher.start();

12. }
13. }

这里先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法,接着在一个for循环里去创建NetworkDispatcher的实例,并分别调用它们的start()方法。这里的CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中CacheDispatcher是缓存线程,NetworkDispatcher是网络请求线程。

得到了RequestQueue之后,我们只需要构建出相应的Request,然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了,那么不用说,add()方法的内部肯定有着非常复杂的逻辑,我们来一起看一下:

1. public <T> Request<T> add(Request<T> request) {

2. // Tag the request as belonging to this queue and add it to the set of current requests.
3. request.setRequestQueue(this);

4. synchronized (mCurrentRequests) {
5. mCurrentRequests.add(request);

6. }
7. // Process requests in the order they are added.

8. request.setSequence(getSequenceNumber());
9. request.addMarker("add-to-queue");

10. // If the request is uncacheable, skip the cache queue and go straight to the network.
11. if (!request.shouldCache()) {

12. mNetworkQueue.add(request);
13. return request;

14. }
15. // Insert request into stage if there's already a request with the same cache key in flight.

16. synchronized (mWaitingRequests) {
17. String cacheKey = request.getCacheKey();

18. if (mWaitingRequests.containsKey(cacheKey)) {
19. // There is already a request in flight. Queue up.

20. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
21. if (stagedRequests == null) {

22. stagedRequests = new LinkedList<Request<?>>();
23. }

24. stagedRequests.add(request);
25. mWaitingRequests.put(cacheKey, stagedRequests);

26. if (VolleyLog.DEBUG) {
27. VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);

28. &
1e92b
nbsp; }
29. } else {

30. // Insert 'null' queue for this cacheKey, indicating there is now a request in
31. // flight.

32. mWaitingRequests.put(cacheKey, null);
33. mCacheQueue.add(request);

34. }
35. return request;

36. }
37. }

可以看到,在第11行的时候会判断当前的请求是否可以缓存,如果不能缓存则在第12行直接将这条请求加入网络请求队列,可以缓存的话则在第33行将这条请求加入缓存队列。在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。

OK,那么既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下CacheDispatcher中的run()方法,代码如下所示:

1. public class CacheDispatcher extends Thread {

2.
3. ……

4.
5. @Override

6. public void run() {
7. if (DEBUG) VolleyLog.v("start new dispatcher");

8. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
9. // Make a blocking call to initialize the cache.

10. mCache.initialize();
11. while (true) {

12. try {
13. // Get a request from the cache triage queue, blocking until

14. // at least one is available.
15. final Request<?> request = mCacheQueue.take();

16. request.addMarker("cache-queue-take");
17. // If the request has been canceled, don't bother dispatching it.

18. if (request.isCanceled()) {
19. request.finish("cache-discard-canceled");

20. continue;
21. }

22. // Attempt to retrieve this item from cache.
23. Cache.Entry entry = mCache.get(request.getCacheKey());

24. if (entry == null) {
25. request.addMarker("cache-miss");

26. // Cache miss; send off to the network dispatcher.
27. mNetworkQueue.put(request);

28. continue;
29. }

30. // If it is completely expired, just send it to the network.
31. if (entry.isExpired()) {

32. request.addMarker("cache-hit-expired");
33. request.setCacheEntry(entry);

34. mNetworkQueue.put(request);
35. continue;

36. }
37. // We have a cache hit; parse its data for delivery back to the request.

38. request.addMarker("cache-hit");
39. Response<?> response = request.parseNetworkResponse(

40. new NetworkResponse(entry.data, entry.responseHeaders));
41. request.addMarker("cache-hit-parsed");

42. if (!entry.refreshNeeded()) {
43. // Completely unexpired cache hit. Just deliver the response.

44. mDelivery.postResponse(request, response);
45. } else {

46. // Soft-expired cache hit. We can deliver the cached response,
47. // but we need to also send the request to the network for

48. // refreshing.
49. request.addMarker("cache-hit-refresh-needed");

50. request.setCacheEntry(entry);
51. // Mark the response as intermediate.

52. response.intermediate = true;
53. // Post the intermediate response back to the user and have

54. // the delivery then forward the request along to the network.
55. mDelivery.postResponse(request, response, new Runnable() {

56. @Override
57. public void run() {

58. try {
59. mNetworkQueue.put(request);

60. } catch (InterruptedException e) {
61. // Not much we can do about this.

62. }
63. }

64. });
65. }

66. } catch (InterruptedException e) {
67. // We may have been interrupted because it was time to quit.

68. if (mQuit) {
69. return;

70. }
71. continue;

72. }
73. }

74. }
75. }

代码有点长,我们只挑重点看。首先在11行可以看到一个while(true)循环,说明缓存线程始终是在运行的,接着在第23行会尝试从缓存当中取出响应结果,如何为空的话则把这条请求加入到网络请求队列中,如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中,否则就认为不需要重发网络请求,直接使用缓存中的数据即可。之后会在第39行调用Request的parseNetworkResponse()方法来对数据进行解析,再往后就是将解析出来的数据进行回调了,这部分代码我们先跳过,因为它的逻辑和NetworkDispatcher后半部分的逻辑是基本相同的,那么我们等下合并在一起看就好了,先来看一下NetworkDispatcher中是怎么处理网络请求队列的,代码如下所示:

1. public class NetworkDispatcher extends Thread {

2. ……
3. @Override

4. public void run() {
5. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

6. Request<?> request;
7. while (true) {

8. try {
9. // Take a request from the queue.

10. request = mQueue.take();
11. } catch (InterruptedException e) {

12. // We may have been interrupted because it was time to quit.
13. if (mQuit) {

14. return;
15. }

16. continue;
17. }

18. try {
19. request.addMarker("network-queue-take");

20. // If the request was cancelled already, do not perform the
21. // network request.

22. if (request.isCanceled()) {
23. request.finish("network-discard-cancelled");

24. continue;
25. }

26. addTrafficStatsTag(request);
27. // Perform the network request.

28. NetworkResponse networkResponse = mNetwork.performRequest(request);
29. request.addMarker("network-http-complete");

30. // If the server returned 304 AND we delivered a response already,
31. // we're done -- don't deliver a second identical response.

32. if (networkResponse.notModified && request.hasHadResponseDelivered()) {
33. request.finish("not-modified");

34. continue;
35. }

36. // Parse the response here on the worker thread.
37. Response<?> response = request.parseNetworkResponse(networkResponse);

38. request.addMarker("network-parse-complete");
39. // Write to cache if applicable.

40. // TODO: Only update cache metadata instead of entire record for 304s.
41. if (request.shouldCache() && response.cacheEntry != null) {

42. mCache.put(request.getCacheKey(), response.cacheEntry);
43. request.addMarker("network-cache-written");

44. }
45. // Post the response back.

46. request.markDelivered();
47. mDelivery.postResponse(request, response);

48. } catch (VolleyError volleyError) {
49. parseAndDeliverNetworkError(request, volleyError);

50. } catch (Exception e) {
51. VolleyLog.e(e, "Unhandled exception %s", e.toString());

52. mDelivery.postError(request, new VolleyError(e));
53. }

54. }
55. }

56. }
同样地,在第7行我们看到了类似的while(true)循环,说明网络请求线程也是在不断运行的。在第28行的时候会调用Network的performRequest()方法来去发送网络请求,而Network是一个接口,这里具体的实现是BasicNetwork,我们来看下它的performRequest()方法,如下所示:

1. public class BasicNetwork implements Network {

2. ……
3. @Override

4. public NetworkResponse performRequest(Request<?> request) throws VolleyError {
5. long requestStart = SystemClock.elapsedRealtime();

6. while (true) {
7. HttpResponse httpResponse = null;

8. byte[] responseContents = null;
9. Map<String, String> responseHeaders = new HashMap<String, String>();

10. try {
11. // Gather headers.

12. Map<String, String> headers = new HashMap<String, String>();
13. addCacheHeaders(headers, request.getCacheEntry());

14. httpResponse = mHttpStack.performRequest(request, headers);
15. StatusLine statusLine = httpResponse.getStatusLine();

16. int statusCode = statusLine.getStatusCode();
17. responseHeaders = convertHeaders(httpResponse.getAllHeaders());

18. // Handle cache validation.
19. if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

20. return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
21. request.getCacheEntry() == null ? null : request.getCacheEntry().data,

22. responseHeaders, true);
23. }

24. // Some responses such as 204s do not have content. We must check.
25. if (httpResponse.getEntity() != null) {

26. responseContents = entityToBytes(httpResponse.getEntity());
27. } else {

28. // Add 0 byte response as a way of honestly representing a
29. // no-content request.

30. responseContents = new byte[0];
31. }

32. // if the request is slow, log it.
33. long requestLifetime = SystemClock.elapsedRealtime() - requestStart;

34. logSlowRequests(requestLifetime, request, responseContents, statusLine);
35. if (statusCode < 200 || statusCode > 299) {

36. throw new IOException();
37. }

38. return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
39. } catch (Exception e) {

40. ……
41. }

42. }
43. }

44. }
这段方法中大多都是一些网络请求细节方面的东西,我们并不需要太多关心,需要注意的是在第14行调用了HttpStack的performRequest()方法,这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象。前面已经说过,这两个对象的内部实际就是分别使用HttpURLConnection和HttpClient来发送网络请求的,我们就不再跟进去阅读了,之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回。

在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。还记得我们在上一篇文章中学习的自定义Request的方式吗?其中parseNetworkResponse()这个方法就是必须要重写的。

在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据,代码如下所示:

1. private class ResponseDeliveryRunnable implements Runnable {

2. private final Request mRequest;
3. private final Response mResponse;

4. private final Runnable mRunnable;
5.

6. public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
7. mRequest = request;

8. mResponse = response;
9. mRunnable = runnable;

10. }
11.

12. @SuppressWarnings("unchecked")
13. @Override

14. public void run() {
15. // If this request has canceled, finish it and don't deliver.

16. if (mRequest.isCanceled()) {
17. mRequest.finish("canceled-at-delivery");

18. return;
19. }

20. // Deliver a normal response or error, depending.
21. if (mResponse.isSuccess()) {

22. mRequest.deliverResponse(mResponse.result);
23. } else {

24. mRequest.deliverError(mResponse.error);
25. }

26. // If this is an intermediate response, add a marker, otherwise we're done
27. // and the request can be finished.

28. if (mResponse.intermediate) {
29. mRequest.addMarker("intermediate-response");

30. } else {
31. mRequest.finish("done");

32. }
33. // If we have been provided a post-delivery runnable, run it.

34. if (mRunnable != null) {
35. mRunnable.run();

36. }
37. }

38. }
代码虽然不多,但我们并不需要行行阅读,抓住重点看即可。其中在第22行调用了Request的deliverResponse()方法,有没有感觉很熟悉?没错,这个就是我们在自定义Request时需要重写的另外一个方法,每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。

好了,到这里我们就把Volley的完整执行流程全部梳理了一遍,你是不是已经感觉已经很清晰了呢?对了,还记得在文章一开始的那张流程图吗,刚才还不能理解,现在我们再来重新看下这张图:

其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。

怎么样,是不是感觉现在理解这张图已经变得轻松简单了?好了,到此为止我们就把Volley的用法和源码全部学习完了,相信你已经对Volley非常熟悉并可以将它应用到实际项目当中了,那么Volley完全解析系列的文章到此结束,感谢大家有耐心看到最后。

Volley源码解释

1. 功能介绍

1.1. Volley

Volley 是
Google 推出的
Android 异步网络请求框架和图片加载框架。在
Google I/O 2013 大会上发布。

名字由来:a burst or emission of many things or a large amount at once

发布演讲时候的配图

从名字由来和配图中无数急促的火箭可以看出 Volley
的特点:特别适合数据量小,通信频繁的网络操作。(个人认为 Android
应用中绝大多数的网络操作都属于这种类型)。

1.2 Volley 的主要特点

(1). 扩展性强。Volley
中大多是基于接口的设计,可配置性强。

(2). 一定程度符合
Http 规范,包括返回
ResponseCode(2xx、3xx、4xx、5xx)的处理,请求头的处理,缓存机制的支持等。并支持重试及优先级定义。

(3). 默认
Android2.3 及以上基于
HttpURLConnection,2.3
以下基于 HttpClient
实现,这两者的区别及优劣在4.2.1 Volley中具体介绍。

(4). 提供简便的图片加载工具。

2. 总体设计

2.1 总体设计图

上面是 Volley
的总体设计图,主要是通过两种Dispatch Thread不断从RequestQueue中取出请求,根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据,然后交由ResponseDelivery去做结果分发及回调处理。

2.2 Volley 中的概念

简单介绍一些概念,在详细设计中会仔细介绍。

Volley 的调用比较简单,通过
newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue后,只需要往这个RequestQueue不断
add Request 即可。

Volley:Volley
对外暴露的 API,通过
newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue。

Request:表示一个请求的抽象类。StringRequest、JsonRequest、ImageRequest 都是它的子类,表示某种类型的请求。

RequestQueue:表示请求队列,里面包含一个CacheDispatcher(用于处理走缓存请求的调度线程)、NetworkDispatcher数组(用于处理走网络请求的调度线程),一个ResponseDelivery(返回结果分发接口),通过
start() 函数启动时会启动CacheDispatcher和NetworkDispatchers。

CacheDispatcher:一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。

NetworkDispatcher:一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理,并判断结果是否要进行缓存。

ResponseDelivery:返回结果分发接口,目前只有基于ExecutorDelivery的在入参
handler 对应线程内进行分发。

HttpStack:处理 Http
请求,返回请求结果。目前 Volley
中有基于 HttpURLConnection
的HurlStack和
基于 Apache HttpClient
的HttpClientStack。

Network:调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。

Cache:缓存请求结果,Volley
默认使用的是基于 sdcard
的DiskBasedCache。NetworkDispatcher得到请求结果后判断是否需要存储在
Cache,CacheDispatcher会从 Cache
中取缓存结果。

3. 流程图

Volley 请求流程图

上图是 Volley
请求时的流程图,在 Volley
的发布演讲中给出,我在这里将其用中文重新画出。


4. 详细设计

4.1 类关系图

这是 Volley
框架的主要类关系图
图中红色圈内的部分,组成了
Volley 框架的核心,围绕 RequestQueue
类,将各个功能点以组合的方式结合在了一起。各个功能点也都是以接口或者抽象类的形式提供。

红色圈外面的部分,在 Volley
源码中放在了 toolbox
包中,作为 Volley
为各个功能点提供的默认的具体实现。

通过类图我们看出, Volley
有着非常好的拓展性。通过各个功能点的接口,我们可以给出自定义的,更符合我们需求的具体实现。

多用组合,少用继承;针对接口编程,不针对具体实现编程。

优秀框架的设计,令人叫绝,受益良多。

4.2 核心类功能介绍

4.2.1 Volley.java

这个和 Volley
框架同名的类,其实是个工具类,作用是构建一个可用于添加网络请求的RequestQueue对象。

(1).
主要函数


Volley.java
有两个重载的静态方法。

public static RequestQueue newRequestQueue(Context context)

public static RequestQueue newRequestQueue(Context context, HttpStack stack)
第一个方法的实现调用了第二个方法,传 HttpStack
参数为 null。

第二个方法中,如果 HttpStatck
参数为 null,则如果系统在
Gingerbread 及之后(即
API Level >= 9),采用基于
HttpURLConnection 的
HurlStack,如果小于
9,采用基于
HttpClient 的
HttpClientStack。

if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
得到了 HttpStack,然后通过它构造一个代表网络(Network)的具体实现BasicNetwork。

接着构造一个代表缓存(Cache)的基于
Disk 的具体实现DiskBasedCache。

最后将网络(Network)对象和缓存(Cache)对象传入构建一个
RequestQueue,启动这个
RequestQueue,并返回。

Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
我们平时大多采用Volly.newRequestQueue(context)的默认实现,构建
RequestQueue。

通过源码可以看出,我们可以抛开 Volley
工具类构建自定义的 RequestQueue,采用自定义的HttpStatck,采用自定义的Network实现,采用自定义的
Cache 实现等来构建RequestQueue。

优秀框架的高可拓展性的魅力来源于此啊

(2). HttpURLConnection
和 AndroidHttpClient(HttpClient
的封装)如何选择及原因:


在 Froyo(2.2)
之前,HttpURLConnection
有个重大 Bug,调用
close() 函数会影响连接池,导致连接复用失效,所以在
Froyo 之前使用
HttpURLConnection 需要关闭
keepAlive。

另外在 Gingerbread(2.3) HttpURLConnection
默认开启了 gzip
压缩,提高了 HTTPS
的性能,Ice Cream Sandwich(4.0) HttpURLConnection
支持了请求结果缓存。

再加上 HttpURLConnection
本身 API
相对简单,所以对 Android
来说,在 2.3
之后建议使用 HttpURLConnection,之前建议使用
AndroidHttpClient。

(3).
关于 User Agent


通过代码我们发现如果是使用 AndroidHttpClient,Volley
还会将请求头中的 User-Agent
字段设置为 App
的 ${packageName}/${versionCode},如果异常则使用
"volley/0",不过这个获取
User-Agent 的操作应该放到
if else 内部更合适。而对于
HttpURLConnection 却没有任何操作,为什么呢?

如果用 Fiddler
或 Charles
对数据抓包我们会发现,我们会发现
HttpURLConnection 默认是有
User-Agent 的,类似:

Dalvik/1.6.0 (Linux; U; Android 4.1.1; Google Nexus 4 - 4.1.1 - API 16 - 768x1280_1 Build/JRO03S)
经常用 WebView
的同学会也许会发现似曾相识,是的,WebView
默认的 User-Agent
也是这个。实际在请求发出之前,会检测 User-Agent
是否为空,如果不为空,则加上系统默认 User-Agent。在
Android 2.1 之后,我们可以通过

String userAgent = System.getProperty("http.agent");
得到系统默认的 User-Agent,Volley
如果希望自定义 User-Agent,可在自定义
Request 中重写
getHeaders() 函数

@Override
public Map<String, String> getHeaders() throws AuthFailureError {
// self-defined user agent
Map<String, String> headerMap = new HashMap<String, String>();
headerMap.put("User-Agent", "android-open-project-analysis/1.0");
return headerMap;
}
4.2.2 Request.java

代表一个网络请求的抽象类。我们通过构建一个Request类的非抽象子类(StringRequest、JsonRequest、ImageRequest
或自定义)对象,并将其加入到·RequestQueue·中来完成一次网络请求操作。

Volley 支持
8 种
Http 请求方式 GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH

Request 类中包含了请求
url,请求请求方式,请求
Header,请求
Body,请求的优先级等信息。

因为是抽象类,子类必须重写的两个方法。

abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
子类重写此方法,将网络返回的原生字节内容,转换成合适的类型。此方法会在工作线程中被调用。

abstract protected void deliverResponse(T response);
子类重写此方法,将解析成合适类型的内容传递给它们的监听回调。

以下两个方法也经常会被重写

public byte[] getBody()
重写此方法,可以构建用于 POST、PUT、PATCH
请求方式的 Body
内容。

protected Map<String, String> getParams()
在上面getBody函数没有被重写情况下,此方法的返回值会被 key、value
分别编码后拼装起来转换为字节码作为 Body
内容。

4.2.3 RequestQueue.java

Volley 框架的核心类,将请求
Request 加入到一个运行的RequestQueue中,来完成请求操作。

(1). 主要成员变量

RequestQueue
中维护了两个基于优先级的 Request
队列,缓存请求队列和网络请求队列。

放在缓存请求队列中的 Request,将通过缓存获取数据;放在网络请求队列中的
Request,将通过网络获取数据。

private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<?>>();
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<Request<?>>();
维护了一个正在进行中,尚未完成的请求集合。

private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
维护了一个等待请求的集合,如果一个请求正在被处理并且可以被缓存,后续的相同 url
的请求,将进入此等待队列。

private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<String, Queue<Request<?>>>();
(2). 启动队列

创建出 RequestQueue
以后,调用 start
方法,启动队列。

/**
* 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();
}
}
start 方法中,开启一个缓存调度线程CacheDispatcher
n 个网络调度线程NetworkDispatcher,这里
n 默认为 4,存在优化的余地,比如可以根据
CPU 核数以及网络类型计算更合适的并发数。

缓存调度线程不断的从缓存请求队列中取出 Request
去处理,网络调度线程不断的从网络请求队列中取出 Request
去处理。

(3). 加入请求

public <T> Request<T> add(Request<T> request);
流程图如下:

(4). 请求完成

void finish(Request<?> request)
Request 请求结束

(1). 首先从正在进行中请求集合mCurrentRequests中移除该请求。

(2). 然后查找请求等待集合mWaitingRequests中是否存在等待的请求,如果存在,则将等待队列移除,并将等待队列所有的请求添加到缓存请求队列中,让缓存请求处理线程CacheDispatcher自动处理。

(5). 请求取消

public void cancelAll(RequestFilter filter)
public void cancelAll(final Object tag)
取消当前请求集合中所有符合条件的请求。

filter 参数表示可以按照自定义的过滤器过滤需要取消的请求。

tag 表示按照Request.setTag设置好的 tag
取消请求,比如同属于某个 Activity
的。

4.2.4 CacheDispatcher.java

一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery 去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。

(1). 成员变量

BlockingQueue<Request<?>> mCacheQueue 缓存请求队列

BlockingQueue<Request<?>> mNetworkQueue 网络请求队列

Cache mCache 缓存类,代表了一个可以获取请求结果,存储请求结果的缓存

ResponseDelivery mDelivery 请求结果传递类

(2). 处理流程图

4.2.5 NetworkDispatcher.java

一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给 ResponseDelivery
去执行后续处理,并判断结果是否要进行缓存。

(1). 成员变量

BlockingQueue<Request<?>> mQueue 网络请求队列

Network mNetwork 网络类,代表了一个可以执行请求的网络

Cache mCache 缓存类,代表了一个可以获取请求结果,存储请求结果的缓存

ResponseDelivery mDelivery 请求结果传递类,可以传递请求的结果或者错误到调用者

(2). 处理流程图

4.2.6 Cache.java

缓存接口,代表了一个可以获取请求结果,存储请求结果的缓存。

(1). 主要方法:

public Entry get(String key); 通过

1fff7
key 获取请求的缓存实体

public void put(String key, Entry entry); 存入一个请求的缓存实体

public void remove(String key); 移除指定的缓存实体

public void clear(); 清空缓存

(2). 代表缓存实体的内部类
Entry

成员变量和方法

byte[] data 请求返回的数据(Body
实体)

String etag Http
响应首部中用于缓存新鲜度验证的 ETag

long serverDate Http
响应首部中的响应产生时间

long ttl 缓存的过期时间

long softTtl 缓存的新鲜时间

Map<String, String> responseHeaders 响应的
Headers

boolean isExpired() 判断缓存是否过期,过期缓存不能继续使用

boolean refreshNeeded() 判断缓存是否新鲜,不新鲜的缓存需要发到服务端做新鲜度的检测

4.2.7 DiskBasedCache.java

继承 Cache
类,基于 Disk
的缓存实现类。

(1). 主要方法:

public synchronized void initialize() 初始化,扫描缓存目录得到所有缓存数据摘要信息放入内存。

public synchronized Entry get(String key) 从缓存中得到数据。先从摘要信息中得到摘要信息,然后读取缓存数据文件得到内容。

public synchronized void put(String key, Entry entry) 将数据存入缓存内。先检查缓存是否会满,会则先删除缓存中部分数据,然后再新建缓存文件。

private void pruneIfNeeded(int neededSpace) 检查是否能再分配
neededSpace 字节的空间,如果不能则删除缓存中部分数据。

public synchronized void clear() 清空缓存。 public
synchronized void remove(String key) 删除缓存中某个元素。

(2). CacheHeader


CacheHeader 是缓存文件摘要信息,存储在缓存文件的头部,与上面的Cache.Entry相似。

4.2.8 NoCache.java

继承 Cache
类,不做任何操作的缓存实现类,可将它作为构建RequestQueue的参数以实现一个不带缓存的请求队列。

4.2.9 Network.java

代表网络的接口,处理网络请求。

唯一的方法,用于执行特定请求。

public NetworkResponse performRequest(Request<?> request) throws VolleyError;
4.2.10 NetworkResponse.java

Network中方法 performRequest
的返回值,Request的 parseNetworkResponse(…)
方法入参,是 Volley
中用于内部 Response
转换的一级。

封装了网络请求响应的 StatusCode,Headers
和 Body
等。

(1). 成员变量

int statusCode Http
响应状态码

byte[] data Body
数据

Map<String, String> headers 响应
Headers

boolean notModified 表示是否为
304 响应

long networkTimeMs 请求耗时

(2). Volley 的内部
Response 转换流程图

从上到下表示从得到数据后一步步的处理,箭头旁的注释表示该步处理后的实体类。

4.2.11 BasicNetwork.java

实现 Network,Volley
中默认的网络接口实现类。调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。

主要实现了以下功能:

(1). 利用
HttpStack 执行网络请求。

(2). 如果
Request 中带有实体信息,如
Etag,Last-Modify 等,则进行缓存新鲜度的验证,并处理
304(Not Modify)响应。

(3). 如果发生超时,认证失败等错误,进行重试操作,直到成功、抛出异常(不满足重试策略等)结束。

4.2.12 HttpStack.java

用于处理 Http
请求,返回请求结果的接口。目前 Volley
中的实现有基于 HttpURLConnection
的 HurlStack
和 基于 Apache HttpClient
的 HttpClientStack。

唯一方法,执行请求

public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
执行 Request
代表的请求,第二个参数表示发起请求之前,添加额外的请求 Headers。

4.2.13 HttpClientStack.java

实现 HttpStack
接口,利用 Apache
的 HttpClient
进行各种请求方式的请求。

基本就是 org.apache.http
包下面相关类的常见用法,不做详解,不过与下面 HttpURLConnection
做下对比就能发现 HttpURLConnection
的 API
相对简单的多。

4.2.14 HurlStack.java

实现 HttpStack
接口,利用 Java
的 HttpURLConnection
进行各种请求方式的请求。

4.2.15 Response.java

封装了经过解析后的数据,用于传输。并且有两个内部接口 Listener
和 ErrorListener
分别可表示请求失败和成功后的回调。

Response
的构造函数被私有化,而通过两个函数名更易懂的静态方法构建对象。

4.2.16 ByteArrayPool.java

byte[] 的回收池,用于
byte[] 的回收再利用,减少了内存的分配和回收。 主要通过一个元素长度从小到大排序的ArrayList作为
byte[] 的缓存,另有一个按使用时间先后排序的ArrayList属性用于缓存满时清理元素。

public synchronized void returnBuf(byte[] buf)
将用过的 byte[]
回收,根据 byte[]
长度按照从小到大的排序将 byte[]
插入到缓存中合适位置。

public synchronized byte[] getBuf(int len)
获取长度不小于 len
的 byte[],遍历缓存,找出第一个长度大于传入参数len的
byte[],并返回;如果最终没有合适的 byte[],new
一个返回。

private synchronized void trim()
当缓存的 byte
超过预先设置的大小时,按照先进先出的顺序删除最早的 byte[]。

4.2.17 PoolingByteArrayOutputStream.java

继承 ByteArrayOutputStream,原始
ByteArrayOutputStream 中用于接受写入
bytes 的
buf,每次空间不足时便会
new 更大容量的
byte[],而
PoolingByteArrayOutputStream 使用了
ByteArrayPool 作为
Byte[] 缓存来减少这种操作,从而提高性能。

4.2.18 HttpHeaderParser.java

Http header 的解析工具类,在
Volley 中主要作用是用于解析
Header 从而判断返回结果是否需要缓存,如果需要返回
Header 中相关信息。

有三个方法

public static long parseDateAsEpoch(String dateStr)
解析时间,将 RFC1123
的时间格式,解析成 epoch
时间

public static String parseCharset(Map<String, String> headers)
解析编码集,在 Content-Type
首部中获取编码集,如果没有找到,默认返回 ISO-8859-1

public static Cache.Entry parseCacheHeaders(NetworkResponse response)
比较重要的方法,通过网络响应中的缓存控制 Header
和 Body
内容,构建缓存实体。如果 Header
的 Cache-Control
字段含有no-cache或no-store表示不缓存,返回
null。

(1). 根据
Date 首部,获取响应生成时间

(2). 根据
ETag 首部,获取响应实体标签

(3). 根据
Cache-Control
和 Expires
首部,计算出缓存的过期时间,和缓存的新鲜度时间

两点需要说明下:

1.没有处理Last-Modify首部,而是处理存储了Date首部,并在后续的新鲜度验证时,使用Date来构建If-Modified-Since。
这与 Http 1.1
的语义有些违背。

2.计算过期时间,Cache-Control
首部优先于 Expires
首部。

4.2.19 RetryPolicy.java

重试策略接口

有三个方法:

public int getCurrentTimeout();
获取当前请求用时(用于 Log)

public int getCurrentRetryCount();
获取已经重试的次数(用于 Log)

public void retry(VolleyError error) throws VolleyError;
确定是否重试,参数为这次异常的具体信息。在请求异常时此接口会被调用,可在此函数实现中抛出传入的异常表示停止重试。

4.2.20 DefaultRetryPolicy.java

实现 RetryPolicy,Volley
默认的重试策略实现类。主要通过在 retry(…)
函数中判断重试次数是否达到上限确定是否继续重试。

其中mCurrentRetryCount变量表示已经重试次数。

mBackoffMultiplier表示每次重试之前的 timeout
该乘以的因子。

mCurrentTimeoutMs变量表示当前重试的 timeout
时间,会以mBackoffMultiplier作为因子累计前几次重试的 timeout。

4.2.21 ResponseDelivery.java

请求结果的传输接口,用于传递请求结果或者请求错误。

有三个方法:

public void postResponse(Request<?> request, Response<?> response);
此方法用于传递请求结果,request 和 response 参数分别表示请求信息和返回结果信息。

public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
此方法用于传递请求结果,并在完成传递后执行 Runnable。

public void postError(Request<?> request, VolleyError error);
此方法用于传输请求错误。

4.2.22 ExecutorDelivery.java

请求结果传输接口具体实现类。

在 Handler
对应线程中传输缓存调度线程或者网络调度线程中产生的请求结果或请求错误,会在请求成功的情况下调用
Request.deliverResponse(…) 函数,失败时调用
Request.deliverError(…) 函数。

4.2.23 StringRequest.java

继承 Request
类,代表了一个返回值为
String 的请求。将网络返回的结果数据解析为
String 类型。通过构造函数的
listener 传参,支持请求成功后的
onResponse(…) 回调。

4.2.24 JsonRequest.java

抽象类,继承自 Request,代表了
body 为
JSON 的请求。提供了构建
JSON 请求参数的方法。

4.2.25 JsonObjectRequest.java

继承自 JsonRequest,将网络返回的结果数据解析为
JSONObject 类型。

4.2.26 JsonArrayRequest.java

继承自 JsonRequest,将网络返回的结果数据解析为
JSONArray 类型。

4.2.27 ImageRequest.java

继承 Request
类,代表了一个返回值为 Image
的请求。将网络返回的结果数据解析为 Bitmap
类型。

可以设置图片的最大宽度和最大高度,并计算出合适尺寸返回。每次最多解析一张图片防止 OOM。

4.2.28 ImageLoader.java

封装了 ImageRequst
的方便使用的图片加载工具类。

1.可以设置自定义的ImageCache,可以是内存缓存,也可以是
Disk 缓存,将获取的图片缓存起来,重复利用,减少请求。

2.可以定义图片请求过程中显示的图片和请求失败后显示的图片。

3.相同请求(相同地址,相同大小)只发送一个,可以避免重复请求。

// TODO

4.2.29 NetworkImageView.java

利用 ImageLoader,可以加载网络图片的
ImageView

有三个公开的方法:

public void setDefaultImageResId(int defaultImage)
设置默认图片,加载图片过程中显示。

public void setErrorImageResId(int errorImage)
设置错误图片,加载图片失败后显示。

public void setImageUrl(String url, ImageLoader imageLoader)
设置网络图片的 Url
和 ImageLoader,将利用这个
ImageLoader 去获取网络图片。

如果有新的图片加载请求,会把这个 ImageView
上旧的加载请求取消。

4.2.30 ClearCacheRequest.java

用于人为清空 Http
缓存的请求。

添加到 RequestQueue
后能很快执行,因为优先级很高,为Priority.IMMEDIATE。并且清空缓存的方法mCache.clear()写在了isCanceled()方法体中,能最早的得到执行。

ClearCacheRequest
的写法不敢苟同,目前看来唯一的好处就是可以将清空缓存操作也当做一个请求。而在isCanceled()中做清空操作本身就造成了歧义,不看源码没人知道在NetworkDispatcher run
方法循环的过程中,isCanceled()这个读操作竟然做了可能造成缓存被清空。只能跟源码的解释一样当做一个 Hack
操作。

4.2.31 Authenticator.java

身份认证接口,用于基本认证或者摘要认证。这个类是 Volley
用于和身份验证打通的接口,比如 OAuth,不过目前的使用不是特别广泛和
Volley 的内部结合也不是特别紧密。

4.2.32 AndroidAuthenticator.java

继承 Authenticator,基于
Android AccountManager 的认证交互实现类。

4.2.33 VolleyLog.java

Volley 的
Log 工具类。

4.2.34 VolleyError.java

Volley 中所有错误异常的父类,继承自
Exception,可通过此类设置和获取
NetworkResponse 或者请求的耗时。

4.2.35 AuthFailureError.java

继承自 VolleyError,代表请求认证失败错误,如
RespondeCode 的
401 和
403。

4.2.36 NetworkError.java

继承自 VolleyError,代表网络错误。

4.2.37 ParseError.java

继承自 VolleyError,代表内容解析错误。

4.2.38 ServerError.java

继承自 VolleyError,代表服务端错误。

4.2.39 TimeoutError.java

继承自 VolleyError,代表请求超时错误。

4.2.40 NoConnectionError.java

继承自 NetworkError,代表无法建立连接错误。

5. 杂谈

5.1 关于
Http 缓存

Volley 构建了一套相对完整的符合
Http 语义的缓存机制。

优点和特点

(1). 根据Cache-Control和Expires首部来计算缓存的过期时间。如果两个首部都存在情况下,以Cache-Control为准。

(2). 利用If-None-Match和If-Modified-Since对过期缓存或者不新鲜缓存,进行请求再验证,并处理
304 响应,更新缓存。

(3). 默认的缓存实现,将缓存以文件的形式存储在
Disk,程序退出后不会丢失。

我个人认为的不足之处

缓存的再验证方面,在构建If-Modified-Since请求首部时,Volley
使用了服务端响应的Date首部,没有使用Last-Modified首部。整个框架没有使用Last-Modified首部。这与
Http 语义不符。

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.serverDate > 0) {
Date refTime = new Date(entry.serverDate);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
服务端根据请求时通过If-Modified-Since首部传过来的时间,判断资源文件是否在If-Modified-Since时间 以后 有改动,如果有改动,返回新的请求结果。如果没有改动,返回
304 not modified。

Last-Modified代表了资源文件的最后修改时间。通常使用这个首部构建If-Modified-Since的时间。

Date代表了响应产生的时间,正常情况下Date时间在Last-Modified时间之后。也就是Date>=Last-Modified。

通过以上原理,既然Date>=Last-Modified。那么我利用Date构建,也是完全正确的。

可能的问题出在服务端的 Http
实现上,如果服务端完全遵守 Http
语义,采用时间比较的方式来验证If-Modified-Since,判断服务器资源文件修改时间是不是在If-Modified-Since之后。那么使用Date完全正确。


可是有的服务端实现不是比较时间,而是直接的判断服务器资源文件修改时间,是否和If-Modified-Since所传时间相等。这样使用Date就不能实现正确的再验证,因为Date的时间总不会和服务器资源文件修改时间相等。

尽管使用Date可能出现的不正确情况,归结于服务端没有正确的实现
Http 语义。

但我还是希望 Volley
也能完全正确的实现 Http
语义,至少同时处理Last-Modified和Date,并且优先使用Last-Modified。


5.2 Bug

(1). BasicNetwork.performRequest(…)

如下代码:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
……
while (true) {
……
try {
……
} catch (IOException e) {
int statusCode = 0;
NetworkResponse networkResponse = null;
……
if (responseContents != null) {
……
} else {
throw new NetworkError(networkResponse);
}
}
}
}
BasicNetwork.performRequest(…)
最后的

throw new NetworkError(networkResponse);
应该是

throw new NetworkError(e);
更合理。

经过前三篇文章的学习,Volley的用法我们已经掌握的差不多了,但是对于Volley的工作原理,恐怕有很多朋友还不是很清楚。因此,本篇文章中我们就来一起阅读一下Volley的源码,将它的工作流程整体地梳理一遍。同时,这也是Volley系列的最后一篇文章了。

其实,Volley的官方文档中本身就附有了一张Volley的工作流程图,如下图所示。

多数朋友突然看到一张这样的图,应该会和我一样,感觉一头雾水吧?没错,目前我们对Volley背后的工作原理还没有一个概念性的理解,直接就来看这张图自然会有些吃力。不过没关系,下面我们就去分析一下Volley的源码,之后再重新来看这张图就会好理解多了。

说起分析源码,那么应该从哪儿开始看起呢?这就要回顾一下Volley的用法了,还记得吗,使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:

1. public static RequestQueue newRequestQueue(Context context) {

2. return newRequestQueue(context, null);
3. }

这个方法仅仅只有一行代码,只是调用了newRequestQueue()的方法重载,并给第二个参数传入null。那我们看下带有两个参数的newRequestQueue()方法中的代码,如下所示:

1. public static RequestQueue newRequestQueue(Context context, HttpStack stack) {

2. File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
3. String userAgent = "volley/0";

4. try {
5. String packageName = context.getPackageName();

6. PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
7. userAgent = packageName + "/" + info.versionCode;

8. } catch (NameNotFoundException e) {
9. }

10. if (stack == null) {
11. if (Build.VERSION.SDK_INT >= 9) {

12. stack = new HurlStack();
13. } else {

14. stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
15. }

16. }
17. Network network = new BasicNetwork(stack);

18. RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
19. queue.start();

20. return queue;
21. }

可以看到,这里在第10行判断如果stack是等于null的,则去创建一个HttpStack对象,这里会判断如果手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例。实际上HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的,这里为什么这样选择呢?可以参考我之前翻译的一篇文章Android访问网络,使用HttpURLConnection还是HttpClient?

创建好了HttpStack之后,接下来又创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的,紧接着new出一个RequestQueue对象,并调用它的start()方法进行启动,然后将RequestQueue返回,这样newRequestQueue()的方法就执行结束了。

那么RequestQueue的start()方法内部到底执行了什么东西呢?我们跟进去瞧一瞧:

1. public void start() {

2. stop(); // Make sure any currently running dispatchers are stopped.
3. // Create the cache dispatcher and start it.

4. mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
5. mCacheDispatcher.start();

6. // Create network dispatchers (and corresponding threads) up to the pool size.
7. for (int i = 0; i < mDispatchers.length; i++) {

8. NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
9. mCache, mDelivery);

10. mDispatchers[i] = networkDispatcher;
11. networkDispatcher.start();

12. }
13. }

这里先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法,接着在一个for循环里去创建NetworkDispatcher的实例,并分别调用它们的start()方法。这里的CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中CacheDispatcher是缓存线程,NetworkDispatcher是网络请求线程。

得到了RequestQueue之后,我们只需要构建出相应的Request,然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了,那么不用说,add()方法的内部肯定有着非常复杂的逻辑,我们来一起看一下:

1. public <T> Request<T> add(Request<T> request) {

2. // Tag the request as belonging to this queue and add it to the set of current requests.
3. request.setRequestQueue(this);

4. synchronized (mCurrentRequests) {
5. mCurrentRequests.add(request);

6. }
7. // Process requests in the order they are added.

8. request.setSequence(getSequenceNumber());
9. request.addMarker("add-to-queue");

10. // If the request is uncacheable, skip the cache queue and go straight to the network.
11. if (!request.shouldCache()) {

12. mNetworkQueue.add(request);
13. return request;

14. }
15. // Insert request into stage if there's already a request with the same cache key in flight.

16. synchronized (mWaitingRequests) {
17. String cacheKey = request.getCacheKey();

18. if (mWaitingRequests.containsKey(cacheKey)) {
19. // There is already a request in flight. Queue up.

20. Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
21. if (stagedRequests == null) {

22. stagedRequests = new LinkedList<Request<?>>();
23. }

24. stagedRequests.add(request);
25. mWaitingRequests.put(cacheKey, stagedRequests);

26. if (VolleyLog.DEBUG) {
27. VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);

28. }
29. } else {

30. // Insert 'null' queue for this cacheKey, indicating there is now a request in
31. // flight.

32. mWaitingRequests.put(cacheKey, null);
33. mCacheQueue.add(request);

34. }
35. return request;

36. }
37. }

可以看到,在第11行的时候会判断当前的请求是否可以缓存,如果不能缓存则在第12行直接将这条请求加入网络请求队列,可以缓存的话则在第33行将这条请求加入缓存队列。在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。

OK,那么既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下CacheDispatcher中的run()方法,代码如下所示:

1. public class CacheDispatcher extends Thread {

2.
3. ……

4.
5. @Override

6. public void run() {
7. if (DEBUG) VolleyLog.v("start new dispatcher");

8. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
9. // Make a blocking call to initialize the cache.

10. mCache.initialize();
11. while (true) {

12. try {
13. // Get a request from the cache triage queue, blocking until

14. // at least one is available.
15. final Request<?> request = mCacheQueue.take();

16. request.addMarker("cache-queue-take");
17. // If the request has been canceled, don't bother dispatching it.

18. if (request.isCanceled()) {
19. request.finish("cache-discard-canceled");

20. continue;
21. }

22. // Attempt to retrieve this item from cache.
23. Cache.Entry entry = mCache.get(request.getCacheKey());

24. if (entry == null) {
25. request.addMarker("cache-miss");

26. // Cache miss; send off to the network dispatcher.
27. mNetworkQueue.put(request);

28. continue;
29. }

30. // If it is completely expired, just send it to the network.
31. if (entry.isExpired()) {

32. request.addMarker("cache-hit-expired");
33. request.setCacheEntry(entry);

34. mNetworkQueue.put(request);
35. continue;

36. }
37. // We have a cache hit; parse its data for delivery back to the request.

38. request.addMarker("cache-hit");
39. Response<?> response = request.parseNetworkResponse(

40. new NetworkResponse(entry.data, entry.responseHeaders));
41. request.addMarker("cache-hit-parsed");

42. if (!entry.refreshNeeded()) {
43. // Completely unexpired cache hit. Just deliver the response.

44. mDelivery.postResponse(request, response);
45. } else {

46. // Soft-expired cache hit. We can deliver the cached response,
47. // but we need to also send the request to the network for

48. // refreshing.
49. request.addMarker("cache-hit-refresh-needed");

50. request.setCacheEntry(entry);
51. // Mark the response as intermediate.

52. response.intermediate = true;
53. // Post the intermediate response back to the user and have

54. // the delivery then forward the request along to the network.
55. mDelivery.postResponse(request, response, new Runnable() {

56. @Override
57. public void run() {

58. try {
59. mNetworkQueue.put(request);

60. } catch (InterruptedException e) {
61. // Not much we can do about this.

62. }
63. }

64. });
65. }

66. } catch (InterruptedException e) {
67. // We may have been interrupted because it was time to quit.

68. if (mQuit) {
69. return;

70. }
71. continue;

72. }
73. }

74. }
75. }

代码有点长,我们只挑重点看。首先在11行可以看到一个while(true)循环,说明缓存线程始终是在运行的,接着在第23行会尝试从缓存当中取出响应结果,如何为空的话则把这条请求加入到网络请求队列中,如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中,否则就认为不需要重发网络请求,直接使用缓存中的数据即可。之后会在第39行调用Request的parseNetworkResponse()方法来对数据进行解析,再往后就是将解析出来的数据进行回调了,这部分代码我们先跳过,因为它的逻辑和NetworkDispatcher后半部分的逻辑是基本相同的,那么我们等下合并在一起看就好了,先来看一下NetworkDispatcher中是怎么处理网络请求队列的,代码如下所示:

1. public class NetworkDispatcher extends Thread {

2. ……
3. @Override

4. public void run() {
5. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

6. Request<?> request;
7. while (true) {

8. try {
9. // Take a request from the queue.

10. request = mQueue.take();
11. } catch (InterruptedException e) {

12. // We may have been interrupted because it was time to quit.
13. if (mQuit) {

14. return;
15. }

16. continue;
17. }

18. try {
19. request.addMarker("network-queue-take");

20. // If the request was cancelled already, do not perform the
21. // network request.

22. if (request.isCanceled()) {
23. request.finish("network-discard-cancelled");

24. continue;
25. }

26. addTrafficStatsTag(request);
27. // Perform the network request.

28. NetworkResponse networkResponse = mNetwork.performRequest(request);
29. request.addMarker("network-http-complete");

30. // If the server returned 304 AND we delivered a response already,
31. // we're done -- don't deliver a second identical response.

32. if (networkResponse.notModified && request.hasHadResponseDelivered()) {
33. request.finish("not-modified");

34. continue;
35. }

36. // Parse the response here on the worker thread.
37. Response<?> response = request.parseNetworkResponse(networkResponse);

38. request.addMarker("network-parse-complete");
39. // Write to cache if applicable.

40. // TODO: Only update cache metadata instead of entire record for 304s.
41. if (request.shouldCache() && response.cacheEntry != null) {

42. mCache.put(request.getCacheKey(), response.cacheEntry);
43. request.addMarker("network-cache-written");

44. }
45. // Post the response back.

46. request.markDelivered();
47. mDelivery.postResponse(request, response);

48. } catch (VolleyError volleyError) {
49. parseAndDeliverNetworkError(request, volleyError);

50. } catch (Exception e) {
51. VolleyLog.e(e, "Unhandled exception %s", e.toString());

52. mDelivery.postError(request, new VolleyError(e));
53. }

54. }
55. }

56. }
同样地,在第7行我们看到了类似的while(true)循环,说明网络请求线程也是在不断运行的。在第28行的时候会调用Network的performRequest()方法来去发送网络请求,而Network是一个接口,这
4000
里具体的实现是BasicNetwork,我们来看下它的performRequest()方法,如下所示:

1. public class BasicNetwork implements Network {

2. ……
3. @Override

4. public NetworkResponse performRequest(Request<?> request) throws VolleyError {
5. long requestStart = SystemClock.elapsedRealtime();

6. while (true) {
7. HttpResponse httpResponse = null;

8. byte[] responseContents = null;
9. Map<String, String> responseHeaders = new HashMap<String, String>();

10. try {
11. // Gather headers.

12. Map<String, String> headers = new HashMap<String, String>();
13. addCacheHeaders(headers, request.getCacheEntry());

14. httpResponse = mHttpStack.performRequest(request, headers);
15. StatusLine statusLine = httpResponse.getStatusLine();

16. int statusCode = statusLine.getStatusCode();
17. responseHeaders = convertHeaders(httpResponse.getAllHeaders());

18. // Handle cache validation.
19. if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

20. return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
21. request.getCacheEntry() == null ? null : request.getCacheEntry().data,

22. responseHeaders, true);
23. }

24. // Some responses such as 204s do not have content. We must check.
25. if (httpResponse.getEntity() != null) {

26. responseContents = entityToBytes(httpResponse.getEntity());
27. } else {

28. // Add 0 byte response as a way of honestly representing a
29. // no-content request.

30. responseContents = new byte[0];
31. }

32. // if the request is slow, log it.
33. long requestLifetime = SystemClock.elapsedRealtime() - requestStart;

34. logSlowRequests(requestLifetime, request, responseContents, statusLine);
35. if (statusCode < 200 || statusCode > 299) {

36. throw new IOException();
37. }

38. return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
39. } catch (Exception e) {

40. ……
41. }

42. }
43. }

44. }
这段方法中大多都是一些网络请求细节方面的东西,我们并不需要太多关心,需要注意的是在第14行调用了HttpStack的performRequest()方法,这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象。前面已经说过,这两个对象的内部实际就是分别使用HttpURLConnection和HttpClient来发送网络请求的,我们就不再跟进去阅读了,之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回。

在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。还记得我们在上一篇文章中学习的自定义Request的方式吗?其中parseNetworkResponse()这个方法就是必须要重写的。

在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据,代码如下所示:

1. private class ResponseDeliveryRunnable implements Runnable {

2. private final Request mRequest;
3. private final Response mResponse;

4. private final Runnable mRunnable;
5.

6. public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
7. mRequest = request;

8. mResponse = response;
9. mRunnable = runnable;

10. }
11.

12. @SuppressWarnings("unchecked")
13. @Override

14. public void run() {
15. // If this request has canceled, finish it and don't deliver.

16. if (mRequest.isCanceled()) {
17. mRequest.finish("canceled-at-delivery");

18. return;
19. }

20. // Deliver a normal response or error, depending.
21. if (mResponse.isSuccess()) {

22. mRequest.deliverResponse(mResponse.result);
23. } else {

24. mRequest.deliverError(mResponse.error);
25. }

26. // If this is an intermediate response, add a marker, otherwise we're done
27. // and the request can be finished.

28. if (mResponse.intermediate) {
29. mRequest.addMarker("intermediate-response");

30. } else {
31. mRequest.finish("done");

32. }
33. // If we have been provided a post-delivery runnable, run it.

34. if (mRunnable != null) {
35. mRunnable.run();

36. }
37. }

38. }
代码虽然不多,但我们并不需要行行阅读,抓住重点看即可。其中在第22行调用了Request的deliverResponse()方法,有没有感觉很熟悉?没错,这个就是我们在自定义Request时需要重写的另外一个方法,每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。

好了,到这里我们就把Volley的完整执行流程全部梳理了一遍,你是不是已经感觉已经很清晰了呢?对了,还记得在文章一开始的那张流程图吗,刚才还不能理解,现在我们再来重新看下这张图:

其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。

怎么样,是不是感觉现在理解这张图已经变得轻松简单了?好了,到此为止我们就把Volley的用法和源码全部学习完了,相信你已经对Volley非常熟悉并可以将它应用到实际项目当中了,那么Volley完全解析系列的文章到此结束,感谢大家有耐心看到最后。

Volley源码解释

1. 功能介绍

1.1. Volley

Volley 是
Google 推出的
Android 异步网络请求框架和图片加载框架。在
Google I/O 2013 大会上发布。

名字由来:a burst or emission of many things or a large amount at once

发布演讲时候的配图

从名字由来和配图中无数急促的火箭可以看出 Volley
的特点:特别适合数据量小,通信频繁的网络操作。(个人认为 Android
应用中绝大多数的网络操作都属于这种类型)。

1.2 Volley 的主要特点

(1). 扩展性强。Volley
中大多是基于接口的设计,可配置性强。

(2). 一定程度符合
Http 规范,包括返回
ResponseCode(2xx、3xx、4xx、5xx)的处理,请求头的处理,缓存机制的支持等。并支持重试及优先级定义。

(3). 默认
Android2.3 及以上基于
HttpURLConnection,2.3
以下基于 HttpClient
实现,这两者的区别及优劣在4.2.1 Volley中具体介绍。

(4). 提供简便的图片加载工具。

2. 总体设计

2.1 总体设计图

上面是 Volley
的总体设计图,主要是通过两种Dispatch Thread不断从RequestQueue中取出请求,根据是否已缓存调用Cache或Network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据,然后交由ResponseDelivery去做结果分发及回调处理。

2.2 Volley 中的概念

简单介绍一些概念,在详细设计中会仔细介绍。

Volley 的调用比较简单,通过
newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue后,只需要往这个RequestQueue不断
add Request 即可。

Volley:Volley
对外暴露的 API,通过
newRequestQueue(…) 函数新建并启动一个请求队列RequestQueue。

Request:表示一个请求的抽象类。StringRequest、JsonRequest、ImageRequest 都是它的子类,表示某种类型的请求。

RequestQueue:表示请求队列,里面包含一个CacheDispatcher(用于处理走缓存请求的调度线程)、NetworkDispatcher数组(用于处理走网络请求的调度线程),一个ResponseDelivery(返回结果分发接口),通过
start() 函数启动时会启动CacheDispatcher和NetworkDispatchers。

CacheDispatcher:一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。

NetworkDispatcher:一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery去执行后续处理,并判断结果是否要进行缓存。

ResponseDelivery:返回结果分发接口,目前只有基于ExecutorDelivery的在入参
handler 对应线程内进行分发。

HttpStack:处理 Http
请求,返回请求结果。目前 Volley
中有基于 HttpURLConnection
的HurlStack和
基于 Apache HttpClient
的HttpClientStack。

Network:调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。

Cache:缓存请求结果,Volley
默认使用的是基于 sdcard
的DiskBasedCache。NetworkDispatcher得到请求结果后判断是否需要存储在
Cache,CacheDispatcher会从 Cache
中取缓存结果。

3. 流程图

Volley 请求流程图

上图是 Volley
请求时的流程图,在 Volley
的发布演讲中给出,我在这里将其用中文重新画出。


4. 详细设计

4.1 类关系图

这是 Volley
框架的主要类关系图
图中红色圈内的部分,组成了
Volley 框架的核心,围绕 RequestQueue
类,将各个功能点以组合的方式结合在了一起。各个功能点也都是以接口或者抽象类的形式提供。

红色圈外面的部分,在 Volley
源码中放在了 toolbox
包中,作为 Volley
为各个功能点提供的默认的具体实现。

通过类图我们看出, Volley
有着非常好的拓展性。通过各个功能点的接口,我们可以给出自定义的,更符合我们需求的具体实现。

多用组合,少用继承;针对接口编程,不针对具体实现编程。

优秀框架的设计,令人叫绝,受益良多。

4.2 核心类功能介绍

4.2.1 Volley.java

这个和 Volley
框架同名的类,其实是个工具类,作用是构建一个可用于添加网络请求的RequestQueue对象。

(1).
主要函数


Volley.java
有两个重载的静态方法。

public static RequestQueue newRequestQueue(Context context)

public static RequestQueue newRequestQueue(Context context, HttpStack stack)
第一个方法的实现调用了第二个方法,传 HttpStack
参数为 null。

第二个方法中,如果 HttpStatck
参数为 null,则如果系统在
Gingerbread 及之后(即
API Level >= 9),采用基于
HttpURLConnection 的
HurlStack,如果小于
9,采用基于
HttpClient 的
HttpClientStack。

if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
得到了 HttpStack,然后通过它构造一个代表网络(Network)的具体实现BasicNetwork。

接着构造一个代表缓存(Cache)的基于
Disk 的具体实现DiskBasedCache。

最后将网络(Network)对象和缓存(Cache)对象传入构建一个
RequestQueue,启动这个
RequestQueue,并返回。

Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
我们平时大多采用Volly.newRequestQueue(context)的默认实现,构建
RequestQueue。

通过源码可以看出,我们可以抛开 Volley
工具类构建自定义的 RequestQueue,采用自定义的HttpStatck,采用自定义的Network实现,采用自定义的
Cache 实现等来构建RequestQueue。

优秀框架的高可拓展性的魅力来源于此啊

(2). HttpURLConnection
和 AndroidHttpClient(HttpClient
的封装)如何选择及原因:


在 Froyo(2.2)
之前,HttpURLConnection
有个重大 Bug,调用
close() 函数会影响连接池,导致连接复用失效,所以在
Froyo 之前使用
HttpURLConnection 需要关闭
keepAlive。

另外在 Gingerbread(2.3) HttpURLConnection
默认开启了 gzip
压缩,提高了 HTTPS
的性能,Ice Cream Sandwich(4.0) HttpURLConnection
支持了请求结果缓存。

再加上 HttpURLConnection
本身 API
相对简单,所以对 Android
来说,在 2.3
之后建议使用 HttpURLConnection,之前建议使用
AndroidHttpClient。

(3).
关于 User Agent


通过代码我们发现如果是使用 AndroidHttpClient,Volley
还会将请求头中的 User-Agent
字段设置为 App
的 ${packageName}/${versionCode},如果异常则使用
"volley/0",不过这个获取
User-Agent 的操作应该放到
if else 内部更合适。而对于
HttpURLConnection 却没有任何操作,为什么呢?

如果用 Fiddler
或 Charles
对数据抓包我们会发现,我们会发现
HttpURLConnection 默认是有
User-Agent 的,类似:

Dalvik/1.6.0 (Linux; U; Android 4.1.1; Google Nexus 4 - 4.1.1 - API 16 - 768x1280_1 Build/JRO03S)
经常用 WebView
的同学会也许会发现似曾相识,是的,WebView
默认的 User-Agent
也是这个。实际在请求发出之前,会检测 User-Agent
是否为空,如果不为空,则加上系统默认 User-Agent。在
Android 2.1 之后,我们可以通过

String userAgent = System.getProperty("http.agent");
得到系统默认的 User-Agent,Volley
如果希望自定义 User-Agent,可在自定义
Request 中重写
getHeaders() 函数

@Override
public Map<String, String> getHeaders() throws AuthFailureError {
// self-defined user agent
Map<String, String> headerMap = new HashMap<String, String>();
headerMap.put("User-Agent", "android-open-project-analysis/1.0");
return headerMap;
}
4.2.2 Request.java

代表一个网络请求的抽象类。我们通过构建一个Request类的非抽象子类(StringRequest、JsonRequest、ImageRequest
或自定义)对象,并将其加入到·RequestQueue·中来完成一次网络请求操作。

Volley 支持
8 种
Http 请求方式 GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH

Request 类中包含了请求
url,请求请求方式,请求
Header,请求
Body,请求的优先级等信息。

因为是抽象类,子类必须重写的两个方法。

abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
子类重写此方法,将网络返回的原生字节内容,转换成合适的类型。此方法会在工作线程中被调用。

abstract protected void deliverResponse(T response);
子类重写此方法,将解析成合适类型的内容传递给它们的监听回调。

以下两个方法也经常会被重写

public byte[] getBody()
重写此方法,可以构建用于 POST、PUT、PATCH
请求方式的 Body
内容。

protected Map<String, String> getParams()
在上面getBody函数没有被重写情况下,此方法的返回值会被 key、value
分别编码后拼装起来转换为字节码作为 Body
内容。

4.2.3 RequestQueue.java

Volley 框架的核心类,将请求
Request 加入到一个运行的RequestQueue中,来完成请求操作。

(1). 主要成员变量

RequestQueue
中维护了两个基于优先级的 Request
队列,缓存请求队列和网络请求队列。

放在缓存请求队列中的 Request,将通过缓存获取数据;放在网络请求队列中的
Request,将通过网络获取数据。

private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<?>>();
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<Request<?>>();
维护了一个正在进行中,尚未完成的请求集合。

private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
维护了一个等待请求的集合,如果一个请求正在被处理并且可以被缓存,后续的相同 url
的请求,将进入此等待队列。

private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<String, Queue<Request<?>>>();
(2). 启动队列

创建出 RequestQueue
以后,调用 start
方法,启动队列。

/**
* 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();
}
}
start 方法中,开启一个缓存调度线程CacheDispatcher
n 个网络调度线程NetworkDispatcher,这里
n 默认为 4,存在优化的余地,比如可以根据
CPU 核数以及网络类型计算更合适的并发数。

缓存调度线程不断的从缓存请求队列中取出 Request
去处理,网络调度线程不断的从网络请求队列中取出 Request
去处理。

(3). 加入请求

public <T> Request<T> add(Request<T> request);
流程图如下:

(4). 请求完成

void finish(Request<?> request)
Request 请求结束

(1). 首先从正在进行中请求集合mCurrentRequests中移除该请求。

(2). 然后查找请求等待集合mWaitingRequests中是否存在等待的请求,如果存在,则将等待队列移除,并将等待队列所有的请求添加到缓存请求队列中,让缓存请求处理线程CacheDispatcher自动处理。

(5). 请求取消

public void cancelAll(RequestFilter filter)
public void cancelAll(final Object tag)
取消当前请求集合中所有符合条件的请求。

filter 参数表示可以按照自定义的过滤器过滤需要取消的请求。

tag 表示按照Request.setTag设置好的 tag
取消请求,比如同属于某个 Activity
的。

4.2.4 CacheDispatcher.java

一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给ResponseDelivery 去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入NetworkDispatcher去调度处理。

(1). 成员变量

BlockingQueue<Request<?>> mCacheQueue 缓存请求队列

BlockingQueue<Request<?>> mNetworkQueue 网络请求队列

Cache mCache 缓存类,代表了一个可以获取请求结果,存储请求结果的缓存

ResponseDelivery mDelivery 请求结果传递类

(2). 处理流程图

4.2.5 NetworkDispatcher.java

一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给 ResponseDelivery
去执行后续处理,并判断结果是否要进行缓存。

(1). 成员变量

BlockingQueue<Request<?>> mQueue 网络请求队列

Network mNetwork 网络类,代表了一个可以执行请求的网络

Cache mCache 缓存类,代表了一个可以获取请求结果,存储请求结果的缓存

ResponseDelivery mDelivery 请求结果传递类,可以传递请求的结果或者错误到调用者

(2). 处理流程图

4.2.6 Cache.java

缓存接口,代表了一个可以获取请求结果,存储请求结果的缓存。

(1). 主要方法:

public Entry get(String key); 通过
key 获取请求的缓存实体

public void put(String key, Entry entry); 存入一个请求的缓存实体

public void remove(String key); 移除指定的缓存实体

public void clear(); 清空缓存

(2). 代表缓存实体的内部类
Entry

成员变量和方法

byte[] data 请求返回的数据(Body
实体)

String etag Http
响应首部中用于缓存新鲜度验证的 ETag

long serverDate Http
响应首部中的响应产生时间

long ttl 缓存的过期时间

long softTtl 缓存的新鲜时间

Map<String, String> responseHeaders 响应的
Headers

boolean isExpired() 判断缓存是否过期,过期缓存不能继续使用

boolean refreshNeeded() 判断缓存是否新鲜,不新鲜的缓存需要发到服务端做新鲜度的检测

4.2.7 DiskBasedCache.java

继承 Cache
类,基于 Disk
的缓存实现类。

(1). 主要方法:

public synchronized void initialize() 初始化,扫描缓存目录得到所有缓存数据摘要信息放入内存。

public synchronized Entry get(String key) 从缓存中得到数据。先从摘要信息中得到摘要信息,然后读取缓存数据文件得到内容。

public synchronized void put(String key, Entry entry) 将数据存入缓存内。先检查缓存是否会满,会则先删除缓存中部分数据,然后再新建缓存文件。

private void pruneIfNeeded(int neededSpace) 检查是否能再分配
neededSpace 字节的空间,如果不能则删除缓存中部分数据。

public synchronized void clear() 清空缓存。 public
synchronized void remove(String key) 删除缓存中某个元素。

(2). CacheHeader


CacheHeader 是缓存文件摘要信息,存储在缓存文件的头部,与上面的Cache.Entry相似。

4.2.8 NoCache.java

继承 Cache
类,不做任何操作的缓存实现类,可将它作为构建RequestQueue的参数以实现一个不带缓存的请求队列。

4.2.9 Network.java

代表网络的接口,处理网络请求。

唯一的方法,用于执行特定请求。

public NetworkResponse performRequest(Request<?> request) throws VolleyError;
4.2.10 NetworkResponse.java

Network中方法 performRequest
的返回值,Request的 parseNetworkResponse(…)
方法入参,是 Volley
中用于内部 Response
转换的一级。

封装了网络请求响应的 StatusCode,Headers
和 Body
等。

(1). 成员变量

int statusCode Http
响应状态码

byte[] data Body
数据

Map<String, String> headers 响应
Headers

boolean notModified 表示是否为
304 响应

long networkTimeMs 请求耗时

(2). Volley 的内部
Response 转换流程图

从上到下表示从得到数据后一步步的处理,箭头旁的注释表示该步处理后的实体类。

4.2.11 BasicNetwork.java

实现 Network,Volley
中默认的网络接口实现类。调用HttpStack处理请求,并将结果转换为可被ResponseDelivery处理的NetworkResponse。

主要实现了以下功能:

(1). 利用
HttpStack 执行网络请求。

(2). 如果
Request 中带有实体信息,如
Etag,Last-Modify 等,则进行缓存新鲜度的验证,并处理
304(Not Modify)响应。

(3). 如果发生超时,认证失败等错误,进行重试操作,直到成功、抛出异常(不满足重试策略等)结束。

4.2.12 HttpStack.java

用于处理 Http
请求,返回请求结果的接口。目前 Volley
中的实现有基于 HttpURLConnection
的 HurlStack
和 基于 Apache HttpClient
的 HttpClientStack。

唯一方法,执行请求

public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
执行 Request
代表的请求,第二个参数表示发起请求之前,添加额外的请求 Headers。

4.2.13 HttpClientStack.java

实现 HttpStack
接口,利用 Apache
的 HttpClient
进行各种请求方式的请求。

基本就是 org.apache.http
包下面相关类的常见用法,不做详解,不过与下面 HttpURLConnection
做下对比就能发现 HttpURLConnection
的 API
相对简单的多。

4.2.14 HurlStack.java

实现 HttpStack
接口,利用 Java
的 HttpURLConnection
进行各种请求方式的请求。

4.2.15 Response.java

封装了经过解析后的数据,用于传输。并且有两个内部接口 Listener
和 ErrorListener
分别可表示请求失败和成功后的回调。

Response
的构造函数被私有化,而通过两个函数名更易懂的静态方法构建对象。

4.2.16 ByteArrayPool.java

byte[] 的回收池,用于
byte[] 的回收再利用,减少了内存的分配和回收。 主要通过一个元素长度从小到大排序的ArrayList作为
byte[] 的缓存,另有一个按使用时间先后排序的ArrayList属性用于缓存满时清理元素。

public synchronized void returnBuf(byte[] buf)
将用过的 byte[]
回收,根据 byte[]
长度按照从小到大的排序将 byte[]
插入到缓存中合适位置。

public synchronized byte[] getBuf(int len)
获取长度不小于 len
的 byte[],遍历缓存,找出第一个长度大于传入参数len的
byte[],并返回;如果最终没有合适的 byte[],new
一个返回。

private synchronized void trim()
当缓存的 byte
超过预先设置的大小时,按照先进先出的顺序删除最早的 byte[]。

4.2.17 PoolingByteArrayOutputStream.java

继承 ByteArrayOutputStream,原始
ByteArrayOutputStream 中用于接受写入
bytes 的
buf,每次空间不足时便会
new 更大容量的
byte[],而
PoolingByteArrayOutputStream 使用了
ByteArrayPool 作为
Byte[] 缓存来减少这种操作,从而提高性能。

4.2.18 HttpHeaderParser.java

Http header 的解析工具类,在
Volley 中主要作用是用于解析
Header 从而判断返回结果是否需要缓存,如果需要返回
Header 中相关信息。

有三个方法

public static long parseDateAsEpoch(String dateStr)
解析时间,将 RFC1123
的时间格式,解析成 epoch
时间

public static String parseCharset(Map<String, String> headers)
解析编码集,在 Content-Type
首部中获取编码集,如果没有找到,默认返回 ISO-8859-1

public static Cache.Entry parseCacheHeaders(NetworkResponse response)
比较重要的方法,通过网络响应中的缓存控制 Header
和 Body
内容,构建缓存实体。如果 Header
的 Cache-Control
字段含有no-cache或no-store表示不缓存,返回
null。

(1). 根据
Date 首部,获取响应生成时间

(2). 根据
ETag 首部,获取响应实体标签

(3). 根据
Cache-Control
和 Expires
首部,计算出缓存的过期时间,和缓存的新鲜度时间

两点需要说明下:

1.没有处理Last-Modify首部,而是处理存储了Date首部,并在后续的新鲜度验证时,使用Date来构建If-Modified-Since。
这与 Http 1.1
的语义有些违背。

2.计算过期时间,Cache-Control
首部优先于 Expires
首部。

4.2.19 RetryPolicy.java

重试策略接口

有三个方法:

public int getCurrentTimeout();
获取当前请求用时(用于 Log)

public int getCurrentRetryCount();
获取已经重试的次数(用于 Log)

public void retry(VolleyError error) throws VolleyError;
确定是否重试,参数为这次异常的具体信息。在请求异常时此接口会被调用,可在此函数实现中抛出传入的异常表示停止重试。

4.2.20 DefaultRetryPolicy.java

实现 RetryPolicy,Volley
默认的重试策略实现类。主要通过在 retry(…)
函数中判断重试次数是否达到上限确定是否继续重试。

其中mCurrentRetryCount变量表示已经重试次数。

mBackoffMultiplier表示每次重试之前的 timeout
该乘以的因子。

mCurrentTimeoutMs变量表示当前重试的 timeout
时间,会以mBackoffMultiplier作为因子累计前几次重试的 timeout。

4.2.21 ResponseDelivery.java

请求结果的传输接口,用于传递请求结果或者请求错误。

有三个方法:

public void postResponse(Request<?> request, Response<?> response);
此方法用于传递请求结果,request 和 response 参数分别表示请求信息和返回结果信息。

public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
此方法用于传递请求结果,并在完成传递后执行 Runnable。

public void postError(Request<?> request, VolleyError error);
此方法用于传输请求错误。

4.2.22 ExecutorDelivery.java

请求结果传输接口具体实现类。

在 Handler
对应线程中传输缓存调度线程或者网络调度线程中产生的请求结果或请求错误,会在请求成功的情况下调用
Request.deliverResponse(…) 函数,失败时调用
Request.deliverError(…) 函数。

4.2.23 StringRequest.java

继承 Request
类,代表了一个返回值为
String 的请求。将网络返回的结果数据解析为
String 类型。通过构造函数的
listener 传参,支持请求成功后的
onResponse(…) 回调。

4.2.24 JsonRequest.java

抽象类,继承自 Request,代表了
body 为
JSON 的请求。提供了构建
JSON 请求参数的方法。

4.2.25 JsonObjectRequest.java

继承自 JsonRequest,将网络返回的结果数据解析为
JSONObject 类型。

4.2.26 JsonArrayRequest.java

继承自 JsonRequest,将网络返回的结果数据解析为
JSONArray 类型。

4.2.27 ImageRequest.java

继承 Request
类,代表了一个返回值为 Image
的请求。将网络返回的结果数据解析为 Bitmap
类型。

可以设置图片的最大宽度和最大高度,并计算出合适尺寸返回。每次最多解析一张图片防止 OOM。

4.2.28 ImageLoader.java

封装了 ImageRequst
的方便使用的图片加载工具类。

1.可以设置自定义的ImageCache,可以是内存缓存,也可以是
Disk 缓存,将获取的图片缓存起来,重复利用,减少请求。

2.可以定义图片请求过程中显示的图片和请求失败后显示的图片。

3.相同请求(相同地址,相同大小)只发送一个,可以避免重复请求。

// TODO

4.2.29 NetworkImageView.java

利用 ImageLoader,可以加载网络图片的
ImageView

有三个公开的方法:

public void setDefaultImageResId(int defaultImage)
设置默认图片,加载图片过程中显示。

public void setErrorImageResId(int errorImage)
设置错误图片,加载图片失败后显示。

public void setImageUrl(String url, ImageLoader imageLoader)
设置网络图片的 Url
和 ImageLoader,将利用这个
ImageLoader 去获取网络图片。

如果有新的图片加载请求,会把这个 ImageView
上旧的加载请求取消。

4.2.30 ClearCacheRequest.java

用于人为清空 Http
缓存的请求。

添加到 RequestQueue
后能很快执行,因为优先级很高,为Priority.IMMEDIATE。并且清空缓存的方法mCache.clear()写在了isCanceled()方法体中,能最早的得到执行。

ClearCacheRequest
的写法不敢苟同,目前看来唯一的好处就是可以将清空缓存操作也当做一个请求。而在isCanceled()中做清空操作本身就造成了歧义,不看源码没人知道在NetworkDispatcher run
方法循环的过程中,isCanceled()这个读操作竟然做了可能造成缓存被清空。只能跟源码的解释一样当做一个 Hack
操作。

4.2.31 Authenticator.java

身份认证接口,用于基本认证或者摘要认证。这个类是 Volley
用于和身份验证打通的接口,比如 OAuth,不过目前的使用不是特别广泛和
Volley 的内部结合也不是特别紧密。

4.2.32 AndroidAuthenticator.java

继承 Authenticator,基于
Android AccountManager 的认证交互实现类。

4.2.33 VolleyLog.java

Volley 的
Log 工具类。

4.2.34 VolleyError.java

Volley 中所有错误异常的父类,继承自
Exception,可通过此类设置和获取
NetworkResponse 或者请求的耗时。

4.2.35 AuthFailureError.java

继承自 VolleyError,代表请求认证失败错误,如
RespondeCode 的
401 和
403。

4.2.36 NetworkError.java

继承自 VolleyError,代表网络错误。

4.2.37 ParseError.java

继承自 VolleyError,代表内容解析错误。

4.2.38 ServerError.java

继承自 VolleyError,代表服务端错误。

4.2.39 TimeoutError.java

继承自 VolleyError,代表请求超时错误。

4.2.40 NoConnectionError.java

继承自 NetworkError,代表无法建立连接错误。

5. 杂谈

5.1 关于
Http 缓存

Volley 构建了一套相对完整的符合
Http 语义的缓存机制。

优点和特点

(1). 根据Cache-Control和Expires首部来计算缓存的过期时间。如果两个首部都存在情况下,以Cache-Control为准。

(2). 利用If-None-Match和If-Modified-Since对过期缓存或者不新鲜缓存,进行请求再验证,并处理
304 响应,更新缓存。

(3). 默认的缓存实现,将缓存以文件的形式存储在
Disk,程序退出后不会丢失。

我个人认为的不足之处

缓存的再验证方面,在构建If-Modified-Since请求首部时,Volley
使用了服务端响应的Date首部,没有使用Last-Modified首部。整个框架没有使用Last-Modified首部。这与
Http 语义不符。

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.serverDate > 0) {
Date refTime = new Date(entry.serverDate);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
服务端根据请求时通过If-Modified-Since首部传过来的时间,判断资源文件是否在If-Modified-Since时间 以后 有改动,如果有改动,返回新的请求结果。如果没有改动,返回
304 not modified。

Last-Modified代表了资源文件的最后修改时间。通常使用这个首部构建If-Modified-Since的时间。

Date代表了响应产生的时间,正常情况下Date时间在Last-Modified时间之后。也就是Date>=Last-Modified。

通过以上原理,既然Date>=Last-Modified。那么我利用Date构建,也是完全正确的。

可能的问题出在服务端的 Http
实现上,如果服务端完全遵守 Http
语义,采用时间比较的方式来验证If-Modified-Since,判断服务器资源文件修改时间是不是在If-Modified-Since之后。那么使用Date完全正确。


可是有的服务端实现不是比较时间,而是直接的判断服务器资源文件修改时间,是否和If-Modified-Since所传时间相等。这样使用Date就不能实现正确的再验证,因为Date的时间总不会和服务器资源文件修改时间相等。

尽管使用Date可能出现的不正确情况,归结于服务端没有正确的实现
Http 语义。

但我还是希望 Volley
也能完全正确的实现 Http
语义,至少同时处理Last-Modified和Date,并且优先使用Last-Modified。


5.2 Bug

(1). BasicNetwork.performRequest(…)

如下代码:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
……
while (true) {
……
try {
……
} catch (IOException e) {
int statusCode = 0;
NetworkResponse networkResponse = null;
……
if (responseContents != null) {
……
} else {
throw new NetworkError(networkResponse);
}
}
}
}
BasicNetwork.performRequest(…)
最后的

throw new NetworkError(networkResponse);
应该是

throw new NetworkError(e);
更合理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: