前言:
Volley是一个发布以来流行至今的网络框架,他的好处也人尽皆知。高并发、不易OOM、代码简介、可拓展性强等等,几乎大多数项目都会集成它或它的改良版。但Volley为什么在高并发上性能好?官网为什么说它只适合体积小的网络请求呢?
通过本章我们可以了解如下问题:
1.了解Volley的工作原理。
2.在同时有两个相同的请求时,如何优化处理?
3.请求数量大、资源竞争激烈的情况,如何优化线程?
4.为什么说Volley只适合于小数据请求(不超过3M)?
目录:
一、简单介绍Volley的工作流程
二、NetworkDispatcher工作原理
三、CacheDispatcher工作原理
一、简单介绍Volley的工作流程,包含如下知识点:
1.了解Volley的工作原理。
2.在同时有两个相同的请求时,如何优化处理?
我们从一个获取String类型数据的网络请求例子来阐述Volley的基本工作流程。
[java] view
plain copy
- RequestQueue requestQueue = Volley.newRequestQueue(this);
- StringRequest stringRequest = new StringRequest("http://www.google.com", new Response.Listener<String>() {
- @Override
- public void onResponse(String response) {
- }
- }, new Response.ErrorListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- }
- });
- requestQueue.add(stringRequest);
Volley.java
首先是RequestQueue对象的创建,Volley.newRequestQueue(this)方法是直接调用Volley工具类
[java] view
plain copy
- public static RequestQueue newRequestQueue(Context context) {
- return newRequestQueue(context, null);
- }
继续向下调
[java] view
plain copy
- public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
- //create cache file
- File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
- String userAgent = "volley/0";
- try {
- String packageName = context.getPackageName();
- PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
- userAgent = packageName + "/" + info.versionCode;
- } catch (NameNotFoundException e) {
- }
- if (stack == null) {
- if (Build.VERSION.SDK_INT >= 9) {
- stack = new HurlStack();
- } else {
- // Prior to Gingerbread, HttpUrlConnection was unreliable.
- // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
- stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
- }
- }
- Network network = new BasicNetwork(stack);
- RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
- queue.start();
- return queue;
- }
这里面会真正创建一个RequestQueue对象然后返回它。RequestQueue相当于一个调度员,负责调度所有事物。
直接看第26行,start()方法是开启网络请求线程和缓存请求线程的入口。
RequestQueue.java
接下来看start()代码。
[java] view
plain copy
- 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();
- }
- }
主要流程:
1.如果线程已经在运行,关闭所有线程(2行);
2.开启一个缓存线程(4 - 5行);
3.开启4个网络线程(8 - 13行);
五个线程开启后,我们在自己的Java类中写的这个方法会将Request请求对象放入任务队列中,但具体是放在哪个队列里面呢?这就要从在文章最上面我们自己写的最后一行代码requestQueue.add(stringRequest);这个方法寻找答案了。
RequestQueue.java
add()源码有4个主要的队列对象
mCurrentRequests:用来记录所有任务对象,每有一个网络请求,都会加入到这个队列中,而如果完成任务或者取消任务后,会把这个Request移除队列。
mNetworkQueue:网络请求队列,如果有Request对象加入到这个队列则直接处理。
mCacheQueue:缓存队列,加入队列后会检测有无缓存,如果没有缓存或者是过期缓存则转入到mNetworkQueue队列中。
mWaitingRequests:Map类型,其中可以存储若干个Queue<Request<?>队列,用来存储相同请求的Request对象。举个例子,比如2个网络请求R1,R2都想通过某URL获取数据,R1率先请求,请求过程中R2开始请求,由于R1还在未完成所以还在mWaitingRequests中的某一Queue<Request<?>队列中,所以R2就自动加入到这个队列中,等待R1请求完毕后会把同一个请求结果发给R1和R2。
[java] view
plain copy
- 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;
- }
- // Insert request into stage if there‘s already a request with the same cache key in flight.
- synchronized (mWaitingRequests) {
- //Url
- String cacheKey = request.getCacheKey();
- if (mWaitingRequests.containsKey(cacheKey)) {
- // There is already a request in flight. Queue up.
- Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
- if (stagedRequests == null) {
- stagedRequests = new LinkedList<Request<?>>();
- }
- stagedRequests.add(request);
- mWaitingRequests.put(cacheKey, stagedRequests);
- if (VolleyLog.DEBUG) {
- VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
- }
- } else {
- // Insert ‘null‘ queue for this cacheKey, indicating there is now a request in
- // flight.
- mWaitingRequests.put(cacheKey, null);
- mCacheQueue.add(request);
- }
- return request;
- }
- }
主要流程:
1、当前Request对象加入当前队列mCurrentRequests中,设置序列号,添加标记(3 - 10行);
2、如果当前Request对象不允许缓存,直接放如网络队列中(13 - 16行);
3、获取URL,在mWaitingRequests中查找是否有相同URL请求,没有则加入到缓存队列,有则在mWaitingRequests中对应的队列中加入自己,等待那个相同的URL请求完成后直接把成果复制过来(19 - 38行);
这里用到了一个设计模式—"生产者消费者模式”,即通过一个容器来解决强耦合问题,生产者、消费者之间不通讯,生产者只负责生产产品放到阻塞队列中,消费者只负责从阻塞队列中获取产品,平衡了生产者和消费者的处理能力,避免生产者与消费者捆绑的问题,提高了并发处理的能力。
在Volley里面,生产者不一定只是主线程,也有可能是CacheDispatcher,上一个消费者可能是下一个消费者的生产者,阻塞队列是网络请求队列和缓存请求队列。
二、NetworkDispatcher工作原理,包含如下问题
3.请求数量大、资源竞争激烈的情况,如何优化线程?
4.为什么说Volley只适合于小数据请求(不超过3M)?
第一次真正用到NetworkDispatcher的run方法是在上面的第14行代码mNetworkQueue.add(request)
NetworkDispatcher.java
[java] view
plain copy
- public void run() {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- while (true) {
- long startTimeMs = SystemClock.elapsedRealtime();
- Request<?> request;
- try {
- // Take a request from the queue.
- request = mQueue.take();
- } catch (InterruptedException e) {
- // We may have been interrupted because it was time to quit.
- if (mQuit) {
- return;
- }
- continue;
- }
- 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");
- continue;
- }
- 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");
- continue;
- }
- // 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);
- } catch (VolleyError volleyError) {
- volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
- parseAndDeliverNetworkError(request, volleyError);
- } 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);
- }
- }
- }
主要流程:
1、查看mCacheQueue队列中是否有Request对象。有,向下执行;没有,则阻塞当前线程(8行);
那么问题来了,它是如何阻塞和唤醒的线程?(后面会讲)先讨论另一个问题,在任务队列里面有几十上百张图片或字符串等等的加载任务,而此时4个网络线程来不断通过这个任务队列来领取其中的任务来完成资源请求,该如何加锁防止同一资源被多个线程拿到呢?我的反应是用”synchronized”,但点进去看源码才发现不是。
PriorityBlockingQueue.java部分源码
(PriorityBlockingQueue.java)502 - 503行,synchronized和ReentrantLock同为锁机制,synchronized为同步锁的首选,这里使用ReentrantLock对象来管理线程锁,那为什么不用synchronized呢?
ReentrantLock是synchronized不适用时的另一种可选择的高级功能,这里选择用ReentrantLock主要是其在高资源竞争下性能的高效和相对于内置锁,显示锁灵活的操作;但如果是资源竞争不激烈的情况下表现不如synchronized,所以这也是为什么volley在下载大文件时表现不如意的原因之一。这里ReentrantLock使用lockInterruptibly()来获取锁,是因为如果遇到网络或者其他问题该线程不能正常下载可随时中断该线程并向上层代码抛出异常,这样如果一个网络请求失败了取消掉重新获取下一个任务。
(PriorityBlockingQueue.java)506 - 507行,获取队列任务,如果队列内容为空,notEmpty.await()将当前线程阻塞,并释放锁(注意:不是通过unlock()解锁)。知道了阻塞,那如何被唤醒呢?
在RequestQueue类中向队列中添加Request对象时调用的add()方法最终会调用到PriorityBlockingQueue类中的offer()方法,offer()中会调用到Condition对象中的signal()方法来唤起当前线程。这样中断和唤起的流程就通了。
详情请参见ReentrantLock显示锁
2、对interruptibly解释做出了诠释,出现中断请求直接退出(10 - 16行)。
3、进行网络请求,这里深讲的话涉及很多的内容,将在下一章讲解(31行)。
4、将返回来的数据,根据对应类型进行解析(42行)。
5、如果需要缓存则将缓存内容加入到mCache队列里面,下一章讲解(47 - 50行)。
6、将解析内容发送给最初的请求端(53 - 54行)。
总结:逻辑清晰,读懂之后会对线程并发有了进一步的认识。
三、CacheDispatcher工作原理
如果读懂了NetworkDispatcher的工作原理,那么CacheDispatcher也自然会明白了,因为前者几乎覆盖了后者的知识点。
至此三个重要的模块已经全部了解完成。
针对于Volley的设计原理,RequestQueue相当于一个调度员,负责处理各种繁琐的事物,一个app只适合创建一个RequestQueue对象,否则会违背Volley“多执行少调度”的基本原则。