Google网络请求框架Volley源码浅析(一)

很久以来一直想要分析一个开源框架来玩玩,最近看到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下载,没梯子网上一搜一大把。
使用流程明白了,分析流程也明白了:

  1. 分析Volley这个对象以及其中的方法,尤其是newRequestQueue(Context context)
  2. 分析各种Request的接口,实现类,作用
  3. 分析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这个类的作用原来这么简单:

  1. 定义缓存目录”volley”
  2. 使用方法重载,创建RequestQueue,可以设定要使用的HttpStack,磁盘缓存大小(单位byte)

HttpStack是啥?不知道!磁盘缓存大小?没设定过!所以我们最常用的还是第四个方法,不用传HttpStack也不用设定缓存大小,全默认的。

方法重载最后都会调用到第一个,这个方法这么长,一看就是干正事儿的:

  1. 创建缓存目录
  2. 创建一个userAgent字符串,用于创建HttpClientStack
  3. 如果用户没给HttpStack参数的话,就根据当前的APILevel创建不同的HttpStack,API9以前的创建HttpClientStack,API9以及以后的创建HurlStack
  4. 创建一个Network的实例:BasicNetwork对象,把刚才创建的HttpStack传进去
  5. 正式创建RequestQueue对象,它需要Network实例和一个DiskBasedCache对象,我们可以看到这个DiskBasedCache的构造也是方法重载,根据用户是否指定缓存大小调用不同的构造方法
  6. 调用RequestQueue的start方法开启消息队列

看来我们分析Volley的任务有了继续向前的目标:

  1. 分析HttpStack以及他的两个子类
  2. 这个Network接口和其子类BasicNetwork有必要看一下是干嘛的
  3. 可以看看这个DiskBasedCache类中是干了什么
  4. 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),让大家都知道你有”前科”,汗…….

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值