前言
学习了 Android 这么久,一直没完整看过一个框架的源码,打算先看看以前用过的 Volley 源码,虽然大体上被 OkHttp 替代了,不过它也有优点,比如其非常适合进行数据量不大,但通信频繁的网络操作、占用空间比较小等。
基本用法
发起一个简单的网络请求代码如下:
private fun getPhoneInfo() { |
本文的流程图如下:
由上述代码可知,使用 Volley 的主要步骤包括创建 RequestQueue、创建 Request、将 Request 加入到RequestQueue 三步。
RequestQueue 的创建
常规做法是调用 Volley.newRequestQueue 这个 API 创建 RequestQueue 实例,代码如下:
public static RequestQueue newRequestQueue(Context context) { |
- 创建了一个 BaseNetwork 实例,传入参数 null 。
- 创建了一个 RequestQueue 实例,传入参数 DiskBasedCache、BaseNetwork 实例。
- 创建了一个 CacheDispatcher( 派生自 Thread ) 实例,传入参数两个 PriorityBlockingQueue(无界,基于二叉小顶堆,插入不会阻塞,取出可能阻塞),并启动。
- 创建了四个 NetworkDispatcher( 派生自 Thread ) 实例,将其保存到 mDispatcher 中,并分别启动。
CacheDispatcher.run
代码如下:
public CacheDispatcher( |
cache
对应 DiskBasedCache 实例,delivery
对应 ExecutorDelivery 实例。- 执行
cache.initialize
、processRequest
。
为了简便起见,暂时不考虑缓存,先看 processRequest 。
DiskBasedCache.initialize
代码如下:
public synchronized void initialize() { |
- 如果目录 cachedir / volley 不存在,那么就创建该目录。
- 如果目录下没有文件,那么直接返回。
- CountingInputStream 只是多了剩余字节统计。
- 读取缓存的文件信息,将读取到的缓存头信息保存到 mEntries 中。
- 读取方式跟 Class 文件基本一致,首先是魔数,如果是字符串前面 4 个字节做为后续字符串的长度。
接下来回到 processRequest 。
CacheDispatcher.processRequest
代码如下:
private void processRequest() throws InterruptedException { |
由于 mCacheQueue 为空,因此 CacheDispatcher 在 mCacheQueue.take()
阻塞。回到 RequestQueue.start ,看看 NetworkDispatcher.run 。
NetworkDispatcher.run
代码如下:
public void run() { |
根据上述代码,4 个 NetworkDispatcher 线程都会在 mQueue.take()
阻塞,接着进行第二步, Request 实例的创建。
Request 的创建
Volley 提供了 StringRequest、JsonRequest、ImageRequest ,分别用于将 Response 转换为 String、Object、Bitmap,一般不需要自定义 Request ,看看 StringRequest 构造器,代码如下:
public class StringRequest extends Request<String> { |
主要进行了 method、url、listener、errorListener 的缓存,以及 RetryPolity、DefaultTrafficStatsTag 的设置,注意, Request 类构造器不接收 Listener 实例,这是因为 NetworkResponse 是子类自身负责解析的,各个子类解析后的结果都不同,因此 Listener 需要子类自己进行处理。接下来看看将 Request 加入到 RequestQueue 。
RequestQueue 添加 Request
这一步是使用 Volley 三步的的最后一步,因此网络请求肯定也是从这里开始的,RequestQueue.add 代码如下:
public <T> Request<T> add(Request<T> request) { |
默认情况下 Request 是需要进行缓存的,不过为了简便起见,先考虑不需要缓存的情况,于是 request 会被放入 mNetworkQueue 中,上文创建 RequestQueue 中已经说到 4 个 NetworkDispatcher 线程全部由于 mNetworkQueue.take
而阻塞,因此 4 个 NetworkDispatcher 线程中的某一个会脱离阻塞。
private void processRequest() throws InterruptedException { |
- 如果在发起请求前 Request 已经被取消了,那么就不发起请求。
- 执行
network.performRequest()
,这里的 network 为 BasicNetWork 实例。 - 执行
request.parseNetworkResponse()
,这里的 request 为 StringRequest 实例。 - 如果需要进行缓存,并且缓存内容不为空,那么放入
mCache
中,对应 DiskBasedCache 实例,暂时不考虑。 - 执行
mDelivery.postResponse()
,这里的 mDelivery 对应 ExecutorDelivery 实例。
BasicNetWork.performRequest
代码如下:
public BasicNetwork(HttpStack httpStack) { |
mBaseHttpStack
为 HurlStack 实例,内部会真正去请求网络。- 如果响应码为 304 ,直接使用缓存数据,对响应头做相应的修改,组装成 NetworkResponse 实例返回。
- 如果有响应体,那么读取字节数组,保存到变量
responseContents
,内部使用了 ByteArrayPool 进行字节数组的缓存,注意其会将响应流的所有内容都读入内存,如果数据很多那么可能导致 OOM 。 - 如果网络请求超时,根据 RetryPolicy 判断是否需要重试,如果需要重试则 while 循环再执行一遍,如果不需要重试再向上抛 SocketTimeoutException 。
- 如果网络请求出现异常,根据 RetryPolicy 判断是否需要重试,如果需要重试则 while 循环再执行一遍,如果不需要重试再向上抛 NetworkError 。
HurlStack.executeRequest
public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders) |
- 将合并后的请求头放入
map
中,当然由于暂时考虑不需要缓存因此additionalHeaders
必定为 null 。 - 提供 UrlRewriter 的接口,在发起请求前,修改 Url ,可以用于批量添加请求参数等。
- 为 HttpUrlConnection 设置是否重定向、超时时间,如果是 Https 请求还会设置 SSLSocketFactory。
- 为 HttpUrlConnection 设置合并后的请求头。
- 为 HttpUrlConnection 设置请求方法,如果是 POST 请求还会添加 Content-Type 请求头以及请求体。
- 判断 Response 是否有响应体,100 <= code < 200 ,code = 204,code = 304、HEAD 请求方式等就没响应头。
- 如果没有响应体那么就直接断开连接,有响应体由于还要读取,先不断开。
StringRequest.parseNetworkResponse
代码如下:
protected Response<String> parseNetworkResponse(NetworkResponse response) { |
- 将响应体使用对应的 Charset 转换为字符串。
- 执行
httpHeaderParser.parseCacheHeaders
解析响应头信息并返回 Response 实例,注意这里必须要解析响应头信息,不然该请求不会进行缓存。
HttpHeaderParser.parseCacheHeaders
代码如下:
public static Cache.Entry parseCacheHeaders(NetworkResponse response) { |
- 将服务器返回的处理请求的时间转化为 long 并保存到
serverDate
变量。 - 缓存控制响应头要求禁止缓存,那么直接返回 null 。
- 解析缓存控制响应头 max-age 信息并保存到
maxAge
变量。 - 解析缓存控制响应头 stale-while-revalidate 信息并保存到
staleWhileRevalidate
变量,该字段意思是在 staleWhileRevalidate 时间内可以直接把缓存内容返回。 - 解析缓存控制响应头 must-revalidate 信息并保存到
mustRevalidate
变量,该字段意思是本地副本过期前,可以使用本地副本,本地副本一旦过期,必须向服务器进行有效性校验。 - 解析响应头 Expires 信息并保存到
serverExpires
变量 。 - 计算缓存有效时间,以缓存控制响应头优先。
ExecutorDelivery.postResponse
代码如下:
public ExecutorDelivery(final Handler handler) { // 1 |
handler
中的 Looper 是主线程的 Looper。- 切换到主线程执行 ResponseDeliveryRunnable.run 。
ResponseDeliveryRunnable.run
代码如下:
public void run() { |
- 如果请求已经被取消了,那么停止分发响应,注意如果子线程在判断后再执行取消,那么还是会被分发,因此尽量在主线程取消,或者在回调中判断没取消再进行相应的操作。
- 如果响应成功,那么调用
mRequest.deliverResponse
,否则调用mRequest.deliverError
。 mRunnable
为空,因此不会执行。
StringRequest.deliverResponse
代码如下:
protected void deliverResponse(String response) { |
执行 listener.onResponse
这里锁是一定要的,volatile 并不适用(可能导致 NPE ),同时只同步了一行语句,提升了效率。
StringRequest.deliverError
代码如下:
public void deliverError(VolleyError error) { |
执行 listener.onErrorResponse
同样的这里锁是一定要的。
到此为止,看完了 Volley 的网络请求逻辑,接着看看 Volley 的缓存处理机制。
Cache
假设 Request 需要进行缓存,回到 NetworkDispatcher.processRequest 。
NetworkDispatcher.processRequest
代码如下:
void processRequest(Request<?> request) { |
这里的 mCache 对应 DiskBasedCache 实例。
DiskBasedCache.put
public synchronized void put(String key, Entry entry) { |
- 由于 DiskBasedCache 内部是基于 LRU 缓存的,默认最大缓存大小为 5MB ,因此如果判断添加本次响应空间不够,就会删除最近最少使用的缓存,直到加上本次缓存,空间占用小于 90% 。
- 将响应头写入缓存文件中。
- 将响应体写入缓存文件中。
至此,缓存文件添加完成,接着来看看缓存文件的读取,在 RequestQueue 的创建部分中说到 CacheDispatcher 会先执行 DiskBasedCache.initialize 。
CacheDispatcher.initalize
代码如下:
public synchronized void initialize() { |
- 如果缓存目录不存在,就创建该目录。
- 如果目录下没有文件,那么直接返回。
- CountingInputStream 只是多了剩余字节统计。
- 读取缓存的文件信息,将读取到的缓存头信息保存到内存中。
- 读取方式跟 Class 文件基本一致,首先是魔数,如果是字符串前面 4 个字节做为后续字符串的长度。
至此,缓存文件的写入、读取都已经完成,接着来看看缓存文件的使用,回到 RequestQueue 添加 Request 。
RequestQueue.add
代码如下:
public <T> Request<T> add(Request<T> request) { |
上文已经说明了不需要缓存的情况,现在考虑需要缓存,mCacheQueue.add
会被执行,于是 CacheDispatcher 线程就会脱离阻塞,执行其 processRequest
。
CacheDispatcher.processRequest
代码如下:
void processRequest(final Request<?> request) throws InterruptedException { |
- 如果请求已经被取消了,那么直接结束本次请求。
- 调用
mCache.get
从缓存中读取 Entry 。 - 如果没有缓存,并且没有相同的请求正在等待响应,那么将其放入网络阻塞队列,进行网络请求。
- 如果缓存过期,并且没有相同的请求正在等待响应,那么将其放入网络阻塞队列,进行网络请求。
- 缓存命中,将其解析成 Response 实例。
- 如果缓存不需要刷新,那么直接分发缓存的响应。
- 如果缓存需要刷新,并且没有相同的请求正在等待响应,那么先分发一次缓存的响应,同时再放入网络阻塞队列进行请求,如果有相同请求正在等待响应,那么直接分发缓存的响应。
DiskBasedCache.get
代码如下:
public synchronized Entry get(String key) { |
由于原先只是把响应头给缓存到了内存中,因此需要从文件中读取响应体。
还有最后一个问题,如果有缓存,无论是否过期,都会给 Request 设置 CacheEntry ,那么这个有什么用?这需要再回到 BasicNetWork.performRequest 。
BasicNetWork.performRequest
代码如下:
public NetworkResponse performRequest(Request<?> request) throws VolleyError { |
如果有缓存,获取需要带上的请求头。
BasicNetwork.getCacheHeaders
代码如下:
private Map<String, String> getCacheHeaders(Cache.Entry entry) { |
- 如果缓存中有 etag ,那么带上 If-None-Match 请求头,这样如果服务端判断 etag 没有发生变化就会返回 304,优化性能。
- 如果缓存中有 lastModified ,那么带上 If-Modified-Since 请求头,这样如果服务端判断资源的最后修改时间与之一致,那么就返回 304 , 优化性能。
缓存相关也基本阅读完成,来看下最后一个 RetryPolicy 。
RetryPolicy
上文已经说到了当网络请求超时,或者遇见异常,都会使用 RetryPolicy 来判断是否需要进行重试操作。
代码如下:
public interface RetryPolicy { |
- 每次请求时都会调用该方法,获取 HttpURLConnection 的连接以及读取超时时间。
- 该方法只用于响应较慢超过 3 秒打印 log 时,可以不需要实。
- 如果想要进行重试,那么该方法可以什么都不做,如果不想要进行重试,抛出异常即可。
总结
Volley 源码基本上算是阅读完毕了,总结下其执行流程。
- 创建 RequestQueue ,开启一个 CacheDispatcher 线程(首先读取文件缓存),以及四个 NetworkDispatcher 线程,分别由于 NetworkPriorityBlockingQueue、CachePriorityBlockingQueue 为空而阻塞。
- 创建 Request 对象,并将其添加到 RequestQueue 中,判断是否需要缓存,如果需要那么将其添加到 CachePriorityBlockingQueue,如果不需要那么将其添加到 NetworkPriorityBlockingQueue 中。
- 如需要缓存,CacheDispatcher 脱离阻塞,读取缓存文件,判断缓存文件是否有效,如有效那么就直接回调请求成功,如果无效或者无缓存,那么将其加入到 NetworkPriorityBlockingQueue 中。
- 如不需要缓存,缓存无效或无缓存,NetworkDispatcher 脱离阻塞,进行网络请求,缓存如果有 etag、lastmodify 会额外添加两个请求头,获取到响应后,如果请求超时或者出现异常情况,根据重试策略判断是否需要进行重试,如果请求成功,交给 Request 解析 Response ,完成后如果需要进行缓存,则将解析后 Response 进行缓存,接着再使用 Handler 切换线程到主线程回调 onResponse 。
由于 Volley 内部使用了 ByteArrayPool 避免每次都去新建字节数组对象,所以适用于处理高频率的请求,同时由于其将所有响应通过字节数组输出流读入了内存所以其不适合进行大文件的下载,否则容易造成OOM。