很久以来一直想要分析一个开源框架来玩玩,最近看到Volley源码确实是质量特别高,抛去一些大数据传输的瑕疵不说,单就简单数据的访问和图片加载,已经可以打高分了,而且关键是它的源码小巧精致,适合像我这种懒人来搞一搞,所以今天就拿它来开刀了.
公司里的项目最近需要使用到Https访问,其实我已经写好了基于HttpUrlConnection和HttpClient的单双向Https访问接口,但是由于在API23中HttpClient被废弃了,纯粹使用HttpUrlConnection搞起来又太麻烦,所以我还是使用了官方的Volley,再加上我项目中网络请求的数据量也不大,确实蛮适合的,但是Volley中却没有发现Https访问接口,本着没有轮子造一个的精神,我决定解剖一下Volley,顺便给它装上Https这个轮子。
准备
讲正事儿之前先预个热,Volley的使用大家应该都会,我们先来回忆一下
RequestQueue requestQueue = Volley.newRequestQueue(context);
StringRequest stringRequest=new StringRequest(Request.Method.GET, url, listener, errorListener);
requestQueue.add(stringRequest);
除了StringRequest,还有ImageRequest,JsonRequest等等等等,我们还可以自定义Request,当然这不是我们今天中重点。
还有就是初始源码的下载,有梯子自己去Google下载,没梯子网上一搜一大把。
使用流程明白了,分析流程也明白了:
- 分析Volley这个对象以及其中的方法,尤其是newRequestQueue(Context context)
- 分析各种Request的接口,实现类,作用
- 分析RequestQueue请求队列,重点是add(Request request)
Volley
先来搞定Volley,直接上代码
/**
* @author jayclf
* Volley框架的起始点,通常我们都会这样做
* Volley.newRequestQueue(Context context)
*/
public class Volley {
/** 磁盘上的默认缓存目录. */
private static final String DEFAULT_CACHE_DIR = "volley";
/**
* 创建一个请求队列的实例并且在它上面调用 {@link RequestQueue#start()} 开启该队列.
* 你可以以bytes单位来设置磁盘缓存的最大空间.
*
* @param context 用户创建缓存目录的 {@link Context}.
* @param stack 用于网络连接的 {@link HttpStack} 默认为null.
* @param maxDiskCacheBytes 磁盘缓存的最大值, 单位是bytes. 传-1表示使用默认值.
* @return 已经开启的 {@link RequestQueue} 实例.
*/
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
//创建缓存文件
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
//该userAgent用于创建HttpClientStack,后面会看到
String userAgent = "volley/0";
try {
//最终userAgent的形式是"包名/应用版本号"
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
//虽说一般情况下不会发生此类异常,但是对于处女座这样子还是不太好...
//打印一下下吧
e.printStackTrace();
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
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;
}
/**
* 方法重载,用户无需指定HttpStack
*
* @param context 用户创建缓存目录的 {@link Context}.
* @param maxDiskCacheBytes 磁盘缓存的最大值, 单位是bytes. 传-1表示使用默认值.
* @return 已经开启的 {@link RequestQueue} 实例.
*/
public static RequestQueue newRequestQueue(Context context, int maxDiskCacheBytes) {
return newRequestQueue(context, null, maxDiskCacheBytes);
}
/**
* 方法重载,使用默认缓存大小
*
* @param context 用户创建缓存目录的 {@link Context}.
* @param stack 用于网络连接的 {@link HttpStack} 默认为null
* @return 已经开启的 {@link RequestQueue} 实例.
*/
public static RequestQueue newRequestQueue(Context context, HttpStack stack)
{
return newRequestQueue(context, stack, -1);
}
/**
* 方法重载,懒得写了
*
* @param context 用户创建缓存目录的 {@link Context}.
* @return 已经开启的 {@link RequestQueue} 实例.
*/
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
}
这代码一目了然,Volley这个类的作用原来这么简单:
- 定义缓存目录”volley”
- 使用方法重载,创建RequestQueue,可以设定要使用的HttpStack,磁盘缓存大小(单位byte)
HttpStack是啥?不知道!磁盘缓存大小?没设定过!所以我们最常用的还是第四个方法,不用传HttpStack也不用设定缓存大小,全默认的。
方法重载最后都会调用到第一个,这个方法这么长,一看就是干正事儿的:
- 创建缓存目录
- 创建一个userAgent字符串,用于创建HttpClientStack
- 如果用户没给HttpStack参数的话,就根据当前的APILevel创建不同的HttpStack,API9以前的创建HttpClientStack,API9以及以后的创建HurlStack
- 创建一个Network的实例:BasicNetwork对象,把刚才创建的HttpStack传进去
- 正式创建RequestQueue对象,它需要Network实例和一个DiskBasedCache对象,我们可以看到这个DiskBasedCache的构造也是方法重载,根据用户是否指定缓存大小调用不同的构造方法
- 调用RequestQueue的start方法开启消息队列
看来我们分析Volley的任务有了继续向前的目标:
- 分析HttpStack以及他的两个子类
- 这个Network接口和其子类BasicNetwork有必要看一下是干嘛的
- 可以看看这个DiskBasedCache类中是干了什么
- RequestQueue的构造和start方法是干嘛的
至于为啥要根据API9前后创建不同的HttpStack对象,你看人家不写明了嘛,”Prior to Gingerbread, HttpUrlConnection was unreliable.”就是说9以前HttpUrlConnection不可靠,有BUG,9以后就被修正了,具体可以看看它给的那个博客。从这里我们可以看出HttpClientStack应该是使用HttpClient进行工作的,而HurlStack应该是基于HttpUrlConnection。
HttpStack
翠花!上代码!
public interface HttpStack {
/**
* 使用给定的实现一个 HTTP 请求.
*
* <p>
* 如果request.getPostBody() == null的话会发送GET请求.
* 如果Content-Type header 被设置为request.getPostBodyContentType()和其他情况下,会发送POST请求.
* </p>
*
* @param request 要执行的请求
* @param additionalHeaders 要一起发送的头信息 {@link Request#getHeaders()}
* @return HTTP 响应
*/
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
}
看来实际的请求都是由HttpStack及其子类来完成的,注释上的注意点还请留意一下,还有就是我们看到请求都被封装成了Request对象,而且还可以设置额外的请求头,也就是说Request已经有了基本的请求头,如果需要修改或者添加额外的请求头也是可以的。
HttpClientStack
先来看看成员和构造
//果然有一个成员变量HttpClient
protected final HttpClient mClient;
//设置请求头中的Content-Type用的字段
private final static String HEADER_CONTENT_TYPE = "Content-Type";
public HttpClientStack(HttpClient client) {
//HttpClient对象是在构造函数中传过来的
mClient = client;
}
果然是有一个HttpClient,这个HttpClient对象在构造的时候从外部传入,我们回到Volley中那段初始化HttpClientStack的代码中一看,传过来的是一个AndroidHttpClient对象,似乎还是个单例,附带了一个userAgent字符串,那这个AndroidHttpClient对象我们等下也要进去看看。
我们直接看看它重写HttpStack的方法吧:
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
//把Request对象转换承HttpUriRequest对象
HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
//把请求头设置到HttpUriRequest中
addHeaders(httpRequest, additionalHeaders);
addHeaders(httpRequest, request.getHeaders());
//onPrepareRequest方法目前是一个空实现,可以在子类中重写该方法以在请求之前做一些额外的工作
onPrepareRequest(httpRequest);
/**
* 下面就是我们使用HttpClient的常规操作了
* 设置一些请求参数之后就调用execute方法执行该请求
*/
HttpParams httpParams = httpRequest.getParams();
int timeoutMs = request.getTimeoutMs();
// TODO: 超时时间在这里写死了,我们可以在这里动态的设置超时时间,比如针对WIFI,3G等不同的网络环境
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
return mClient.execute(httpRequest);
}
其实大家看注释也就懂了,这就是我为啥喜欢写详细注释的原因(其实也就是把人家的注释翻译了一下而已,囧…)。
这个方法中还有一个地方,就是我们把Request对象转换成了HttpUriRequest对象,有人会在这里有疑问,其实这并不难理解,Request是Volley包中的类,而HttpUriRequest是apache的HttpClient中HttpRequest的子类,两个虽然名字一样,但是HttpClient要execute的是自家的HttpRequest,而不是Volley中的Request,所以需要做一个转换。
看看这个转换是怎么做的:
/**
* 通过传递过来的Request创建出HttpUriRequest合适的子类
*/
@SuppressWarnings("deprecation")
/* protected */ static HttpUriRequest createHttpRequest(Request<?> request, Map<String, String> additionalHeaders) throws AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST: {
/**
* 该方法是过时的,我们需要做向后兼容的处理.
* 如果请求的POST部分为空,那就认为该请求为GET请求,否则的话就设置为POST请求
*/
byte[] postBody = request.getPostBody();
if (postBody != null) {
HttpPost postRequest = new HttpPost(request.getUrl());
//因为是Post请求,所以要为请求增加"Content-Type"请求头
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
//设置Post的内容
HttpEntity entity;
entity = new ByteArrayEntity(postBody);
postRequest.setEntity(entity);
return postRequest;
} else {
return new HttpGet(request.getUrl());
}
}
case Method.GET:
return new HttpGet(request.getUrl());
case Method.DELETE:
return new HttpDelete(request.getUrl());
case Method.POST: {
HttpPost postRequest = new HttpPost(request.getUrl());
postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(postRequest, request);
return postRequest;
}
case Method.PUT: {
HttpPut putRequest = new HttpPut(request.getUrl());
putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(putRequest, request);
return putRequest;
}
case Method.HEAD:
return new HttpHead(request.getUrl());
case Method.OPTIONS:
return new HttpOptions(request.getUrl());
case Method.TRACE:
return new HttpTrace(request.getUrl());
case Method.PATCH: {
HttpPatch patchRequest = new HttpPatch(request.getUrl());
patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType());
setEntityIfNonEmptyBody(patchRequest, request);
return patchRequest;
}
default:
throw new IllegalStateException("Unknown request method.");
}
}
这个方法是根据Volley的Request对象,生成具体的HttpUriRequest对象,我们知道HttpUriRequest根据具体的请求方式,会有对应的POST,GET子类对象,这里就是生成对应的请求类型,而且我们还发现如果是POST等有参数传递到服务器的请求类型,还需要设置”Content-Type”请求头和POST的内容。
HurlStack
HurlStack实现了HttpStack,使用HttpUrlConnection进行网络连接,先看看成员有哪些:
private static final String HEADER_CONTENT_TYPE = "Content-Type";
/**
* URL转换器
* 一个在使用之前转换URL的接口.
*/
public interface UrlRewriter {
/**
* 为提供的url转换成URL,如果该URL不可用将返回null
*/
String rewriteUrl(String originalUrl);
}
private final UrlRewriter mUrlRewriter;
private final SSLSocketFactory mSslSocketFactory;
在构造中定义了一个URL 转换接口和该类型的变量,应该是用于转换URL的,还有一个SSLSocketFactory对象,用于HTTPS连接。
直接看实现的方法:
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
//从request中取出URL
String url = request.getUrl();
//把Header集中到map中
HashMap<String, String> map = new HashMap<String, String>();
map.putAll(request.getHeaders());
map.putAll(additionalHeaders);
//使用URL转换器对URL进行转换
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL被rewriter阻塞 : " + url);
}
url = rewritten;
}
//封装URL对象
URL parsedUrl = new URL(url);
//进行标准的HttpURLConnection连接
HttpURLConnection connection = openConnection(parsedUrl, request);
for (String headerName : map.keySet()) {
//进行HttpURLConnection设置请求头
connection.addRequestProperty(headerName, map.get(headerName));
}
//设置连接参数
setConnectionParametersForRequest(connection, request);
// 使用HttpURLConnection返回的数据进行HttpResponse的初始化.
ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
//获取响应码
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// 如果没有有效的返回码将返回-1.通知调用者在连接过程中发生错误
throw new IOException("无法从HttpUrlConnection读取响应码.");
}
//封装返回状态栏
StatusLine responseStatus = new BasicStatusLine(protocolVersion, connection.getResponseCode(), connection.getResponseMessage());
//封装返回信息
BasicHttpResponse response = new BasicHttpResponse(responseStatus);
if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
response.setEntity(entityFromConnection(connection));
}
//
for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
if (header.getKey() != null) {
Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
response.addHeader(h);
}
}
return response;
}
主要的工作就这些,创建了一个HttpUrlConnection并进行连接,由于接口定义的返回类型是HttpResponse,所以在后半部分创建了apache 的BasicHttpResponse对象把HttpUrlConnection返回的数据封装起来并返回,一个使用UrlRewriter进行了URL转换,创建连接使用了openConnection方法:
/**
* 使用指定的参数创建 {@link HttpURLConnection}.
* @param url 要连接的URL
* @return HttpURLConnection对象
* @throws IOException
*/
private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
HttpURLConnection connection = createConnection(url);
//设置连接超时
int timeoutMs = request.getTimeoutMs();
connection.setConnectTimeout(timeoutMs);
//设置从主机读取数据超时
connection.setReadTimeout(timeoutMs);
//是否使用用户缓存,由于Volley自己设计了缓存,所以这里是false
connection.setUseCaches(false);
//是否从HttpURLConnection读取数据,默认就是true
connection.setDoInput(true);
// 使用用户提供的自定义SSlSocketFactory,用于HTTPS连接
if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
}
return connection;
}
在这里把SSlSocketFactory对象设置进了连接,如果要使用HTTPS连接只需要把自己实现的SSlSocketFactory传递进来即可,是不是很简单?
还可以看一下setConnectionParametersForRequest这个方法,该方法用于向设置请求类型并向服务器传递业务数据:
static void setConnectionParametersForRequest(HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST:
// This is the deprecated way that needs to be handled for backwards compatibility.
// If the request's post body is null, then the assumption is that the request is
// GET. Otherwise, it is assumed that the request is a POST.
byte[] postBody = request.getPostBody();
if (postBody != null) {
// Prepare output. There is no need to set Content-Length explicitly,
// since this is handled by HttpURLConnection using the size of the prepared
// output stream.
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getPostBodyContentType());
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(postBody);
out.close();
}
break;
case Method.GET:
// Not necessary to set the request method because connection defaults to GET but
// being explicit here.
connection.setRequestMethod("GET");
break;
case Method.DELETE:
connection.setRequestMethod("DELETE");
break;
case Method.POST:
connection.setRequestMethod("POST");
addBodyIfExists(connection, request);
break;
case Method.PUT:
connection.setRequestMethod("PUT");
addBodyIfExists(connection, request);
break;
case Method.HEAD:
connection.setRequestMethod("HEAD");
break;
case Method.OPTIONS:
connection.setRequestMethod("OPTIONS");
break;
case Method.TRACE:
connection.setRequestMethod("TRACE");
break;
case Method.PATCH:
connection.setRequestMethod("PATCH");
addBodyIfExists(connection, request);
break;
default:
throw new IllegalStateException("Unknown method type.");
}
}
这个方法和HttpClientStack中的一样,根据Request的请求类型,设置HttpUrlConnection的请求类型,如果有请求Body的话就使用OutputStream写出去。
至此我们就把HttpStack家族的类都分析完了,看起来HttpStack家族是用来实现Http请求的,是具体业务实现者,如果我们自己有自定义的Http请求实现,可以继承HttpStack来实现。
Network
我们在Volley源码中看到创建了一个Network对象,是一个BasicNetwork对象,把创建的HttpStack对象传递进去,我们看看Network接口:
public interface Network {
/**
* 执行指定的请求.
* @param request 要被执行的Request
* @return 一个存储了数据和缓存元数据的 {@link NetworkResponse} 对象; 不会为空
* @throws VolleyError on errors
*/
NetworkResponse performRequest(Request<?> request) throws VolleyError;
}
这个接口定义了一个方法performRequest,返回的是一个NetworkResponse对象,看来BasicNetwork一定实现了该方法。那我们去找BasicNetwork聊聊吧。
BasicNetwork
进来也是老规矩,先看成员变量和构造方法:
//指定慢请求的时间阈值
private static int SLOW_REQUEST_THRESHOLD_MS = 3000;
//默认Buffer池大小
private static int DEFAULT_POOL_SIZE = 4096;
//传递进来的HttpStack
protected final HttpStack mHttpStack;
//ByteArrayPool是一个Byte数组List
protected final ByteArrayPool mPool;
/**
* @param httpStack 要使用的HTTP stack
*/
public BasicNetwork(HttpStack httpStack) {
// 如果不传入ByteArrayPool, 那就创建一个小的默认大小的ByteArrayPool
// 这样很好,因为我们不需要太多的内存
this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
}
/**
* @param httpStack 要使用的HTTP stack
* @param pool 一个可以提高复制操作中GC性能的缓冲池
*/
public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
mHttpStack = httpStack;
mPool = pool;
}
成员变量中定义了一个慢请求阈值,看来是判断请求时间,如果超过了这个请求阈值,那么就会做一些特定的操作,有一个HttpStack,这个我们知道,跟随着构造函数传进来的,还有一个ByteArrayPool成员,进去一看原来是封装着两个byte数组的List,看来是用来做缓存池用的,默认的缓存池大小限制为4096,单位是byte。
构造方法就比较清晰了,把两个必要的成员HttpStack和ByteArrayPool实例化完毕就O了。我们比较关注的是对接口中方法的实现:
@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>();
//获取Request的Cache信息
addCacheHeaders(headers, request.getCacheEntry());
//调用HttpStack执行请求,返回的对象是apache的HttpRequest
httpResponse = mHttpStack.performRequest(request, headers);
//获取响应栏和响应码
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
//获取响应头
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// 验证Cache.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
// 响应码:304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
Entry entry = request.getCacheEntry();
if (entry == null) {
//返回304且没有缓存,直接结束请求并返回一个新的NetworkResponse
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, responseHeaders, true, SystemClock.elapsedRealtime() - requestStart);
}
// 有缓存
// HTTP 304 响应并不返回所有的头字段. 我们还需要使用 cache entry的头字段加上返回的头字段.
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
entry.responseHeaders.putAll(responseHeaders);
//数据没有改变,我们只需要返回Cache中的数据和新的响应头
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, entry.responseHeaders, true, SystemClock.elapsedRealtime() - requestStart);
}
// 响应码:301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
// 响应码:302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
// 从响应头中获取资源新的URL
String newUrl = responseHeaders.get("Location");
// 把请求重定向到新的URL
request.setRedirectUrl(newUrl);
}
// 另外有些204之类的响应没有内容. 我们必须检查这类情况.
if (httpResponse.getEntity() != null) {
//获取响应内容
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// 没有内容的话,我们就诚实的返回一个长度为0的byte数组.
responseContents = new byte[0];
}
// 如果请求很慢,则需要打印出来
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
// 响应码:小于200或者大于299,且除去之前安已经判断过的301,302,和204类的响应之外,其余的响应都表示该请求失败
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
// 返回响应内容,封装为NetworkResponse
return new NetworkResponse(statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
} catch (SocketTimeoutException e) {
// 发生超时,准备重试
attemptRetryOnException("socket", request, new TimeoutError());
} catch (ConnectTimeoutException e) {
// 发生超时,准备重试
attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
// 无效的URL,直接抛出异常
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
// 抛出IOException的地方有很多,我们需要区别对待
// 获取状态码
int statusCode;
NetworkResponse networkResponse;
if (httpResponse != null) {
statusCode = httpResponse.getStatusLine().getStatusCode();
} else {
throw new NoConnectionError(e);
}
// 301.302,打印被重定向的URL,其余响应码直接打印进Log
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
} else {
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
}
// 判断响应内容
if (responseContents != null) {
// 封装响应内容
networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
// 响应码:401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
// 响应码:403 (禁止) 服务器拒绝请求。
if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) {
// 尝试重新请求
attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
} else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
// 301.302,重新请求新的URL
attemptRetryOnException("redirect", request, new RedirectError(networkResponse));
} else {
// TODO: 5xx 系列的返回码,客户端无能为力,只能抛异常.
throw new ServerError(networkResponse);
}
} else {
// 响应内容为空
throw new NetworkError(e);
}
}
}
}
方法有点长,不过思路倒是很清晰,我们一部分一部分的来:
- 记录一下请求的时间,这个可能和慢请求有关,待会儿请求结束之后我们再看看到底要干嘛。
- 进入了无限循环,应该是要执行请求了,而且应该是多次执行请求,如果遇到return或者break才会停止这个牛(e)B(xin)的无限循环。
- 初始化一堆容器,httpResponse是apache的响应对象,responseContents用来存储响应内容,responseHeaders用来存储响应头,headers用来存储请求头信息,
- addCacheHeaders这个方法我们有必要在这里分析一下,等会儿会用的到:
- 代码我就不贴了,贴多了你们说我整篇文章净是代码了……,这个方法大概流程就是说在Request中的Cache.Entry(暂称为缓存项目)中查找,如果entry的Etag(String)不为空,则将Etag的放进headers中,键为”If-None-Match”,值就是Etag的值,同样,如果entry的Last-Modified不为空,也放到headers中,键为”If-Modified-Since”,值为Last-Modified的值,最后那个时间转换的操作暂且忽略。
- 那么问题来了,为啥要把请求对象缓存中的这两个变量单单拎出来,存储在map中,而且还各自准备了一个对应的key?其实我们一看到Cache类就知道和缓存有关系了。首先我们应该明白在Http协议中[“Etag”,”If-None-Match”,”Last-Modified”,”If-Modified-Since”]都是头信息,这四个头构成了Http协议的一种缓存机制,称作”Etag&Last-Modified”缓存机制,”Etag”对应的头就是”If-None-Match”,同样”Last-Modified”对应的头为”If-Modified-Since”.
- 再进一步,我们分析一下Etag机制,Etag存储的是访问资源的属性,标识着该资源和上一次对比,是否被修改过,我们可以暂且认为Etag的值为资源的MD5。”If-None-Match”是请求头,而”Etag”是响应头,客户端第一次请求某个资源的时候,该资源的Etag还没有在本地存储,所以我们对资源的第一次Request中If-None-Match应该是null,接着,服务器返回的时候把Etag的值是放到了响应头中返回过来,我们收到该Etag之后存储起来,第二次对相同的资源进行请求的时候,我们就有了该资源的Etag的值(以If-None-Match头形式发送),服务器收到If-None-Match头信息之后,判断我们发过去的If-None-Match和服务器上的Etag是否一致,如果一致就返回304,表示该资源没有被修改,不返回请求内容,我们直接使用上一次缓存的资源即可,如果有修改,就返回新的Etag值和请求内容,如果我们不使用Etag机制的话,每次请求同一个资源都会返回200和请求内容。
- 再说说Last-Modified,If-Modified-Since是请求头,Last-Modified是响应头,两个头存储的都是资源最后一次被修改的时间,和Etag的流程一样,客户端第一次请求资源是没有If-Modified-Since头信息的,服务器则老老实实返回资源内容加上Last-Modified头,客户端收到之后进行缓存,再次请求同一个资源时,则会在请求头上加上If-Modified-Since时间戳,服务器收到If-Modified-Since时间戳之后和对应的资源的Last-Modified时间戳作对比,如果一致就返回304,不一致返回新的Last-Modified时间戳和资源内容。
- 可以看到在Volley实现了Http缓存机制,使得Volley的效率为人称道,如果我们以后要自己实现Http请求的话,这一块儿是很值得我们学习的。
- 我们继续向下分析,下面就开始执行HttpStack中的请求了,请求之后我们如愿以偿的拿到了状态行,状态码,响应头和响应体,接着我们就开始分析这些响应信息了。
- 首先是对我们上面讲过的”Etag&Last-Modified”机制结果的判断,如果服务器返回了304,则认为请求资源已经被缓存,我们直接使用本地的缓存资源即可,需要注意的是在这里做了一个判断,如果本次Request的CacheEntry为空,则表示本地没有对应的缓存子资源,但是服务器又返回了304,则说明这个资源是没有实体数据的,所以NetworkResponse的data字段为null。还有就是304头字段不完整的问题,我们可以去看看它给出的文档,在这里不做多讲。
- 其次判断的是返回301和302的情况,这种情况叫做资源移动,分为301永久移动和302临时移动,但是不管它怎么移动,都会在响应头里边的Location中给出资源移动后的位置,我们可以根据Location来重新定位资源,
- 然后我们把响应体提取出来,这里做了个判断,像204,205这种是没有响应体的,所以给了responseContents一个空数组,囧……
- 接着打印了一下这次请求的用时,也就是说如果在Debug模式下,或者是本次请求用时超过了静态变量SLOW_REQUEST_THRESHOLD_MS的阈值,就打印出Log反馈出本次请求的耗时情况
- 接下来,因为除了200-299是请求成功的返回码,其余的都是发生异常的,所以干脆都抛出IO异常放在Catch块中一起处理了,包括之前已经判断过的301,302
- 对于超时异常,不论是socket超时还是连接超时,都调用attemptRetryOnException方法尝试重新连接,由于我们是运行在一个while(true)循环中,并且attemptRetryOnException中进行了超时重连次数判断,所以重连一定次数还是连不上才会抛出真正的超时异常。对于MalformedURLException,因为URL本身就不正确,所以Volley狠心的抛出了一个RunTimeException来警告你
- 接下类就是处理IO异常了,IO异常有好几个地方抛出,一个是在HttpStack的请求调用的地方,一个是在entityToBytes把请求实体转换为byte数组的时候,最后一个是我们自己抛出的,当返回码不是200开头的时候:
- 先从HttpResponse中取出响应码,如果连HttpResponse都是null的时候那就是彻底的没连上服务器
- 接着打印了一下响应码,如果是301,302的时候顺便还打印了一下原始URL和新的URL
- 然后是响应体判断,没有响应体直接抛异常,否则的话进行重连,具体为啥要在301,302,以及401,403的时候进行重连,其他响应码不重连,可以看看Http协议中有关返回码的解释,在这里不多赘述。
至此整个网络请求就搞定,我们终于解脱了,但是别松懈,还有一个问题,我们在整个while(true)中看到所有重连都是调用了attemptRetryOnException方法,具体是怎么重连的你不想看看嘛?这两个异常和四个返回码的漏网之鱼岂能放过?
/**
* 为再次请求准备一个Request. 如果没有重试次数的话,就抛出超时异常.
* @param request The request to use.
*/
private static void attemptRetryOnException(String logPrefix, Request<?> request, VolleyError exception) throws VolleyError {
// 从Request中取出重连策略
RetryPolicy retryPolicy = request.getRetryPolicy();
// 获取我们上一次请求的用时
int oldTimeout = request.getTimeoutMs();
try {
// 进行重连
retryPolicy.retry(exception);
} catch (VolleyError e) {
// 连重连也失败的话,那就彻底没机会咯
request.addMarker(String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
throw e;
}
request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
}
看来是调用了Request中的RetryPolicy接口方法来进行重连的,由于不同的Request实现最终有不同的重连策略,这个以后再分析,最后不管有没有重连成功,都会addMarker标注一下该请求,记录一下这个Request有重试的”前科”.
小结
到这里我们先总结一下,我们本篇文章主要分析了HttpStack家族和Network家族,这两个接口都用于执行网络请求连接,只不过HttpStack是具体实现连接的工作类,而Network则用于调度和检查HttpStack的工作,如果打个比方,HttpStack就相当于我们底层专注于实现需求的苦B程序员,而Network则相当于小组长或者是PM,HttpStack想各种办法(HttpClient || HttpUrlConnection)来实现用户的需求(Http请求),而Network则负责给程序员HttpStack指定工作任务,还要监督你的工作质量,如果HttpStack的一个项目用时超过了Network指定时间(慢请求),那这个PM则会对你bulabula说你一通(打印慢请求Log),如果项目延期(timeout超时)或者结果不符合用户需求(301,302,401,403),那么这个PM不仅会让你重头再来(attemptRetryOnException),而且会在全员大会上给你记过(addMarker),让大家都知道你有”前科”,汗…….