温故而知新 Volley源码解读与思考
相比新的网络请求框架Volley真的很落后,一无是处吗,要知道Volley是由google官方推出的,虽然推出的时间很久了,但是其中依然有值得学习的地方。 从命名我们就能看出一些端倪,volley中文意为群射,齐射,官方解释说它适合通信频繁但是数据量不大的网络请求操作( a burst or emission of many things or a large amount at once ),至于为什么我们解读完源码就知道了。
回想下使用Volley的过程:比如请求一个网页的内容。
1. 创建RequestQueue对象
RequestQueue mQueue = Volley.newRequestQueue(MyApplication.getInstance());
2. 先创建一个StringRequest对象
private StringRequest stringRequest = new StringRequest(
Request.Method.GET,
"https://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d(TAG, "current thread :" + Thread.currentThread().getName()); // main thread
((TextView)findViewById(R.id.content)).setText(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "error :" + error.getMessage());
}
}
) ;
3. 将请求对象添加到mQueue中
mQueue.add(stringRequest);
如下流程描述请自行结合Volley中的源码阅读(需要说明的是本文分析的Volley代码不是最新版本,还是1.0.x的版本):
请求执行流程:
首先我们要构造RequestQueue, 其内部封装了缓存请求队列:
首先我们要构造RequestQueue, 其内部封装了缓存请求队列PriorityBlockingQueue<Request<?>> mCacheQueue 和网络请求队列 PriorityBlockingQueue<Request<?>> mNetworkQueue,同时也封装了一条缓存调度线程mCacheDispatcher和若干条网络请求调度线程 NetworkDispatcher[] mDispatchers,虽然RequestQueue的构造方法是public,但是我们还是调用Volley的newRequestQueue方法,因为在newRequestQueue方法有些重要的处理,比如设置DiskBasedCache的目录, 添加请求的User-agent,判断SDK的版本号,如果是2.3(API=9)以下则使用HttpClient, 如果是>=2.3的版本,则使用HttpUrlConnection,接着构建RequestQueue对象,并调用其start方法,创建并启动缓存调度线程和网络请求调度线程,目前的版本是1条缓存线程和4条网络请求线程。
接着查看RequestQueue.add的相关逻辑:
将构造的Request添加到RequestQueue中,即调用RequestQueue.add方法,这里会将请求先Add到一个Set集合中,即Set<Request<?>> mCurrentRequests中,然后判断是否禁用了缓存,如果禁用缓存则直接添加到mNetworkQueue中, 又因为NetworkDispatcher调度线程run方法中是while死循环,会一直取队列中的对象,故加入网络请求队列后,就相当于直接发起了网络请求。 而如果允许缓存,即Request.shouldCache返回true,则判断Map(Map<String,Queue<Request<?>> mWaitingRequests中是否有相同的请求,判断的标准就是请求的url,即request.getCacheKey()),如果mWaitingRequests中存在,则做提示处理,如果不存在则将请求添加到map中做记录,并执行mCacheQueue.add(request)
请求加入了CacheQueue队列中,则缓存调度线程就可以从队列中取出requeset做处理。查看缓存调度线程CacheDispatcher的run方法,while循环中的逻辑如下,先取出缓存queue中的请求对象request,根据请求的url得到cache, 判断cache中entry是否为空,如果为空则说明没有缓存,则将请求添加到mNetworkQueue中,mNetworkQueue.put(request), 交由网络请求线程处理。如果有缓存,判断缓存是否过期,如果过期则同上,如果缓存可用,则取出缓存中数据做解析并返回,即调用request.parseNetworkResponse方法,解析之后调用mDelivery.postResponse方法做结果的投递,这里就将操作从子线程转移到主线程了,具体是由mDelivery去处理切换的操作, mDelivery(具体实现类是ExecutorDelivery)内部封装了Handler和Executor,将最终解析出的结果投递到主线程handler.post(runnable), 此handler是主线程的handler,构造RequestQueue队列时创建了主线程的Handler对象了,代码如下:
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
5. 当请求添加到网络请求队列queue之后,在NetworkDispatcher的run方法中执行真正的网络请求,首先会判断线程是否退出了,或者request是否被取消了等逻辑,一切ok则执行mNetwork.performRequest(request),发起网络请求,然后解析结果,做缓存操作,派发解析结果到主线程等等
// 这里注意BlockingQueue的add offer put//// remove poll take peek等方法的区别
1.add 将元素插入queue中,如果立即可行且不违反容量规则返回true,如果当前没有可用空间,则抛出IllegalStateExecption
2.offer 与add方法类似,但是使用有限制容量的queue时,此方法通常优于add方法,后者可能可能无法插入元素,只是抛出一个异常
3. put 插入元素到queue尾部,如果空间不够,则等待空间变得可用
-----------------------------------------------------------------------------------------------------------------------------------
4. remove 移除元素,返回true如果queue总包含此元素
5.poll 获取并移除头部元素, E poll(), 如果queue为空,则返回null
6.take 获取并移除头部元素,如果没有则等待直到有头部元素变得可用, E take() throws InterruptedException。
7.peek 只是获取头部元素,并不做移除操作,如果queue为空,则返回null。
缓存执行流程
上面简要分析了请求执行的过程,那么Volley是如何实现缓存和获取缓存的呢,我们接着分析,试想我们第一次请求某个网络资源时,必然是没有缓存的,那么最终会走到网络调用线程NetworkDispatcher run方法中的逻辑,执行网络请求拿到NetworkResponse,然后解析networkResponse,即调用request的parseNetworkResponse得到Response对象,然后判断request是否允许缓存,如果需要缓存且response中的Cache.Entry即缓存对象不为空,则做缓存的操作。Cache.Entry对象cacheEntry什么时候被赋值的呢?就是在parseNetworkResponse返回Response对象的过程中,构造Response对象调用Response.success(result, HttpHeaderParser.parseCacheHeaders(response));, success函数的第二参数即为cacheEntry,查看parseCacheHeaders方法可以看到,entry中包含有data, etag,softTtl,lastModified,responseHeaders等数据。我们要缓存就是上边的cacheEntry,对应代码中的mCache.put(request.getCacheKey(), response.cacheEntry); 这里的mCache又是什么呢。查找mCache的源头又回到了Volley.newRequestQueue方法中,这里构建RequestQueue时传入了DiskBasedCache,那么看来mCache的具体实现类就是DiskBasedCache了。查看DiskBasedCache的源码,可以看到其默认缓存路径是/data/data/packagename/cache/volley/ , 默认的缓存大小为10M,其中最关键的就是put方法,put(String key, Entry entry) ,此方法首先会根据entry中data数组的长度判断是否能够缓存得下,也就是缓存后是否超过了设定的最大缓存容量值。具体在pruneINeed中做判断,如果超过最大值,则会按顺序依次从已缓存的文件中做删除操作(PS:如何做到按顺序删除呢,因为在putEntry方法中将key和cacheHeader的信息存储在了LinkedHashMap中了, 所以删除的时候才能依次按照缓存的先后顺序删除,最先缓存的先被删除掉),直到缓存本次data不再超过最大值为止,然后创建一个File对象存储缓存数据,File的name是将Url字符串的前半部分的hashcode加上字符串后半部分的hashcode组合而成,具体请查看getFilenameForKey(String key)方法,然后构建FileOutputStream对象分别将CacheHeader信息和data数据部分信息写入文件,如果写入的过程中发生了异常,则会做删除文件的处理。至于读取的操作请查看get方法.
ClearCacheRequest请求执行流程
可以看到在toolbox包下有一个ClearCacheRequest的类,看名字大概能猜测出来它是做清除缓存操作的。因为我们已经知道在Volley中的缓存逻辑是在DiskBasedCache中,查看DiskBasedCache中的的代码,可以找个一个clear方法, 我们可以在此方法的第一行打上断点,然后构造一个ClearCacheRequest对象,并添加到请求队列中(在构造ClearCacheRequest方法中需要传递两个参数,一个是mCache,一个是Runnable,其实mCache就是我们内部实现缓存的引用,Runnable可以做Clear后主线程上的操作), 启动调试模式,可以看到其执行流程 CacheDispatcher.run --- > ClearCacheReqeuest.isCanceled -->
DiskBasedCache.clear方法,其中ClearCacheRequest的isCanceled方法与其他xxxRequest的isCancled方法不同,其内部调用了mCache.clear() ,,并将Runnable对象投递到主线程的消息队列中,如果mCallback不为空的话。在DiskBasedCache的clear方法中则分别做了对文件缓存删除 和对内存缓存mEntries clear的操作。
网络请求流程
发起网络请求的逻辑在BasicNetwork的performRequest方法中,我们可以看到方法内部使用的是while死循环也就是说要么得到请求的结果,要么抛出异常。 而使用while循环也是重试机制的关键。 先看下大致的流程, 添加请求的header (这里会从CacheHeader中获取,如果entry不为空,取出etag,headers.put("If-None-Match", etag, 取出lastModified,headers.put("If-Modified-Since", lastModified)) --> 发起网络请求 mHttpStack.performRequest --> 得到response ---> 解析response --> 返回NetworkResponse。 如果返回的状态码statusCode == 304 ,那么说明服务器在对比etag和lastModified后发现资源没有修改过,客户端直接使用缓存即可, 如果返回的状态码是301或302,则说明请求的资源移动了位置,需要重定向,我们取出响应头中的location信息,调用request.setRedirectUrl(url), 而后由于逻辑的处理返回的状态码不是2XX则会抛出IOException异常, 在catch的处理中会再次判断状态码并调用attemptRetryOnException,而此方法中的默认重试代理是DefaultRetryPolicy, 那么这个RetryPolicy是在哪设置的呢,查看Request的构造方法不难发现, 其中有setRetryPolicy(new DefaultRetryPolicy()) 的身影, 其retry方法中会对重试次数做判断,如果超过最大重试次数,则抛出异常,那么performRequest方法也会终止执行,如果小于等于最大重试次数则while循环的逻辑会再次执行,直到有结果。 其中需要注意到一点, 因为默认的连接超时时间较短只有2500ms,(不管是HttpClientStack的PerformRequest方法还是HurlStack的openConnection方法都会拿到request中设置的超时时间 int time = request.getTimeOutMs();)在国内复杂的网络环境中可能从发起请求到响应时间会超过此值,一旦超过此值Volley默认则认为是超时了,从而触发重试的机制,导致一个请求发送两次的情况。解决的办法是可以增大默认超时的时间值,比如设置5000ms,或者设置不使用重试机制。
request.setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); 关于这个问题Volley的github库issue中也有提及:https://github.com/google/volley/issues/7
其实说了这么多,还是下面这张流程图的内容:

