读懂Volley,必须要理解的几个问题

Posted 似水流云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了读懂Volley,必须要理解的几个问题相关的知识,希望对你有一定的参考价值。

Volley是一个应用广泛的网络请求开源框架,由Google于2013年推出,它可扩展性性强,适合于数据量小,请求频繁的网络请求,用来加载网络图片也很方便,GitHub地址:https://github.com/google/volley

关于Volley的使用介绍和源码解析,网络资料很多,这里就不再写了,可参考:
想看框架原理:Volley 源码解析
更详细的从使用到源码解析:郭霖的《Volley全解析》

本篇文章主要是记录一下我在使用及读Volley源码时的一些问题的思考,包括以下问题:

一、volley的并发请求是怎么实现的
二、线程里的while无限循环不会影响性能吗?
三、Request的优先级是怎么处理的?
四、为什么没有用线程池来处理请求?
五、有些Request会被放到一个等待的Map里(RequestQueue里的mWaitingRequests这个Map),它的作用是什么?
六、对响应的处理是怎么切换回主线程的?
七、Volley对HTTP的304响应是怎么处理的?
八、为什么Volley不适合数据量大的场景?
九、可缓存的网络请求结果,是以什么为key进行保存的?

一、volley的并发请求是怎么实现的

Volley维护了一个缓存调度线程CacheDispatcher和 n 个网络调度线程NetworkDispatcher,这里 n 默认为 4。
Volley会根据Request是否可缓存,确定该Request是发起网络请求来处理,还是从缓存里直接得到结果(是否可缓存,可以在构造每个Request的时候自行定义)。

缓存调度线程的run()方法是一个while(true)无限循环,不断从缓存请求队列中取出 Request去处理(尝试从缓存中拿结果,如果拿不到结果,或者结果数据已经过期,则把Request放到网络请求队列里)。
网络调度线程的run()方法也是while(true)无限循环,不断从网络请求队列取出Request去处理。这样就实现了并发请求。

二、线程里的while无限循环不会影响性能吗?

这里就要介绍下,Volley使用的缓存请求队列和网络请求队列都是无界有序的阻塞队列(PriorityBlockingQueue),它的特点就是从队列里取元素的时候,如果队列为空,则调用此方法的线程会挂起,直至队列有元素可取,线程才会继续运行。同样放入元素的时候,如果队列满了也会挂起,直至队列有空间可放(但是PriorityBlockingQueue是无最大限制的,所以不会满)。

所以如果队列里的请求都处理完了,线程就都会处于挂起状态,而不会继续循环运行。

另外,PriorityBlockingQueue是线程安全的,所以不必担心n个线程都会从网络请求队列里取Request的同步问题。

三、Request的优先级是怎么处理的?

同样用到了PriorityBlockingQueue。
Request类实现了Comparable接口并实现了这个接口的compareTo(Request other)方法,用以比较各个Request的优先级。
在把每个Request加入PriorityBlockingQueue的时候,就会自动根据这个Request的优先级加入队列合适的位置,这也是PriorityBlockingQueue的特点之一。

而从队列取出Request的时候,都是从队列头部取出的,所以取出的就是优先级最高的。

Volley默认对Request划分了四种优先级。

    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }

Request比较优先级的时候先比较Priority属性,如果相同再比较它的mSequence属性。
默认每个Request的优先级都是Priority.NORMAL,可以自行设定。

四、为什么没有用线程池来处理请求?

一个说法是,Volley默认用到了四个线程来同时处理网络请求,其实就是线程池的作用了。这样的好处是避免了创建线程和回收线程的开销,毕竟网络请求的开销基本上要大于消息队列的处理,这样可以提高性能。

但是我觉得用线程池的好处就是,线程池会根据请求数量,动态增加或者减少线程数,而用Volley的做法,如果短时间来了很多请求的话,也只能处理几个,其他的都得排队等待,短时间无法得到响应。网上有很多人也实现了用线程池的Volley版本,例如下面。

ThreadPoolExecutor threadPoolExecutor = ThreadPoolExecutor)Executors.newFixedThreadPool(mDispatchers.length);
// 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;
     threadPoolExecutor.submit(networkDispatcher);
     //networkDispatcher.start();
}

五、有些Request会被放到一个等待的Map里(RequestQueue里的mWaitingRequests这个Map),它的作用是什么?

先回答,这个Map的作用是,避免同样的Request重复进行网络请求。

详细来说,如果放入缓存请求队列里的好几个Request都是同样的请求,但是缓存里还拿不到数据,按优先级,先拿出来的那个Request就会先被放到网络请求队列里去执行,因为网络请求的结果可能得一会才能返回并存入缓存,在这期间,缓存请求队列的其他几个相同的Request可能都会被到网络请求队列里去执行了,这就产生了多个不必要的网络请求,浪费了资源。

而用了这个等待Map,如果有重复的网络请求,之前一个正在处理中,后面来的,就会被暂时放入这个Map里。在这个Map里,key就是Request的URL,相同URL的Request都处于同一个队列元素里。(这个Map类型是HashMap<String, Queue<Request<?>>>)。最先的Request被处理完成之后,后面重复的Request会直接从缓存里拿数据。

