博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Volley 源码解析之网络请求
阅读量:6248 次
发布时间:2019-06-22

本文共 21695 字,大约阅读时间需要 72 分钟。

Volley源码分析三部曲

Volley 是 Google 推出的一款网络通信框架,非常适合数据量小、通信频繁的网络请求,支持并发、缓存和容易扩展、调试等;不过不太适合下载大文件、大量数据的网络请求,因为volley在解析期间将响应放到内存中,我们可以使用okhttp或者系统提供的DownloadManager来下载文件。

一、简单使用

首先在工程引入volley的library:

dependencies {    implementation 'com.android.volley:volley:1.1.1'}复制代码

然后需要我们打开网络权限,我这里直接贴出官网简单请求的示例代码:

final TextView mTextView = (TextView) findViewById(R.id.text);// ...// Instantiate the RequestQueue.RequestQueue queue = Volley.newRequestQueue(this);String url ="http://www.google.com";// Request a string response from the provided URL.StringRequest stringRequest = new StringRequest(Request.Method.GET, url,            new Response.Listener
() { @Override public void onResponse(String response) { // Display the first 500 characters of the response string. mTextView.setText("Response is: "+ response.substring(0,500)); }}, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { mTextView.setText("That didn't work!"); }});// Add the request to the RequestQueue.queue.add(stringRequest);复制代码

使用相对简单,回调直接在主线程,我们取消某个请求直接这样操作:

  1. 定义一个标记添加到requests中

    public static final String TAG = "MyTag";StringRequest stringRequest; // Assume this exists.RequestQueue mRequestQueue;  // Assume this exists.// Set the tag on the request.stringRequest.setTag(TAG);// Add the request to the RequestQueue.mRequestQueue.add(stringRequest);复制代码
  2. 然后我们可以在 onStop() 中取消所有标记的请求

    @Overrideprotected void onStop () {    super.onStop();    if (mRequestQueue != null) {        mRequestQueue.cancelAll(TAG);    }}复制代码

二、源码分析

我们先从Volley这个类入手:

public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {    BasicNetwork network;    if (stack == null) {        if (Build.VERSION.SDK_INT >= 9) {            network = new BasicNetwork(new HurlStack());        } else {            String userAgent = "volley/0";            try {                String packageName = context.getPackageName();                PackageInfo info =                    context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);                userAgent = packageName + "/" + info.versionCode;            } catch (NameNotFoundException e) {            }            network =                new BasicNetwork(                new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));        }    } else {        network = new BasicNetwork(stack);    }    return newRequestQueue(context, network);}private static RequestQueue newRequestQueue(Context context, Network network) {    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);    queue.start();    return queue;}public static RequestQueue newRequestQueue(Context context) {    return newRequestQueue(context, (BaseHttpStack) null);}复制代码

当我们传递一个Context的时候,首先为BaseHttpStack为null,会执行到创建BaseHttpStackBaseHttpStack是一个网络具体的处理请求,Volley默认提供了基于HttpURLCollectionHurlStack和基于HttpClientHttpClientStack。Android6.0移除了HttpClient,Google官方推荐使用HttpURLCollection类作为替换。所以这里在API大于9的版本是用的是HurlStack,为什么这样选择,详情可见这篇博客。我们使用的是默认的构造,BaseHttpStack传入为null,如果我们想使用自定义的okhttp替换底层,我们直接继承HttpStack重写即可,也可以自定义NetworkRequestQueue,Volley的高扩展性充分体现。接下来则创建一个Network对象,然后实例化RequestQueue,首先创建了一个用于缓存的文件夹,然后创建了一个磁盘缓存,将文件缓存到指定目录的硬盘上,默认大小是5M,但是大小可以配置。接下来调用RequestQueuestart()方法进行启动,我们进入这个方法查看一下:

public void start() {    stop();     mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);    mCacheDispatcher.start();    for (int i = 0; i < mDispatchers.length; i++) {        NetworkDispatcher networkDispatcher =            new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);        mDispatchers[i] = networkDispatcher;        networkDispatcher.start();    }}复制代码

开始启动的时候先停止所有的请求线程和网络缓存线程,然后实例化一个缓存线程并运行,然后一个循环开启DEFAULT_NETWORK_THREAD_POOL_SIZE(4)个网络请求线程并运行,一共就是5个线程在后台运行,不断的等待网络请求的到来。

