Volley算是比较简单的http库,所以决定从Volley入手去开始读源码之路。
打算写一系列Volley源码阅读的文章,顺序按照我的源码阅读顺序。这是第一章,为何适用于多而小。
本章主要介绍我阅读源码的起始过程,理解Volley的初始化工作和Volley中请求线程的工作原理。
1. 初步浏览 - 接口
接口 | 实现类 | 主要方法/作用 |
---|---|---|
Cache | NoCache | |
NetWork | BasikNetWork | performRequest() |
MockNetWork | ||
ResponseDelivery | ExecutorDelivery | postResponse()/postError() |
RetryPolicy | DefaultRetryPolicy | |
toolbox.HttpStack | HttpClientStack | 使用HttpClient来performRequest |
HurlStack | 使用HttpURLConnection来performRequest | |
MockHttpStack | ||
toolbox.Authenticator | DefaultAuthenticator |
Volley源码算是比较少的,我也没有看源码的经验。于是先将接口列出,试图知道设计人员一开始都想做些啥,这个方法卓有成效。
接口列出如上,如何快速找到接口呢?我用的Idea看的源码,里面非常清晰,如下图:
图中1处绿色写着“I”就是Interface接口,甚至还有2处圆圈两边灰色的,打开Request类会发现这是个抽象类。
那下面把抽象类也列一下:
抽象类 | 实现类 | 主要方法/作用 |
---|---|---|
Request | ClearCacheRequest | |
ImageRequest | ||
StringRequest | ||
JsonRequest | JsonArrayRequest | |
JsonObjectRequest |
比较特殊的是JsonRequest也是Request的子类,同时它也是一个抽象类,再分别由JsonArrayRequest和JsonObjectRequest实现。
这些东西列出来,可能就对框架已经有了初步的了解,比如:
- 这个框架可能做了缓存 —— 基于Cache接口
- 用NetWork还是HttpStack来实现请求? —— 基于二者都有的performRequest()方法
- 应答有一个分发机制 —— 基于ResponseDelivery
- 有重发机制 —— 基于RetryPolicy
- 可能还有认证机制 —— 基于Authenticator
这些东西先有个印象,有个印象能加快后面的进度。
2. 简单使用
网上一搜一大把,这里建议大家看博客的时候,找一些排版比较好的。那些连代码格式都没有的就不要去看了。
参考Android Volley完全解析(一),初识Volley的基本用法,这个是我搜到的百度的第一个。
第三条 StringRequest介绍到:
(1). 初始化,创建RequestQueue对象
RequestQueue mQueue = Volley.newRequestQueue(context);
(2). 创建StringRequest请求,并add进RequestQueue
StringRequest stringRequest = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d("TAG", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG", error.getMessage(), error);
}
});
mQueue.add(stringRequest);
3. 参考简单使用理解初始化工作
看2中代码,初始化仅仅创建了一个RequestQueue,这里面完成了什么工作呢?
43行:初始化了默认的缓存目录
53-59行:根据不同的sdk版本创建不同的HttpStack(主要的实现网络请求的对象)
63行:以HttpStack对象作为参数新建NetWork对象
猜想:这应该是NetWork对象调用HttpStack对象实现网络请求了
65行:以DiskBasedCache对象和netWork对象作为参数创建了RequestQueue对象
66行:RequestQueue执行start方法
理一理思绪:
- 简单说就是创建了缓存对象DiskBasedCache和网络请求对象BasicNetwork,然后传给RequestQueue的构造器创建了RequestQueue对象。
- BasicNetWork对象好像代理了HttpStack对象来执行http请求的具体实现
- 非常贴心地可以传入HttpStack对象,表示开发人员可以自己选择http请求的具体实现
- RequestQueue对象执行了start方法,不知道做了什么操作
现在来找一些比较关心的事情。
Q: 网络请求到底怎么执行的?NetWork和HttpStack到底是如何工作的?
Volley只是一个封装库,底层的http请求还是调用JDK接口。基于这样的背景,去看一下NetWork和HttpStack的performRequest()方法,这个方法一看起来就像是执行http请求的。
BasicNetWork的performRequest()
发现关键的httpResponse还是由HttpStack实现的,所以只要看HttpStack。
HurlStack的performRequest()
ps. 由于14号字体无法截全方法,不得不缩小字体
可以不去管其中的具体实现,找到了其中比较重点的部分:responseStatus和response是依靠HttpURLConnection获得的。我一开始学习Android,就是手写HttpURLConnection来实现get和post方法的。
反正response是HttpURLConnection实现的,暂时先不求甚解,假装已经拿到了response。
RequestQueue 构造器做了什么工作?
StringRequest例子里面是两个参数的RequestQueue构造器
两个参数的构造器调用了3个参数的构造器方法,并将第三个参数设置为默认4。看注释我们就知道,这个4是4个用来执行请求的线程,大胆猜想,RequestQueue中有一个线程池,默认线程数量是4。
看3个参数的构造器
第三个参数确实命名为threadPoolSize。同时加上了第4个参数默认值,一个ResponseDelivery的唯一实现ExecutorDelivery的对象。这个类我们在一开始整理接口的时候就见过,暂时只需要知道用来分发response。
再看最终的4个参数的构造器
原来只是赋值给成员变量而已。而且心心念念的线程池也仅仅是一个线程对象数组。
很好,其实可以发现,一开始整理出来的接口,有3个已经被RequestQueue成员变量实现了:Cache、NetWork、和ResponseDelivery
queue.start();完成了什么工作?
start之前都是对象的创建,到这一步,终于要运行起来了吧!
142行:备注已经说明了,确保关闭dispatcher线程。具体逻辑肯定和start相反,那么就先不care吧
144-145行:这里居然偷偷创建了一个缓存分发器,进入实现会发现这继承了线程。
148-152行:这里估计就是关键了,for循环一个一个地创建NetworkDispatcher对象将线程池数组填满,然后再将线程运行起来。
看到这里想必和我一样不耐烦了,这个dispatcher是个什么东西,线程现在就运行起来,岂不是占资源,而且,怎么保证线程一直在等用户发请求?
所以还是先看一看,networkDispatcher.start();之后会有什么发生?
这既然是继承了线程,那直接找到run()方法,好在一开始就看到了关键:
- while(true)这是一个死循环
- 第二个红框里面应该就是从队列里面拿请求了。所以不断从队列里面拿请求,然后执行。
这个mQueue是什么对象?队列里面要是没有请求存在又怎么办?
再看start方法中NetworkDispatcher对象的构造器参数
这个mNetworkQueue是一个优先级阻塞队列,保存的是Request对象
NetworkDispatcher中的mQueue确实就是这个mNetworkQueue
那么不难猜想,mQueue(优先级阻塞队列PriorityBlockingQueue)在为空的时候take会阻塞线程,直到queue不为空。那么进入take()方法去验证一下这个猜想:
直接点击take方法会跳入BlockingQueue接口里面的方法,如下
点击左边那个向下箭头就能看到进入实现,我们选择PriorityBlockingQueue:
我们看到关键的一行,dequeue()方法肯定就是队列弹出一个对象(可以自己去看看具体实现),为空的话,就会await()。
其中notEmpty对象是一个条件对象,这设计了多线程的锁。
在PriorityBlockingQueue的构造器内初始化
这里面的原理需要再开一篇来细讲。这里我们只需要知道阻塞队列为空,当前线程就会被阻塞await()。如何唤醒线程,后文讲一个请求的发送流程继续讲。
但是默认有4个线程,4个线程都会被阻塞吗?
我们不妨进入await()方法里面去再深入看一看:
这里我们需要换一种寻找实现方法的方式,找到notEmpty的初始化
进入newCondition()方法
再找到sync对象的声明地方
!!!这是可重入锁!而且下面就是Sync抽象类的声明,找到其中newCondition()方法
发现返回的是一个ConditionObject对象,找到这个类的声明并找到其中的await()方法:
2031行:将当前线程加入条件等待队列
2034行:检查当前线程node是否还在等待条件,需要等就进入循环执行LovkSupport.Park()来阻塞线程。
看一看这部分源码,可以理解,线程来一个阻塞一个,就是一个阻塞队列。
初始化工作也是为了一般工作能够正常进行,这里也保留一个印象,便于后面具体工作展开的理解。
4. 参考简单使用理解请求如何发送
StringRequest stringRequest = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.d("TAG", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG", error.getMessage(), error);
}
});
mQueue.add(stringRequest);
再看一看基本使用的代码,我已经写累了,太长了。太长不想看,我也不想写了。看,里面requestQueue.add(request)这样就把请求发送出去了。前面我们知道线程被阻塞住了。那当有请求add进来的时候,是如何唤醒线程的呢?
找到RequestQueue类中的add方法:
简单分析一下:
229:让request对象持有当前RequestQueue对象的引用
236:addMarker,注意request执行到每一个步骤都会设置一个marker,用来标志请求进行的阶段
239:是否需要缓存的条件语句,当然先从不需要的看起
240:mNetworkQueue.add,这个就是关键。
那240这里add进去了,怎么唤醒线程去执行呢?看源码
看到最后一个signal方法,这就是和await相对应的唤醒方法。假如还是不太理解这个阻塞和唤醒到底怎么回事,建议和我一样,写一个小demo看看
打印出来的效果是
不会无限打印print,也不会打印print finally
结合初始化那部分,就完整地呈现了多线程如何去实时发送新增的请求的流程。
结论
这也解释了为何Volley有利于多而小的请求,毕竟有4个线程可以使用。反而大的请求其实并没有做什么优化,用Volley执行大请求并没有什么优势,反而可能因为太耗时而占用了线程,导致其他小的请求无法及时执行。