【源码】"拆" 网络请求库-Volley
0x1、引言
Volley跟AsyncTask一样,都是老古董了,最早发布于2013年的Google I/O大会,初衷就是:让Android开发者少写重复的请求代码。
怎么说?早期网络请求都是用 HttpURLConnection 或者 HttpClient,直接用非常麻烦,比如一段请求百度的代码:
private void sendRequest() {
//开线程发起网络请求
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection conn = null;
BufferedReader reader = null;
try {
//获取HttpRULConnection实例
URL url = new URL("https://www.baidu.com");
conn = (HttpURLConnection) url.openConnection();
//设置请求方法和自由定制
conn.setRequestMethod("GET");
conn.setConnectTimeout(8000);
conn.setReadTimeout(8000);
//获取响应码和返回的输入流
int i = conn.getResponseCode();
InputStream in = conn.getInputStream();
//对输入流进行读取
reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
showResponse(response.toString());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
// 请求结果展示
private void showResponse( final String response) {
runOnUiThread(new Runnable() {
@Override
public void run() {
//进行UI操作
xxx.setText(response);
}
});
}
每个请求都是写上这样一串又长又臭的玩意,写的人死了,看的人也死了,所以一般要封装下。
封装得好也不简单,得去考虑并发,请求取消等等。而Volley所做的事情也是封装,将复杂通信细节封装在内部,开发者仅需写少量代码,即可完成网络请求。
Github仓库,Google竟然还有在维护这个库,而且有API文档 《Volley 概览》,文档中介绍了它的一些优势:
使用方法很简单,先在build.gradle中添加依赖:
implementation 'com.android.volley:volley:1.1.1'
然后写段简单的请求示例(官方示例有Kotlin版~)
// 创建请求队列
RequestQueue mQueue = Volley.newRequestQueue(context);
// 构造请求
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);
相比起上面直接粗暴的写法,代码赏心悦目了不少,文档中还给出了一张Volley中请求的处理流程图:
让我们大概知道,Volley中的请求,还支持缓存,命中缓存直接读缓存里的数据,不用再去请求后台。
实际业务开发中,都是OkHttp的天下了,Volley基本绝迹了,也不推荐在项目里用。但其设计思想是可以借鉴的,代码虽简五脏俱全,了解它对后续其他框架的学习也有所裨益,故本节花上亿点点时间来对他进行 拆解
。
0x2、并发设计
像上面HttpUrlConnection的例子,每发起一次请求,都新起一个线程,太蠢了:
- 没复用线程,频繁创建销毁线程造成不必要的开销;
- 没对最大线程数做一个限制,可能会造成过度资源竞争,系统使用率不高;
所以涉及到并发的开源项目,线程池 基本是没得走的了,再配个 任务队列 进出队列操作加锁,有时为了解耦,还会再拆出来一个 调度器,死循环 访问 任务队列,取出任务交由线程池执行。
Volley是支持并发的,看下它是如何设计的,没有找到线程池初始化代码,倒是找到了 两个线程实现类:
NetworkDispatcher
继承 Thread类,所以这里默认定义了 容量为4的线程数组,另外一个线程 CacheDispatcher
。它们都在 RequestQueue → start()
中启动:
跟下start()的调用处 Volley → newRequestQueue
:
就是实例化RequestQueue的时候,就创建并开启了这些线程。
0x3、请求调度设计
调用 RequestQueue → add()
请求入队,跟下具体实现:
用到三个集合,根据对应注释推演出各种用途:
- mCurrentRequests : HashSet → 正在处理的所有请求(包括等待和处理中)的集合;
- mNetworkQueue : PriorityBlockingQueue → 非缓存请求队列;
- mCacheQueue : PriorityBlockingQueue → 缓存请求队列;
入队比较简单,接着看下具体的调度流程,不难看出分成两类,走缓存(默认)和不走缓存,先看前者~
① 不走缓存的请求
NetworkDispatcher → run()
死循环拿队列的请求,处理请求后的结果流向有四个:
- 请求处于取消状态 → request.finish("network-discard-cancelled");
- 304且响应已解析过 → request.finish("not-modified");
- 正常结束 → mDelivery.postResponse(request, response);
- 异常结束 → mDelivery.postError(request, volleyError);
先跟下 Request → finish()
:
比较简单:移除集合中的请求,打日志,调用下请求结束的回调,接着看下后两个,跟下 ExecutorDelivery
跟下 mResponsePoster
初始化部分代码:
好吧,就是一个 执行器
,负责将任务分发出去,因为后续操作在主线程操作,所以得用 Handler。
接着,看下任务具体做了啥 ResponseDeliveryRunnable → run()
正常请求的流转还是很好理解的,接着看下走缓存请求的调度过程~
② 走缓存的请求
直接跟 CacheDispatcher → run()
,同样是死循环,直接看 processRequest()
部分代码:
简单点说:有缓存且缓存没过期直接返会,其他情况把请求加到请求队列中。接着再看看缓存这块是咋设计的~
0x4、缓存设计
首先,缓存适用于那些不会更新服务端数据的请求,所以一般是缓存GET请求,很少对POST请求进行缓存。
其次,缓存是非必要的,但加了会带来两个好处:
- 客户端/浏览器 → 减少网络延迟,加快页面打开速度;
- 后台 → 减少带宽消耗,降低服务器压力;
在扒Volley缓存的实现细节前,我们试试自己来设计一个缓存,从一个简陋的方案开始:
- 键值对集合存储,URL做Key,缓存(响应结果)为Value;
- 执行请求前,先到集合里根据URL查,查到直接返回缓存,查不到执行请求,请求结束后存起来;
问题来了:后台的资源是会变的,上一秒是这个,下一秒是那个,此时客户端还用本地缓存,结果可能是不对的!
- 后台配合,给资源加上有效期,可以是 直接过期时间,也可以是 资源生产时间 + 有效时长,客户端缓存的时候绑定上这个;
- 客户端请求时,如果命中缓存,校验缓存是否在有效期内,在直接返回缓存,没有则执行请求;
问题又来了:后台资源变化,客户端是无感知的,以为缓存还在有效期内,不会去请求新数据。
- 后台变化,主动通知客户端显然不太行 (硬要主动推送让客户端刷新也可以);
- 客户端还是得主动去请求下后台;
- 后台为资源添加上 标记,第一次请求时丢给客户端,客户端下次请求相关链接时丢给后台,后台判断TAG和自己算的是否相等,不等说明资源发生改变,给予客户端不同的反馈。比如:没发生改变,只返回一个304的状态码,发生改变,返回 200的状态码 + 新的响应,客户端根据状态码,决定是读缓存,还是解析新响应。这样还有个好处:后台的资源有可能一直没变,虽然过了有效期,但是没必要刷新,这样客户端也不需要更新缓存。
2333,上面的流程其实就是 保证HTTP缓存一致性 的部分思路,上面说到的 附加信息 都放在 信息报头 中:
HTTP缓存由多种规则,根据是否需要重新向服务器发起请求来分类,分为两大类:
- 强制缓存 → 缓存数据未失效,直接使用缓存数据,用响应头中的 Expires/Cache-Control 来标明失效规则;
- 对比缓存 → 第一次请求时后台将缓存标识一起返回给客户端,客户端再次请求数据时,将备份的缓存标识发送给服务器,服务器根据标识进行判断,返回304代表客户端可以使用缓存数据,其他则说明不能使用缓存数据。然后这个缓存标识一般分两种:资源修改时间 → Last-Modified/If-Modified-Since,后台生成的资源标识 → Etag/If-None-Match,后者优先级高于前者。
两类缓存可以混合使用!具体的HTTP缓存策略流程图如下:
笔者对此也只知道个大概,感兴趣想深入了解的可见:《浏览器 HTTP 协议缓存机制详解 》
Volley中也是这样的缓存策略:
接着看看缓存的定义 Cache
接口,包含了一个实体缓存实体 Entity
和缓存相关的操作方法。
跟到具体实现类 DiskBasedCache
,关注缓存增删,先看看 put()
:
简单说下流程:将消息报头和响应数据依次写入文件,然后存一个消息报头到缓存集合中,请求时判定缓存是否可用时用到。接着看看 get()
:
很好理解,缓存命中后,响应实体从流 → 字节数组 → Entity实例返回。其它情况返回Null,表示没有缓存可用。其它方法先不看了,接着看看这个 CacheKey 是怎么设计的?
跟下哪里调的put()方法 → NetworkDispatcher → processRequest()
:
跟下 Request → getCacheKey()
:
所以Key就是两种:
- GET || DEPRECATED_GET_OR_POST → 直接url
- 其它 → 请求方式数值-url
直接URL做Key,真·简单粗暴啊!
0x5、请求处理细节
回到Volley的入口方法 newRequestQueue()
,啧啧,扩展性体现之一:
HurlStack
对应HttpUrlConnection的封装,HttpClientStack
对应HttpClient的封装,还可以自行定制,继承 BaseHttpStack
类,重写 executeRequest()
返回处理后的响应即可。
另外,这里用 代理模式
套了一层,代理类是 BasicNetwork
,在 HttpClientStack
的基础上做一些附加操作。
所以,这个附加操作就是:获取缓存相关的请求头,对响应结果/异常的处理。然后,看到异常重试都是调用的 attemptRetryOnException()
:
跟下 RetryPolicy
发现是一个接口:
看下哪里实现了这个接口,跟到默认实现类 DefaultRetryPolicy
,可以看到默认最大重试次数为1:
调用一次 retry()
计数加1,如果<=最大重试次数,抛出异常
可以看到这里只是做了一个计数,并没有进行执行请求,或者请求入队的操作,那请求是怎么重新发起的?
注意这里的死循环,以及 重试方法的层级,是在catch里的,如果此时超过重试次数,抛出异常,就能退出这个死循环。
这样不会崩溃吗?当然不会,因为在调用此方法的 NetworkDispatcher
中套了一层try-catch兜底:
当真是妙啊!!!
最后还有一个点:Volley使用 内存缓存
来存放请求获得的数据,而不是直接在内存中开辟一个区域存放。
这是为啥?因为网络请求一般会很频繁,不停创建byte[],会引起频繁的GC,间接对APP性能造成影响。所以Volley定义了 ByteArrayPool
来缓存数据。
看着有点懵是吧,我简单说说:
- 规定了缓存池的默认大小为:DEFAULT_POOL_SIZE = 4096字节 = 4KB,池中维护两个列表,一个按照 最近使用顺序 排序(表①),一个按照 byte[]大小 排序(表②);
- 从缓存区取空间:不直接开辟空间,先循环迭代查找列表②中是否有 len>=所需字节 的已开辟空间,有的话返回此空间,没有的话开辟新的空间返回;
- 将空间返还给缓存区:检查插入数据是否超出边界,有的话直接返回,没有的话添加到表①尾部,然后二分查找找到表②中合适的位置插入,当前开辟字节数增加。同时判断是否超过最大值,是回收表②的第一个元素;
0x6、可扩展性
Tips:其实看下都有哪些接口和抽象类,就知道有啥可以自定义的~
RequestQueue构造方法传入:
- Cache → 缓存,默认是将响应数据放磁盘中,你可以弄成二三级缓存,实现接口;
- Network → 请求框架封装,默认封了HttpUrlConnection和HttpClient,弄成其他请求库也可以;
- ResponseDelivery → 响应交付,就是请求响应的后续处理,正常与异常情况的处理;
Request构造方法传入
- RetryPolicy → 请求失败重试策略;
其它
- Request → 请求,toolbox里有常用的实现类,如:JsonRequest、ImageRequest、StringRequest等;
- Authenticator → 令牌授权
大概就这些吧,toolbox里还送了一个图片加载的工具,不过性能不算特别好,就不展开讲了。
0x7、小结
本节对请求库Volley的设计进行了多方面的拆解,获益良多,至少现在让我封装一个请求库,不会无从入手了。
源码难啃,但弄懂了设计的原理,就会觉得豁然开朗,妙啊,也顺应了前老大说的,品经典项目源码,如品经典名著般,沁人心脾~
参考文献:
转载自:https://juejin.cn/post/7052982509826474020