更详细的源码分析,如下,可以略过。
————————————————

每来一个新的Request,是这样处理的:

先判断是否可缓存。如果不可缓存,就放到网络请求队列里去执行。
如果可缓存,再判断这个Map里是否含有以这个Request的URL为key的键值对。
第一次肯定是没有这个key的,那Map就会添加这个键值对,key就是这个Request的url,对应的value为null。同时把这个Request放到缓存队列里去执行。
这时如果有同样的Request再来时(请求的URL是一样的),这时这个Map里已经有了这个key,那么就会创建新的Queue作为key对应的value,并在Queue里放入这个Request。
再有同样的Request来,直接会放到这个Queue里。
上面的3~5步,看代码更清楚,在RequestQueue的add()方法:

            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                	// 上面的第4步
                    stagedRequests = new LinkedList<Request<?>>();
                }
                // 第4,第5步都会到这里
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
            } else {
                // 上面的第3步
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }

那么这个mWaitingRequests什么时候会把添加的元素移除呢?
每个Request完成以后,等待Map会根据判断它是否含有这个Request的url对应的key,如果有的话,就把key对应的value(也就是处于等待状态的请求队列Queue)从Map移除,并添加到缓存请求队列里去处理。此流程见下RequestQueue的finish()方法:

        if (request.shouldCache()) {
            synchronized (mWaitingRequests) {
                String cacheKey = request.getCacheKey();
                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
                if (waitingRequests != null) {
                    // Process all queued up requests. They won\'t be considered as in flight, but
                    // that\'s not a problem as the cache has been primed by \'request\'.
                    mCacheQueue.addAll(waitingRequests);
                }
            }
        }

六、对响应的处理是怎么切换回主线程的?

我们已知Volley的网络请求和缓存处理都是在子线程,那么处理完成后,得到的结果会交给一个结果传递器来处理,这个传递器是在一开始构造RequestQueue的时候,传入构造方法的:

    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

从new Handler(Looper.getMainLooper())可以知道,这个handler是主线程的handler。在每个Request完成之后,结果会包装成Runnable对象,传入这个handler的post方法里进行处理。

七、Volley对HTTP的304响应是怎么处理的?

http的304状态码的含义是:

如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not
Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似。从而
保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。

在Volley的流程里,如果网络请求返回304,就直接使用缓存的数据作为结果,然后结束这个请求。
NetworkDispatcher的run()方法:

public void run() {
    ...
    NetworkResponse networkResponse = mNetwork.performRequest(request);
    request.addMarker("network-http-complete");
    // 如果是304并且已经将缓存分发出去里,就直接结束这个请求
    if (networkResponse.notModified && request.hasHadResponseDelivered()) {
        request.finish("not-modified");
        continue;
    }
    ...
    }
}

下面是BasicNetwork.performRequest()里的相关处理:

        if (statusCode == HttpStatus.SC_NOT_MODIFIED) { //SC_NOT_MODIFIED即304
            return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
                    request.getCacheEntry().data, responseHeaders, true);
        }

八、为什么Volley不适合数据量大的场景?

http传输的数据,不管是发起请求的数据,还是得到的数据,都是会读取到内存中。那么如果几个线程同时访问数据量大的请求,就容易OOM了。

九、可缓存的网络请求结果,是以什么为key进行保存的?

从上面的分析我们已经知道,如果我们设置一个请求是可以从缓存里拿结果的,那就会优先从缓存里拿结果。网络请求的方式有GET, POST等多种,如果是POST,那么提交的参数可能是不一样的,那请求结果是怎么保存的呢?

我们来看下Request类里的getCacheKey(),即获取保存的数据的Key:

    /** Returns the cache key for this request. By default, this is the URL. */
    public String getCacheKey() {
        String url = getUrl();
        // If this is a GET request, just use the URL as the key.
        // For callers using DEPRECATED_GET_OR_POST, we assume the method is GET, which matches
        // legacy behavior where all methods had the same cache key. We can\'t determine which method
        // will be used because doing so requires calling getPostBody() which is expensive and may
        // throw AuthFailureError.
        // TODO(#190): Remove support for non-GET methods.
        int method = getMethod();
        if (method == Method.GET || method == Method.DEPRECATED_GET_OR_POST) {
            return url;
        }
        return Integer.toString(method) + \'-\' + url;
    }

注释也说的比较清楚了,如果是GET方式,那么key就是请求的url。如果是其他方式,那么key是Integer.toString(method) + ‘-’ + url。但是将来会移除对非GET方式的支持。
也就是说如果我们想使用缓存结果的话,最好还是用GET方式来请求数据。

本文转自:https://blog.csdn.net/fenggering/article/details/88563418

以上是关于读懂Volley,必须要理解的几个问题的主要内容,如果未能解决你的问题,请参考以下文章

了解SSL必须要懂得密码技术

Volley框架简介

如何读代码

读懂执行计划

必须要懂得的密码技术

Volley框架源代码分析