Volley 是 Android 中一个经典的网络框架,所以现如今面试的时候问 Volley 的源码分析有很多,那我们就有很有必要细致的研究一下它的源码了,让我们走进它的内心世界里吧!
一些错误的理解
在分析 Volley 源码之前, 我们纠正一下一些错误的观念,在比较网络框架的时候,有人会问到:“Volley 和 Okhttp 哪个网络框架好?”或者 “HttpURLConnection 和 Volley 哪个网络框架好?”,虽说都是可以用来请求数据的,但是一般都不会这么讲,因为 HttpURLConnection 和 Okhttp 是一个级别的, Volley、Retrofit、OkhttpUtils、Xutils 是一个级别的,他们之间的关系我画了一张图。
Volley 网络请求步骤
在我们分析源码之前我们要理清思路,不然一头扎进去,估计两眼也懵逼,但是 Volley 源码读起来真心的赏心悦目,很流畅, 那我们按照什么思路呢? 我们要关注他的关键的地方,比如为什么他能能够轻松处理好高并发的网络请求?网络请求的步骤在 Volley 中是如何实现的呢?我们有目的性的研究源码才可以高效的理解它的流程。
我们开始来分析 Volley 的网络请求步骤,它是如何发起的请求,如何处理的请求,最后返回给我们客户端。
我这里主要分析它的源码,默认大家都用过 Volley ,我这里简单的写一下的引入步骤,首先引入他的gradle,这个不是 google 官方的 引入地址,不保证以后都有效。
compile 'com.mcxiaoke.volley:library:1.0.19'
然后我们开始用知乎开源 api 来做一次最简单的 StringRequest Get 请求,网络请求代码如下:
//创建一个网络请求队列
RequestQueue requestQueue = Volley.newRequestQueue(this);
String url = "http://news-at.zhihu.com/api/4/news/latest";
//创建一个网络请求
StringRequest request = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.e("SourceAnalysis", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("SourceAnalysis", " error:" + error.toString());
}
});
//网络请求添加到请求队列
requestQueue.add(request);
创建一个 RequestQueue 队列
我们发现在第一步我们会创建一个 RequestQueue , 官方为什么设计的初衷是什么?为什么首先就先创建它,我们一般在做什么事情的时候之前都会做出一些准备,那们可以根据这个猜测来看一下 Volley 源码中在创建 RequestQueue 的时候会做什么准备,我们查看 RequestQueue 中有四个构造方法,他们分别是:
public static RequestQueue newRequestQueue(Context context)
public static RequestQueue newRequestQueue(Context context, HttpStack stack)
public static RequestQueue newRequestQueue(Context context, int maxDiskCacheBytes)
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes)
newRequestQueue(Context context, HttpStack stack) 可以用来自定义底层的网路请求,这里我们就不做探究,如果感兴趣的话,可以查看 这篇文章 ;public static RequestQueue newRequestQueue(Context context, int maxDiskCacheBytes) 这个方法从参数名称就可以看出他的意思,指定最大的缓存大小。
上边的构造方法都会最终调用三参的构造方法,我们查看这个三参的构造方法
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
...
//HurlStack 的初始化
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue;
//缓存的大小的初始化
if (maxDiskCacheBytes <= -1)
{
// No maximum size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
}
else
{
// Disk cache size specified
queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
}
//重点关注这个方法 ,这是开启网络请求的关键
queue.start();
return queue;
}
我们来分析一下网络库的初始化,如果 stack == null 我们才用自己默认的,否者就会用自定义的底层网络请求库。然后是设置缓存大小如果没有设置就会走默认的,如果设置的话会走赋值自定义的值。我们需要关注的是网络请求这方面的初始化,可以留意一下BasesicNetwork 和 HurlStack 这两个类,后边我们真正网络请求的关键就这这两个类。
然后我们继续往下跟踪 RequestQueue 的创建过程,最后还调用了他自己的 start() 方法,我们看一下他的所用的方法的源码。
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
//三参数调用四参数的构造方法,并且创建了线程池分发器
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
//二参调用三参数的构造方法,并且传了默认的线程池大小=4
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
//关键的方法,创建出四个网络请求分发器和一个缓存网络分发器
public void start() {
stop(); //保证现在运行的网络请求分发器都关闭
//创建缓存网络请求分发器并启动
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
//创建四个网络请求分发器并启动
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
我们从上边的代码中看到一个创建了一个线程池分发器(ExecutorDelivery) ,四个网络请求分发器 (NetworkDispatcher ),一个缓存网络请求分发器(CacheDispatcher)。
创建一个 StringRequest 请求
这个是我们发起的请求,我们可以注意上边的方法有四个参数的方法,(int method, String url, Listener listener,ErrorListener errorListener) ,我们可以主要关注一个后边的两个回调,一个是成功的回调,一个是失败的回调,而且我们可以看一下他的源码。
public class StringRequest extends Request<String> {
private Listener<String> mListener;
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
//如果不传Method默认使用Get请求
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
@Override
protected void onFinish() {
super.onFinish();
mListener = null;
}
//这个方法要注意了,是后边网络请求后通过dliver回调listener回调结果返回的重要方法
@Override
protected void deliverResponse(String response) {
if (mListener != null) {
mListener.onResponse(response);
}
}
//这个方法是作用是: 解析 NetworkResponse -> Response<String>
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}
将 StringRequest 添加到 RequestQueue 中
上边我们分析完 创建了请求队列 RequestQueue 中,然后创建了 StringRequet,然后我们将请求添加到请求队列中,我们看看会发生什么化学反应。
public <T> Request<T> add(Request<T> request) {
//把这个请求和当前队列发生关系
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// 按照顺序添加到队列
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
//如果这个请求不能进行缓存则将其直接添加到网络请求队列,request.shouldCache()
//这个一般都是返回true,所以一般会走这个方法,可以直接跳过这个方法
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
//如果有同样的请求真正进行中,则保持多线程同步
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
//如果是同样的请求,包含cacheKey则添加到
if (mWaitingRequests.containsKey(cacheKey)) {
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
...
} else {
//添加一个cacheKey 表明当前请求正在进行中
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
通过我们上边的注释分析,一般的请求都会要求缓存,如果一个新的请求添加进来,mWaitingRequests.containsKey(cacheKey) 的值是 false ,所以直接回将请求添加到到 mCacheQueue 缓存队列中,同时将 mWaitingRequests 添加对应这个请求cacheKey(cacheKey: mMethod + “:” + mUrl 字符串的拼接),所以下一次同样的请求直接添加到 mWaitingRequests 中,然后等待的请求怎么办? 别着急下边我们会分析。
private final PriorityBlockingQueue<Request<?>> mCacheQueue =
new PriorityBlockingQueue<Request<?>>();
private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
new PriorityBlockingQueue<Request<?>>();
到此为止,我们可以看到两个请求队列,从上边分析我们只看到往 缓存队列 mCacheQueue 和 等待请求队列中加入了,没有往 请求队列 mNetworkQueue 中添加。但是分析到这我们有点断片了,只是将请求添加到队列中,怎么从队列中取出并请求,然后返回给我们的回调函数?那我们下一步的研究重心就在分析如果处理队列中的请求的问题了。
处理 RequestQueue 中的请求
从上面分析add()方法我们可以得知 Volley 的每一个请求都会先进入缓存队列,而且我们在创建RequestQueue的时候创建了四个网络请求线程和一个缓存线程,这时候我们应该直接分析一下缓存线程 CacheDispatcher 的 run() 方法里边有什么注意的地方,我们来分析一下它的代码。
@Override
public void run() {
...
Request<?> request;
while (true) {
request = null;
try {
//从缓存队列中取出一个请求
request = mCacheQueue.take();
} catch (InterruptedException e) {
//如果已经拦截则退出循环
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("cache-queue-take");
// 如果已经取消,则跳出此次请求,执行下一个请求
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// 看这个请求是否有缓存,如果没有则添加到网络请求的队列中,并执行下一个请求
Cache.Entry entry = mCache.get(request.getCacheKey());
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;
}
//如果有缓存,则取出缓存赋值给response
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
//检查缓存是否新鲜,如果不新鲜则重新添加到网络请求队列,如果新鲜则通过分发器回调给请求
if (!entry.refreshNeeded()) {
mDelivery.postResponse(request, response);
} else {
...
final Request<?> finalRequest = request;
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());
}
从上边缓存线程代码的分析,如果有缓存最终会调用 mDelivery.postResponse(request, response) 方法来回调给我们的Request,我们下文会分析如何回调的,如果没有缓存会把缓存队列取出的请求添加到网络请求队列中,那我们继续分析真正的网络请求是如何调用的呢?继续跟一个 NetworkDispatcher 里边的run() 方法。
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
//循环从队列中取出请求
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
//释放以前的请求,置空
request = null;
try {
//从队列中拿出请求
request = mQueue.take();
} catch (InterruptedException e) {
//如果已经退出就直接退出循环
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
//如果请求在过程中被取消,则跳出这次循环,执行下一个请求
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// 执行网络请求得到相应的结果(mNetWork就是刚开始的BasicNetWork)
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// 如果返回的结果与上次比较没有变化,或者已经请求过了,就不返回结果,直接执行下一个请求
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// 在工作线程解析networkResponse,并得到我们想要的Response结果
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
//写入缓存
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
request.markDelivered();
//通过分发器回调给我们的请求
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
...
}
}
真正的网络请求
从上边的源码分析,我们可以得到的结果是通过 mNetwork.performRequest(request) 来得到我们想要的结果,这个网络请求是怎么进行的呢,mNetWork 我们从创建 RequestQueue 的时候就创建了,也就是 NetWork 的实现类 BasicNetwork,那我们看一下 BasicNetwork 的 performRequest(request) 方法。
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = Collections.emptyMap();
try {
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
//我们通过mHttpStack方法进行真正的网络请求
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
...
return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
SystemClock.elapsedRealtime() - requestStart);
}
}
}
分析代码我们可以明白真正网络请求还没有找到,BasicNetWork 只是将结果进行了进一步封装,为了找到真正的网络请求,我们继续往下跟踪源码, mHttpStack.performRequest(request, headers) 方法,我们首先看看 mHttpStack是什么?
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
mHttpStack = httpStack;
mPool = pool;
}
发现 mHttpStack 是构造方法传过来的,我们可以看一下我们上边分析创建RequestQueue的时候就是调用的这个构造方法,当时我们说过 Volley 也支持okhttp只需要实现 HttpStack 即可,如果没有自定义实现,那么我们用的就是 HttpURLConnection 来进行网路请求,我们从上边可以知道用的是 HurlStack 的 performRequest() 方法,我们来看一下主要代码。
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
...
URL parsedUrl = new URL(url);
// 我们熟悉的网络请求
HttpURLConnection connection = openConnection(parsedUrl, request);
for (String headerName : map.keySet()) {
connection.addRequestProperty(headerName, map.get(headerName));
}
....
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
...
//返回结果
return response;
}
分析以上代码我们可以清楚的明白是 Volley 默认用的是 HttpURLConnection 进行的网络请求,并返回 response交由BasicNetwork对Response进一步封装并返回。
如果回调给我们的 StringRequest 请求
我们在 NetworkDispatcher 中进行网路请求或者 CacheDispatcher 回调都会调用 mDelivery.postResponse(request, response) 那我们就看一下 mDelivery 的这个方法实现。
//通过构造方法创建一个线程池,来接收并执行Runnable
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
//2参调用3参
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
//向线程池发送Runnable
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
private class ResponseDeliveryRunnable implements Runnable {
...
@SuppressWarnings("unchecked")
@Override
public void run() {
// 如果结果是正确的,则回调Requet的deliverResponse,反之,调用Requet的deliverError
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
...
}
}
最终回调了Requet中的方法,从而回调给我们的Listener和ErrorListener 然后我们得到相应的结果,从而再进行自己的业务处理。那么我们的Volley源码分析就到此为止。
总结
下面我放一下我画的Volley源码分析的流程图。