现在来做下问题总结:
1. 为什么说Volley不适合大文件的下载等操作,而是数据量小的通信网络场景?
因为从Volley的源码中我们可以发现,其内部执行网络请求的线程是固定数量4条线程,如果下载大文件可能就会导致线程被长时间占用,后面排队的Request可能长时间得不到执行,Volley解析结果是直接放到byte[] 数组中,如果文件较大,则有可能发生oom, 且在Volley内部有缓存机制,如果大文件也允许缓存,而设定的最大缓存容量值较小,则可能发生长时间的IO操作(因为可能超过最大容量而要做删除文件操作),导致应用性能下降。
2. Volley中的缓存调度线程和网络调用线程的run方法中是while死循环,什么时候退出,也就是缓存和网络调度线程什么时候结束工作?
其实在run方法的内部有相关逻辑, 比如NetworkDispatcher的run方法中,会捕获InterruptedException异常,在异常处理中判断mQuit的值,如果为true则直接返回。而调用Interrupt方法和设置mQuit值的处理就在NetworkDispatcher对应的quit() 方法中。
3. 可否将处理网络请求的线程改成线程池ThreadPoolExecutor?
可以改,但是即使改为线程池实现,性能可能也不会有提升,一方面对于手机cpu来说其核心数是有限的,如果线程池内的线程数配置的较大,则网络请求时可能导致线程的频繁的发生切换,而线程的切换是有开销的。
4. Volley可否加载较大的图片,比如十几M,几十M等?
因为Volley中解析完数据是要保存在byte[] data,中的,所以如果数据过大则有可能发生OOM异常。https://github.com/google/volley/issues/12
5. 使用Volley时应该在哪里创建RequestQueue合适?
具体可以在自定义的Application中,主要是传递给newRequestQueue的Context应该使用ApplicationContext,这样可以避免可能发生的内存泄漏的情况,试想如果持有Activity的context那么Volley内部的工作没有做完则一直持有Activity,导致Activity无法释放,故在自定义的Apllication初始化一个全局的请求队列即可。
6. onResponse是在主线程中执行,但是返回结果后还需要做耗时操作怎么办?
从Volley的源码中我们能够知道派发器mDelivery的是ExecutorDelivery,其默认实现是传递主线程的handler的构造方法,而ExecutorDelivery的内部还有一个传递executor的构造方法,只要构建一个的executor,在new RequestQueue时,让 mDelivery = new ExecutorDelivery(executor), 那么onResponse最终就在executor的线程中执行, 不再是主线程了。
7. 如何取消某个或者多个网络请求?
取消单个request可以调用request.cancel(), 如果是多个可以给某个类别的request设置一个tag,想要取消请求调用requestQueue.cancelAll(tag),调用cancel方法后Request内的属性mCanneled即被复制为true,在CacheDispatcher或者NetworkDispatcher的run方法中会对request.isCanceled做判断。如果是取消多个请求,调用cancelAll 方法,则会在当前的请求集合中进行遍历,找到tag一致的request。
7. Volley有什么优缺点。
优点:
还是那句: 适合网络通信频繁,但是通信数据量不大的请求,不适合大文件的下载。
可以缓存http请求,过滤重复请求(一般网络请求框架也都支持)
支持请求的优先级
支持取消请求的API,可以取消单个请求,也可以设置取消请求的范围域
基于接口的设计,使扩展相对容易(比如写一个XMLRequest类 继承Request,实现onResponse方法和parseNetworkResponse方法)
缺点:
对于文件的上传和下载支持的不好
与Apache的Httpclient 和 HttpUrlConnection耦合较紧密
Android 6.0系统移除对HttpClient的支持,所以要使用Volley,需要配置org.apache.http.legacy.jar的引用
https://github.com/google/volley/releases 最新的Volley是1.1.0的版本,修复了如下问题:
- Apache HTTP is now an optional dependency (#2). See Migrating from Apache HTTP for details on how to avoid using it.
- Fix OutOfMemoryErrors and NegativeArraySizeExceptions in DiskBasedCache (#12).
- Fix memory leak in Request#mErrorListener (#15).
- Support for multiple identical response headers (#21).
- Fix potential NullPointerException in ImageRequest/JsonRequest/StringRequest (#64).
- Fix soft TTL for duplicate in-flight requests (#73).
- Fix case-sensitive header reads from cache (#76).
待补充。。。
温故而知新 Volley源码解读与思考的更多相关文章
- 201709021工作日记--Volley源码解读(四)
接着volley源码(三)继续,本来是准备写在(三)后面的,但是博客园太垃圾了,写了半天居然没保存上,要不是公司这个博客还没被限制登陆,鬼才用这个...真是垃圾 继续解读RequestQueue的源码 ...
- RequireJs 源码解读及思考
写在前面: 最近做的一个项目,用的require和backbone,对两者的使用已经很熟悉了,但是一直都有好奇他们怎么实现的,一直寻思着读读源码.现在项目结束,终于有机会好好研究一下. 本文重要解读r ...
- BackBone 源码解读及思考
说明 前段时间略忙,终于找到时间看看backbone代码. 正如知友们说的那样,backbone简单.随性. 代码简单的看一眼,就能知道作者的思路.因为简单,所以随性,可以很自由的和其他类库大搭配使用 ...
- Spark Streaming源码解读之流数据不断接收和全生命周期彻底研究和思考
本节的主要内容: 一.数据接受架构和设计模式 二.接受数据的源码解读 Spark Streaming不断持续的接收数据,具有Receiver的Spark 应用程序的考虑. Receiver和Drive ...
- 15、Spark Streaming源码解读之No Receivers彻底思考
在前几期文章里讲了带Receiver的Spark Streaming 应用的相关源码解读,但是现在开发Spark Streaming的应用越来越多的采用No Receivers(Direct Appr ...
- SDWebImage源码解读之SDWebImageDownloaderOperation
第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...
- underscore 源码解读之 bind 方法的实现
自从进入七月以来,我的 underscore 源码解读系列 更新缓慢,再这样下去,今年更完的目标似乎要落空,赶紧写一篇压压惊. 前文 跟大家简单介绍了下 ES5 中的 bind 方法以及使用场景(没读 ...
- 线程本地变量ThreadLocal源码解读
一.ThreadLocal基础知识 原始线程现状: 按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步.但是Spring中的各种模板 ...
- SDWebImage源码解读之SDWebImageDownloader
SDWebImage源码解读之SDWebImageDownloader 第八篇 前言 SDWebImageDownloader这个类非常简单,作者的设计思路也很清晰,但是我想在这说点题外话. 如果有人 ...
随机推荐
- video标签
Video标签的使用 Video标签含有src.poster.preload.autoplay.loop.controls.width.height等几个属性, 以及一个内部使用的标签<sour ...
- 数据结构与算法->树->2-3-4树的查找,添加,删除(Java)
代码: 兵马未动,粮草先行 作者: 传说中的汽水枪 如有错误,请留言指正,欢迎一起探讨. 转载请注明出处. 目录 一. 2-3-4树的定义 二. 2-3-4树数据结构定义 三. 2-3-4树的可以得到 ...
- Ansible系列(二):选项和常用模块
html { font-family: sans-serif } body { margin: 0 } article,aside,details,figcaption,figure,footer,h ...
- Git的使用详解
起步 关于版本控制 Git 简史 Git 基础 安装 Git 初次运行 Git 前的配置 获取帮助 小结 Git 基础 取得项目的 Git 仓库 记录每次更新到仓库 查看提交历史 撤消操作 远程仓库的 ...
- JavaSE(十二)之IO流的字节流(一)
前面我们学习的了多线程,今天开始要学习IO流了,java中IO流的知识非常重要.但是其实并不难,因为他们都有固定的套路. 一.流的概念 流是个抽象的概念,是对输入输出设备的抽象,Java程序中 ...
- java 对象的序列化与反序列化
一.序列化和反序列化的概念 把对象转换为字节序列的过程称为对象的序列化. 把字节序列恢复为对象的过程称为对象的反序列化. 对象的序列化主要有两种用途: 1) 把对象的字节序列永久地保存到硬盘上,通常存 ...
- 反射结合xml简单的模拟spring创建bean
框架最底层其实就是通过反射实现的,就像spring,当你配置各种各样的bean时都是以配置文件的形式配置的,你需要用到哪些bean就配哪些,spring容器就会根据你的需求去动态加载,这儿写一个简单的 ...
- 关于如何更好地使用Github的一些建议
关于如何更好地使用Github的一些建议 原文(Github repository形式): https://github.com/Wasdns/github-example-repo 本文记录了我对于 ...
- Day4 闭包、装饰器decorator、迭代器与生成器、面向过程编程、三元表达式、列表解析与生成器表达式、序列化与反序列化
一.装饰器 一.装饰器的知识储备 1.可变长参数 :*args和**kwargs def index(name,age): print(name,age) def wrapper(*args,**k ...
- 在CentOS6上配置MHA过程全记录
在CentOS6上配置MHA过程全记录 MHA(Master High Availability)是一款开源的MariaDB or MySQL高可用程序,为MariaDB or MySQL主从复制架构 ...