前言
Volley是Google在2013I/O大会上发布的一款轻量级Android端网络请求框架,它的特点:适合数据量小,通讯频繁的网络请求操作。在前两篇文章我们分析了OKHttp的源码,通过源码的分析能让我们对HTTP有更加深入的了解,并且每一个优秀的框架内部都涵盖了很多设计模式,这些都是非常值得我们去学习的,所以今天我们继续分析另一款优秀的HTTP网络请求框架Volley。
注:本文只分析关于Volley运行流程方面源码,对HTTP细节、缓存、四大Request源码暂不做分析
1. Volley的使用
RequestQueue queue = Volley.newRequestQueue(this);
StringRequest request = new StringRequest(Request.Method.GET,"https://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.i("volley","success:"+response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.i("volley","error:"+error.toString());
}
});
queue.add(request);
打印结果:
<html><!--STATUS OK--><head><meta charset="utf-8"><title>百度一下,你就知道......
StringRequest中四个参数分别是:请求方法、url、成功回调、失败回调。请求体定义完毕加入queue中即可进行请求。StringRequest只能接收字符串结果,在Volley中为我们提供了四种Request分别如下:
- StringRequest 响应的主体为字符串
- JsonArrayRequest 发送和接收JSON数组
- JsonObjectRequest 发送和接收JSON对象
- ImageRequest 发送和接收Image
使用方式基本一致,所以就不一一叙述。另外一个Activity创建一个RequestQueue即可,至于为什么分析完源码你就知道了。Volley的使用相比于OKHttp还是简单不少的,下面我们进入源码分析部分。
2. 源码分析
首先从入口Volley开始分析
2.1 Volley
Volley中代码非常简单,里面就做了一件事情:构造RequestQueue,内部只存在四个方法分别是newRequestQueue()的四种重载形式,不过最终调用的是拥有三个参数的newRequestQueue(),源码如下:
public static RequestQueue newRequestQueue(Context context, HttpStack stack,
int maxDiskCacheBytes) {
//定义磁盘缓存文件
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();//SDK大于9使用HttpUrlConnection
} else {
//SDK<9使用HttpClient
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
//网络请求类
Network network = new BasicNetwork(stack);
RequestQueue queue;
if (maxDiskCacheBytes <= -1) {
//未指定缓存大小
queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
} else {
// 指定了磁盘缓存大小
queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
}
//开启
queue.start();
return queue;
}
构造RequestQueue大概分为4步分别如下:
- 创建磁盘缓存文件
- 判断当前版本,如果>=9使用HttpUrlConnection否则使用HttpClient
- 构造网络请求类Network
- 构造RequestQueue
RequestQueue构造完毕后调用start()方法开启,此时很多同学会有疑问,RequestQueue是什么?调用start()开启的又是什么,别急我们马上分析。
2.2 RequestQueue
RequestQueue可直译为请求队列,就是用来存放请求的队列,下面我们来分析一下它的核心源码:
入队方法add():
public <T> Request<T> add(Request<T> request) {
//标记request所属的队列
request.setRequestQueue(this);
//添加到正在被执行的集合
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
//为request设置序号
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// 如果该请求不能缓存,直接加入到网络请求队列并return
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
//等价于url相同
if (mWaitingRequests.containsKey(cacheKey)) {
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
//后续相同url的请求都放到该链表中
stagedRequests.add(request);
//进入此等待队列
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("...", cacheKey);
}
} else {
mWaitingRequests.put(cacheKey, null);
//加入缓存队列
mCacheQueue.add(request);
}
return request;
}
}
注释写的很清楚,就不再重复解释,这里着重说一下用到的四个集合:
- mCurrentRequests:是一个Map集合,用来存储当前正在执行的Request
- mNetworkQueue:网络请求优先级队列,通过网络获取数据
- mWaitingRequests:是一个Map集合,如果请求可以被缓存并且挣扎被处理,后续有相同url的请求就会进入此等待集合
- mCacheQueue:缓存优先级队列,通过缓存获取数据
开启方法start():
public void start() {
//首先停止所有的线程
stop();
//创建一个缓存线程并开启
mCacheDispatcher = new CacheDispatcher(mCacheQueue,
mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
//创建网络请求线程,默认为4个
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
RequestQueue内部维护了一个默认长度为4的NetworkDispatcher 数组和一个CacheDispatcher对象,这两个类都直接继承自Thread,分别用来从网络和缓存中获取数据。调用start()后者4个网络请求线程和一个缓存线程都会被开启。
结束方法finish():
当一个请求结束后需要将Request从mCurrentRequests、mWaitingRequests移除,如果允许缓存就再加入到缓存,代码中内容很容易理解所以就不贴出来了。
add()、start()、finish()是RequestQueue的三个核心方法,此外它还提供了取消请求、停止线程等方法,都比较简单,感兴趣的同学可自行了解。接下来我们来分析两个线程的源码
2.3 CacheDispatcher
CacheDispatcher是一个用来处理缓存的线程,缓存策略遵循HTTP协议,我们来看其run()方法源码:
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//初始化缓存
mCache.initialize();
Request<?> request;
while (true) {
//释放上一个request对象
request = null;
try {
//从缓存队列获取请求
request = mCacheQueue.take();
} catch (InterruptedException e) {
// 线程被停止直接return
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("cache-queue-take");
// 该请求已经被取消
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
//根据cacheKey从缓存中得到对应的记录
Cache.Entry entry = mCache.get(request.getCacheKey());
//缓存中没有cacheKey对应的记录,
if (entry == null) {
request.addMarker("cache-miss");
//将请求加入到网络请求队列
mNetworkQueue.put(request);
continue;
}
//如果缓存中有记录,但是已经过期,则加入到网络中进行获取
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
//能执行到这说明命中了一个有效缓存
request.addMarker("cache-hit");
//将响应体进行格式转换
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
//缓存记录不需要更新,
if (!entry.refreshNeeded()) {
//直接在UI线程中处理响应结果
mDelivery.postResponse(request, response);
} else {//通过网络验证,缓存为软过期,即虽然过期但是可用
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
response.intermediate = true;
final Request<?> finalRequest = request;
//将response响应给UI线程并更新网络数据
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
//从网络中更新数据
mNetworkQueue.put(finalRequest);
} catch (InterruptedException e) {}
}
});
}
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
}
}
}
整个流程完全遵循HTTP缓存策略,代码较多但核心代码就那么几句,下面我简要描述一下从缓存获取具体流程:
- 拿着request试图从缓存中火树response
- 如果缓存不存在对应数据直接将request加入到网络队列,从网络获取数据
- 如果缓存存在但已经过去也将request加入到网络队列
- 如果缓存新鲜则直接将response响应给UI线程
- 如果缓存不新鲜将response响应给UI线程后再去网络做新鲜度验证
2.4 NetworkDispatcher
NetworkDispatcher 是一个进行网络请求的线程,run()方法源码如下:
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
request = null;
try {
//从优先级队列中取请求体,take()方法为阻塞式方法
request = mQueue.take();
} catch (InterruptedException e) {
if (mQuit) {//线程已经停止直接return
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
if (request.isCanceled()) {//判断请求是否被取消
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
//进行网络请求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
//服务器返回304,不返回响应body
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
//将响应体进行转换String、JSON等等
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// TODO: Only update cache metadata instead of entire record for 304s.
//将响应体加入缓存
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// Post the response back.
request.markDelivered();
//在UI线程中回调
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
很多代码跟CacheDispatcher类似,注释写的很清楚,下面我简要描述一下流程:
- 从网络队列中取出一个请求体,判断请求是否取消,如果是直接continue
- 通过Network进行网络请求
- 服务器返回304代表更新缓存,所以不存在body直接结束即可
- 将response转换为响应的格式
- 判断是否需要加入缓存
- 响应给UI线程
代码虽然看去来眼花缭乱但逻辑也很简单。两个线程拿到数据后都是通过ResponseDelivery类型的mDelivery响应给UI线程的,下面我们来分析ResponseDelivery的实现类ExecutorDelivery的源码
2.5 ExecutorDelivery
首先来看ExecutorDelivery的一个内部类ResponseDeliveryRunnable:
public void run() {
// 如果请求已经取消直接return
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// 请求成功回调成功方法
if (mResponse.isSuccess()) {
//响应请求的回调接口,对应我们请求时写的成功回调方法
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
...
...
}
ResponseDeliveryRunnable实现Runnable接口,run()方法中将Response响应至UI线程。
ExecutorDelivery中也维护了一个Executor,主要是用于跟UI线程通讯,来看Executor实现源码:
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
//将任务切换到UI线程
handler.post(command);
}
};
通过Handler实现与UI线程的通讯,command即ResponseDeliveryRunnable类型对象。
NetworkDispatcher和CacheDispatcher获取到response时都会调用ExecutorDelivery的postResponse(request, response)方法,来看一下这个方法的源码:
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
很简单,只是将ResponseDeliveryRunnable任务传递给了Executor,最后会将response响应给UI线程。代码没啥难度,就不再进行赘述。
Volley逻辑上的代码大概就是这些。
总结
Volley的使用非常简单,如果碰到频繁做数据量小,通讯频繁的网络请求我还是推荐使用Volley。内部源码也比较简单,主要逻辑都封装在了消息队列和两个线程中。由于Volley的定位是一款轻量级框架所以复杂程度远远比不上OKHttp,但里面的一些设计思想是非常值得我们去借鉴的。本篇文章差不多就这些内容了,今天是元宵节,祝大家节日快乐。
网友评论