构造了RequestQueue之后,我们调用add()方法将相应的Request传入就开始执行网络请求了,我们看看这个方法:

public 
Request
add(Request
request) { //将请求队列和请求关联起来 request.setRequestQueue(this); //添加到正在请求中但是还未完成的集合中 synchronized (mCurrentRequests) { mCurrentRequests.add(request); } //设置请求的一个序列号,通过原子变量的incrementAndGet方法, //以原子方式给当前值加1并获取新值实现请求的优先级 request.setSequence(getSequenceNumber()); //添加一个调试信息 request.addMarker("add-to-queue"); //如果不需要缓存则直接加到网络的请求队列,默认每一个请求都是缓存的, //如果不需要缓存需要调用Request的setShouldCache方法来修改 if (!request.shouldCache()) { mNetworkQueue.add(request); return request; } //加到缓存的请求队列 mCacheQueue.add(request); return request;}复制代码

关键地方都写了注释,主要作用就是将请求加到请求队列,执行网络请求或者从缓存中获取结果。网络和缓存的请求都是一个优先级阻塞队列,按照优先级出队。上面几个关键步骤,添加到请求集合里面还有设置优先级以及添加到缓存和请求队列都是线程安全的,要么加锁,要么使用线程安全的队列或者原子操作。

接下来我们看看添加到CacheDispatcher缓存请求队列的run方法:

@Overridepublic void run() {    if (DEBUG) VolleyLog.v("start new dispatcher");    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);   //初始化DiskBasedCache的缓存类    mCache.initialize();    while (true) {        try {            processRequest();        } catch (InterruptedException e) {            if (mQuit) {                Thread.currentThread().interrupt();                return;            }            VolleyLog.e(                "Ignoring spurious interrupt of CacheDispatcher thread; "                + "use quit() to terminate it");        }    }}复制代码

接下来的重点是看看processRequest()这个方法:

private void processRequest() throws InterruptedException {    //从缓存队列取出请求    final Request
request = mCacheQueue.take(); processRequest(request);}@VisibleForTestingvoid processRequest(final Request
request) throws InterruptedException { request.addMarker("cache-queue-take"); // 如果请求被取消,我们可以通过RequestQueue的回调接口来监听 if (request.isCanceled()) { request.finish("cache-discard-canceled"); return; } // 从缓存中获取Cache.Entry Cache.Entry entry = mCache.get(request.getCacheKey()); //没有取到缓存 if (entry == null) { request.addMarker("cache-miss"); // 缓存未命中,对于可缓存的请求先去检查是否有相同的请求是否已经在运行中, //如果有的话先加入请求等待队列,等待请求完成,返回true;如果返回false则表示第一次请求 if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { //加入到网络请求的阻塞队列 mNetworkQueue.put(request); } return; } // 如果缓存完全过期,处理过程跟上面类似 if (entry.isExpired()) { request.addMarker("cache-hit-expired"); //设置请求缓存的entry到这个request中 request.setCacheEntry(entry); if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { mNetworkQueue.put(request); } return; } //缓存命中,将数据解析并返回到request的抽象方法中 request.addMarker("cache-hit"); Response
response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); //判断请求结果是否需要刷新 if (!entry.refreshNeeded()) { // 未过期的缓存命中,通过ExecutorDelivery回调给我们的request子类的接口中, // 我们在使用的时候就可以通过StringRequest、JsonRequest等拿到结果, // 切换到主线程也是在这个类里执行的 mDelivery.postResponse(request, response); } else { request.addMarker("cache-hit-refresh-needed"); request.setCacheEntry(entry); // 将这个响应标记为中间值,即这个响应是新鲜的,那么第二个响应正在请求随时到来 response.intermediate = true; if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) { //发起网络请求,这里为什么直接调用上面的mNetworkQueue.put(request);呢, //主要是为了添加一个已经分发的标记,在响应分发的时候不再回调给用户, //不然就回调了两次 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 { //这里第三个参数传递null,不用再去分发,因为已经有相同的请求已经在执行, //直接添加到了等待请求的列表中,然后返回的时候从已经执行的请求收到响应 mDelivery.postResponse(request, response); } }}复制代码

这部分主要是对请求的缓存判断,是否过期以及需要刷新缓存。我们调用取消所有请求或者取消某个请求实质上就是对mCanceled这个变量赋值,然后在缓存线程或者网络线程里面都回去判断这个值,就完成了取消。上面的isExpiredrefreshNeeded,两个区别就是,前者如果过期就直接请求最新的内容,后者就是还在新鲜的时间内,但是把内容返回给用户还是会发起请求,两者一个与ttl值相比,另一个与softTtl相比。

其中有一个WaitingRequestManager,如果有相同的请求那么就需要一个暂存的地方,这个类就是做的这个操作

private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {    //所有等待请求的集合,键是缓存的key    private final Map
>> mWaitingRequests = new HashMap<>(); private final CacheDispatcher mCacheDispatcher; WaitingRequestManager(CacheDispatcher cacheDispatcher) { mCacheDispatcher = cacheDispatcher; } //请求接受到一个有效的响应,后面等待的相同请求就可以使用这个响应 @Override public void onResponseReceived(Request
request, Response
response) { //如果缓存为空或者已经过期,那么就释放等待的请求 if (response.cacheEntry == null || response.cacheEntry.isExpired()) { onNoUsableResponseReceived(request); return; } String cacheKey = request.getCacheKey(); //等待的请求的集合 List
> waitingRequests; synchronized (this) { //从map里面移除这个请求的集合 waitingRequests = mWaitingRequests.remove(cacheKey); } if (waitingRequests != null) { if (VolleyLog.DEBUG) { VolleyLog.v( "Releasing %d waiting requests for cacheKey=%s.", waitingRequests.size(), cacheKey); } // 里面所有的请求都分发到相应的回调执行,下面会讲解 for (Request
waiting : waitingRequests) { mCacheDispatcher.mDelivery.postResponse(waiting, response); } } } //没有收到相应,则需要释放请求 @Override public synchronized void onNoUsableResponseReceived(Request
request) { String cacheKey = request.getCacheKey(); List
> 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); //将剩下的请求放到等待请求的map中 mWaitingRequests.put(cacheKey, waitingRequests); //在request里面注册一个回调接口,因为重新开始请求,需要重新注册一个监听, //后面请求成功失败以及取消都可以收到回调 nextInLine.setNetworkRequestCompleteListener(this); try { //从上面if判断方法可以得出:waitingRequests != null && !waitingRequests.isEmpty() //排除了第一次请求失败、取消的情况,后面的那个条件则表示这个等待请求队列必须要有一个请求, //同时满足才会执行这里面的代码,一般只要这里面的请求执行成功一次后续所有的请求都会被移除, //所以这里对多个请求的情况,失败一次,那么后续的请求会继续执行 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(); } } } //对于可以缓存的请求,相同缓存的请求已经在运行中就添加到一个发送队列, //等待运行中的队列请求完成,返回true表示已经有请求在运行,false则是第一次执行 private synchronized boolean maybeAddToWaitingRequests(Request
request) { String cacheKey = request.getCacheKey(); // 存在相同的请求则把请求加入到相同缓存键的集合中 if (mWaitingRequests.containsKey(cacheKey)) { // There is already a request in flight. Queue up. List
> stagedRequests = mWaitingRequests.get(cacheKey); //如果包含相同的请求但是有可能是第二次请求,前面第一次请求插入null了 if (stagedRequests == null) { stagedRequests = new ArrayList<>(); } 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 { //第一次请求那么则插入一个null,表示当前有一个请求正在运行 mWaitingRequests.put(cacheKey, null); //注册一个接口监听 request.setNetworkRequestCompleteListener(this); if (VolleyLog.DEBUG) { VolleyLog.d("new request, sending to network %s", cacheKey); } return false; } }}复制代码

这个类主要是避免相同的请求多次请求,而且在NetworkDispatcher里面也会通过这个接口回调相应的值在这里执行,最终比如在网络请求返回304、请求取消或者异常那么都会在这里来处理,如果收到响应则会把值回调给用户,后面的请求也不会再去请求,如果无效的响应则会做一些释放等待的请求操作,请求完成也会将后面相同的请求回调给用户,三个方法都在不同的地方发挥作用。

我们接下来看看NetworkDispatcher网络请求队列的run方法中的processRequest方法:

@VisibleForTestingvoid processRequest(Request
request) { long startTimeMs = SystemClock.elapsedRealtime(); try { request.addMarker("network-queue-take"); // 请求被取消了,就不执行网络请求, if (request.isCanceled()) { request.finish("network-discard-cancelled"); request.notifyListenerResponseNotUsable(); return; } addTrafficStatsTag(request); // 这里就是执行网络请求的地方 NetworkResponse networkResponse = mNetwork.performRequest(request); request.addMarker("network-http-complete"); // 如果服务器返回304响应,即没有修改过, //缓存依然是有效的并且是在需要刷新的有效期内,那么则不需要解析响应 if (networkResponse.notModified && request.hasHadResponseDelivered()) { request.finish("not-modified"); //没有收到来自网络的有效响应,释放请求 request.notifyListenerResponseNotUsable(); return; } // 在工作线程中解析这些响应 Response
response = request.parseNetworkResponse(networkResponse); request.addMarker("network-parse-complete"); // 将缓存写入到应用 if (request.shouldCache() && response.cacheEntry != null) { mCache.put(request.getCacheKey(), response.cacheEntry); request.addMarker("network-cache-written"); } // 标记此请求已将分发 request.markDelivered(); //将请求的响应回调给用户 mDelivery.postResponse(request, response); //请求接受到了一个响应,其他相同的请求可以使用这个响应 request.notifyListenerResponseReceived(response); } catch (VolleyError volleyError) { ... }}复制代码

这里才是网络请求的真正执行以及解析分发的地方,重点看两个地方的代码,执行和解析,我们先看看执行网络请求这个代码,执行的地方是BasicNetwork.performRequest,下面看看这个方法:

@Overridepublic NetworkResponse performRequest(Request
request) throws VolleyError { long requestStart = SystemClock.elapsedRealtime(); while (true) { HttpResponse httpResponse = null; byte[] responseContents = null; List
responseHeaders = Collections.emptyList(); try { // 构造缓存的头部,添加If-None-Match和If-Modified-Since,都是http/1.1中控制协商缓存的两个字段, // If-None-Match:客服端再次发起请求时,携带上次请求返回的唯一标识Etag值, //服务端用携带的值和最后修改的值作对比,最后修改时间大于携带的字段值则返回200,否则304; // If-Modified-Since:客服端再次发起请求时,携带上次请求返回的Last-Modified值, //服务端用携带的值和服务器的Etag值作对比,一致则返回304 Map
additionalRequestHeaders = getCacheHeaders(request.getCacheEntry()); //因为现在一般的sdk都是大于9的,那么这里执行的就是HurlStack的executeRequest方法, //执行网络请求,和我们平时使用HttpURLConnection请求网络大致相同 httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders); int statusCode = httpResponse.getStatusCode(); responseHeaders = httpResponse.getHeaders(); // 服务端返回304时,那么就表示资源无更新,可以继续使用缓存的值 if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) { Entry entry = request.getCacheEntry(); if (entry == null) { return new NetworkResponse( HttpURLConnection.HTTP_NOT_MODIFIED, /* data= */ null, /* notModified= */ true, SystemClock.elapsedRealtime() - requestStart, responseHeaders); } // 将缓存头和响应头组合在一起,一次响应就完成了 List
combinedHeaders = combineHeaders(responseHeaders, entry); return new NetworkResponse( HttpURLConnection.HTTP_NOT_MODIFIED, entry.data, /* notModified= */ true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders); } // 如果返回204,执行成功,没有数据,这里需要检查 InputStream inputStream = httpResponse.getContent(); if (inputStream != null) { responseContents = inputStreamToBytes(inputStream, httpResponse.getContentLength()); } else { //返回204,就返回一个空的byte数组 responseContents = new byte[0]; } // if the request is slow, log it. long requestLifetime = SystemClock.elapsedRealtime() - requestStart; logSlowRequests(requestLifetime, request, responseContents, statusCode); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } return new NetworkResponse( statusCode, responseContents, /* notModified= */ false, SystemClock.elapsedRealtime() - requestStart, responseHeaders); } catch (SocketTimeoutException e) { //异常进行重新请求等... } }}复制代码

这里主要执行了添加缓存头并发起网络请求,然后将返回值组装成一个NetworkResponse值返回,接下来我们看看是如何解析这个值的,解析是由Request的子类去实现的,我们就看系统提供的StringRequest

@Override@SuppressWarnings("DefaultCharset")protected Response
parseNetworkResponse(NetworkResponse response) { String parsed; try { parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); } catch (UnsupportedEncodingException e) { // Since minSdkVersion = 8, we can't call // new String(response.data, Charset.defaultCharset()) // So suppress the warning instead. parsed = new String(response.data); } return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));}复制代码

我们可以看到将值组装成一个String,然后组装成一个Response返回,接下来看看这里如何将这个值回调给用户的这个方法mDelivery.postResponse(request, response),这里我们先重点看看这个类ExecutorDelivery:

public class ExecutorDelivery implements ResponseDelivery {    //构造执行已提交的Runnable任务对象    private final Executor mResponsePoster;   //这里在RequestQueue构造参数中初始化,new ExecutorDelivery(new Handler(Looper.getMainLooper())),   //那么这里runnable就通过绑定主线程的Looper的Handler对象投递到主线程中执行    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);                    }                };    }    public ExecutorDelivery(Executor executor) {        mResponsePoster = executor;    }    //这个方法就是我们NetworkDispatcher里面调用的方法,调用下面这个三个参数的构造方法    @Override    public void postResponse(Request
request, Response
response) { postResponse(request, response, null); } @Override public void postResponse(Request
request, Response
response, Runnable runnable) { request.markDelivered(); request.addMarker("post-response"); //构造了一个ResponseDeliveryRunnable类,传入execute,现在这个runnable就是在主线程里执行 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() { //请求取消,那么就不分发给用户 if (mRequest.isCanceled()) { mRequest.finish("canceled-at-delivery"); return; } // 根据isSuccess这个值来提供相应的回调给用户,调用Response会通过error的值是否为null来确定这个值, //我们调用VolleyError这个构造函数的时候就为这个值就为false if (mResponse.isSuccess()) { mRequest.deliverResponse(mResponse.result); } else { mRequest.deliverError(mResponse.error); } // 如果这是一个在新鲜的时间内的请求的响应,就添加一个标记,否则就结束 if (mResponse.intermediate) { mRequest.addMarker("intermediate-response"); } else { mRequest.finish("done"); } // 在CacheDispatcher里面当请求第一次请求时直接调用三个参数的构造方法,通过这个runnable就执行run方法 if (mRunnable != null) { mRunnable.run(); } } }}复制代码

上面方法主要是将值回调给用户,那么整个网络请求大致就完成了,其中还涉及很多细节的东西,但是大致流程是走通了,不得不说这个库有很多值得我们学习的地方。

三、总结

现在我们看官网的一张图,总结一下整个流程:

  • 蓝色是主线程
  • 绿色是缓存线程
  • 黄色是网络线程

我们可以看到首先是请求添加到RequestQueue里,首先是添加到缓存队列,然后查看是否已经缓存,如果有并且在有效期内的缓存直接回调给用户,如果没有查找到,那么则需要添加到网络请求队列重新请求并且解析响应、写入缓存在发送到主线程给用户回调。

参考以及相关链接

转载于:https://juejin.im/post/5c1c58b35188251f1f320e70

你可能感兴趣的文章
windows查看端口占用
查看>>
Yii用ajax实现无刷新检索更新CListView数据
查看>>
App 卸载记录
查看>>
JavaScript变量和作用域
查看>>
南京大学周志华教授当选欧洲科学院外籍院士
查看>>
计算机网络与Internet应用
查看>>
Mars说光场(3)— 光场采集
查看>>
Django 文件下载功能
查看>>
xBIM 插入复制功能
查看>>
AI技术出海 - 阿里云GPU服务器助力旷视勇夺4项世界第一
查看>>
走红日本 阿里云如何能够赢得海外荣耀
查看>>
HTML DOM 之 DOM对象:Document Object Model (文档对象模型)
查看>>
centos 6.5安装vncserver 并开启远程桌面
查看>>
在RHEL上配置epel的yum源及其他开源YUM源
查看>>
qt 学习之路2
查看>>
刘启成_补充知识:awk:报告生成器
查看>>
ASP.NET 使用List<T>.Remove 不生效
查看>>
TCP有限状态机
查看>>
XenServer常用Debug问题的命令介绍
查看>>
算法分析-快速排序QUICK-SORT
查看>>