温故而知新 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这个类非常简单,作者的设计思路也很清晰,但是我想在这说点题外话. 如果有人 ...
随机推荐
- Linux系统下用户与组的管理
Linux系统下用户与组的管理 一.用户及组基本概述 Linux 系统上,用户管理是基于用户名和密码的方式进行资源的分配. 1.uid(用户身份标识) (1)root用户 uid为0 (2)普通用户: ...
- Java:final、static关键字 详解+两者结合使用
一 final关键字 1) 关于final的重要知识点 final关键字可以用于成员变量.本地变量.方法以及类. final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误. ...
- C# 泛型集合
原文出处我的wiki,转载请说明出处 考虑到泛型在集合类型中的广泛应用,这里一起讨论. 1. 泛型 1.1 泛型的定义与约束 创建泛型方法.委托.接口或类时,需要在名称后增加尖括号及其中的泛型参数,泛 ...
- CVE-2016-10191 FFmpeg RTMP Heap Buffer Overflow 漏洞分析及利用
作者:栈长@蚂蚁金服巴斯光年安全实验室 一.前言 FFmpeg是一个著名的处理音视频的开源项目,使用者众多.2016年末paulcher发现FFmpeg三个堆溢出漏洞分别为CVE-2016-10190 ...
- mysql、mariadb安装和多实例配置
本文目录:1. mysql单实例安装 1.1 rpm安装mysql 1.2 通用二进制包安装mysql 1.2.1 初始化数据库 1.2.2 安装后的规范化操作 1.3 编译安装 1.3.1 编译安装 ...
- java中Set类接口的用法
在Java中使用Set,可以方便地将需要的类型,以集合类型保存在一个变量中.主要应用在显示列表. Set是一个不包含重复元素的collection.更确切地讲,set 不包含满足 e1.equals( ...
- spring boot / cloud (十八) 使用docker快速搭建本地环境
spring boot / cloud (十八) 使用docker快速搭建本地环境 在平时的开发中工作中,环境的搭建其实一直都是一个很麻烦的事情 特别是现在,系统越来越复杂,所需要连接的一些中间件也越 ...
- 第4章 同步控制 Synchronization ----Interlocked Variables
同步机制的最简单类型是使用 interlocked 函数,对着标准的 32 位变量进行操作.这些函数并没有提供"等待"机能,它们只是保证对某个特定变量的存取操作是"一个一 ...
- JavaWeb(一)Servlet中的request与response
一.HttpServletRequest概述 1.1.HttpServletRequest简介 HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP ...
- Stochastic Gradient Descent
一.从Multinomial Logistic模型说起 1.Multinomial Logistic 令为维输入向量; 为输出label;(一共k类); 为模型参数向量: Multinomial Lo ...