Android APP开发实战:从规划到上线全程详解
上QQ阅读APP看书,第一时间看更新

6.5 Volley网络库简介

6.5.1 Volley网络库

Volley库是Google官方提供的开源网络库,在Android系统中也使用了这个网络库。

Volley库对网络功能进行了封装,默认根据Android系统的不同版本使用不同的HTTP协议栈。在Android 2.3及以上版本使用HurlStack协议栈,在Android 2.3以下版本使用HttpClientStack协议栈。使用者也可以自己设置其中使用的HTTP协议栈,使用比较灵活。

Volley库支持字符串、图片和JSON格式数据的处理,但因为在解析服务器端的响应消息时,Volley库是把响应消息存储在内存中,所以Volley库不适合大数据量的网络请求,如下载大文件等。

在Volley库的Volley类中,提供了设置请求HTTP协议栈的方法。

        public class Volley {

            /** Default on-disk cache directory. */
            private static final String DEFAULT_CACHE_DIR = "volley";

            /**
            * Creates a default instance of the worker pool and calls {@link
              RequestQueue#start()} on it.
              *
              * @param context A {@link Context} to use for creating the cache dir.
              * @param stack An {@link HttpStack} to use for the network, or null for
              default.
            * @return A started {@link RequestQueue} instance.
            */
            public static RequestQueue newRequestQueue(Context context, HttpStack stack){
                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();
                      } 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 = new RequestQueue(new DiskBasedCache(cacheDir), network);
                queue.start();

                return queue;
            }

            /**
            * Creates a default instance of the worker pool and calls {@link RequestQueue
              #start()} on it.
            *
            * @param context A {@link Context} to use for creating the cache dir.
            * @return A started {@link RequestQueue} instance.
            */
            public static RequestQueue newRequestQueue(Context context) {
                return newRequestQueue(context, null);
            }
      }

在Volley库的Request类中,定义了Volley库支持的HTTP方法。

        public abstract class Request<T> implements Comparable<Request<T>> {
            …
            /**
              * Supported request methods.
              */
            public interface Method {
                int DEPRECATED_GET_OR_POST = -1;
                int GET = 0;
                int POST = 1;
                int PUT = 2;
                int DELETE = 3;
                int HEAD = 4;
                int OPTIONS = 5;
                int TRACE = 6;
                int PATCH = 7;
            }
        …
            public Request(int method, String url, Response.ErrorListener listener) {
                mMethod = method;
                mUrl = url;
                mIdentifier = createIdentifier(method, url);
                mErrorListener = listener;
                setRetryPolicy(new DefaultRetryPolicy());
                mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url);
            }
        …
        }

在Volley库中,HttpHeaderParser类用于处理从服务器获得的头字段数据。

        public class HttpHeaderParser {

                public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
                  long now = System.currentTimeMillis();

                  Map<String, String> headers = response.headers;

                  long serverDate = 0;
                  long lastModified = 0;
                  long serverExpires = 0;
                  long softExpire = 0;
                  long finalExpire = 0;
                  long maxAge = 0;
                  long staleWhileRevalidate = 0;
                  boolean hasCacheControl = false;
                  boolean mustRevalidate = false;

                  String serverEtag = null;
                  String headerValue;

                  headerValue = headers.get("Date");
                  if (headerValue ! = null) {
                        serverDate = parseDateAsEpoch(headerValue);
                  }

                  headerValue = headers.get("Cache-Control");
                  if (headerValue ! = null) {
                        hasCacheControl = true;
                        String[] tokens = headerValue.split(", ");
                        for (int i = 0; i < tokens.length; i++) {
                              String token = tokens[i].trim();
                              if (token.equals("no-cache") || token.equals("no-store")) {
                                  return null;
                              } else if (token.startsWith("max-age=")) {
                                  try {
                                      maxAge = Long.parseLong(token.substring(8));
                                  } catch (Exception e) {
                                  }
                              } else if (token.startsWith("stale-while-revalidate=")) {
                                  try {
                                      staleWhileRevalidate = Long.parseLong(token.substring(23));
                                  } catch (Exception e) {
                                  }
                            } else if(token.equals("must-revalidate")||token.equals ("proxy-revalidate")) {
                                  mustRevalidate = true;
                              }
                        }
                }

                headerValue = headers.get("Expires");
                if (headerValue ! = null) {
                      serverExpires = parseDateAsEpoch(headerValue);
                }

                headerValue = headers.get("Last-Modified");
                if (headerValue ! = null) {
                      lastModified = parseDateAsEpoch(headerValue);
                }
                serverEtag = headers.get("ETag");

                // Cache-Control takes precedence over an Expires header, even if both exist and Expires
                // is more restrictive.
                if (hasCacheControl) {
                    softExpire = now + maxAge * 1000;
                    finalExpire = mustRevalidate
                            ? softExpire
                            : softExpire + staleWhileRevalidate * 1000;
                } else if (serverDate > 0 && serverExpires >= serverDate) {
                    // Default semantic for Expire header in HTTP specification is softExpire.
                    softExpire = now + (serverExpires - serverDate);
                    finalExpire = softExpire;
                }

                Cache.Entry entry = new Cache.Entry();
                entry.data = response.data;
                entry.etag = serverEtag;
                entry.softTtl = softExpire;
                entry.ttl = finalExpire;
                entry.serverDate = serverDate;
                entry.lastModified = lastModified;
                entry.responseHeaders = headers;

                return entry;
            }
        …
        }

目前大多数APP与服务器间传输数据时,都是使用JSON格式,Volley库中提供了使用JSON格式传递数据的类。

        public abstract class JsonRequest<T> extends Request<T> {
            /** Default charset for JSON request. */
            protected static final String PROTOCOL_CHARSET = "utf-8";

            /** Content type for request. */
            private static final String PROTOCOL_CONTENT_TYPE =
                String.format("application/json; charset=%s", PROTOCOL_CHARSET);

            private final Listener<T> mListener;
            private final String mRequestBody;
            /**
            * Deprecated constructor for a JsonRequest which defaults to GET unless {@link
            #getPostBody()}
              * or {@link #getPostParams()} is overridden (which defaults to POST).
              *
              * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
              */
            public JsonRequest(String url, String requestBody, Listener<T> listener,
                    ErrorListener errorListener) {
                this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
            }

            public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
                    ErrorListener errorListener) {
                super(method, url, errorListener);
                mListener = listener;
                mRequestBody = requestBody;
            }

            @Override
            protected void deliverResponse(T response) {
                mListener.onResponse(response);
            }

            @Override
            abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

            /**
              * @deprecated Use {@link #getBodyContentType()}.
              */
            @Override
            public String getPostBodyContentType() {
                return getBodyContentType();
            }

            /**
              * @deprecated Use {@link #getBody()}.
              */
            @Override
            public byte[] getPostBody() {
                return getBody();
            }

            @Override
            public String getBodyContentType() {
                return PROTOCOL_CONTENT_TYPE;
            }

            @Override
            public byte[] getBody() {
                try {
                    return mRequestBody == null ? null : mRequestBody.getBytes (PROTOCOL_
                    CHARSET);
                } catch (UnsupportedEncodingException uee) {
                    VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
                            mRequestBody, PROTOCOL_CHARSET);
                    return null;
                }
            }
        }

        public class JsonObjectRequest extends JsonRequest<JSONObject> {

            /**
              * Creates a new request.
              * @param method the HTTP method to use
              * @param url URL to fetch the JSON from
            * @param jsonRequest A {@link JSONObject} to post with the request. Null is
            allowed and
              *   indicates no parameters will be posted along with request.
              * @param listener Listener to receive the JSON response
              * @param errorListener Error listener, or null to ignore errors.
              */
            public JsonObjectRequest(int method, String url, JSONObject jsonRequest,
                    Listener<JSONObject> listener, ErrorListener errorListener) {
                super(method, url, (jsonRequest == null) ? null : jsonRequest. toString(), listener,
                            errorListener);
            }

            /**
            * Constructor which defaults to <code>GET</code> if <code>jsonRequest </code> is
              * <code>null</code>, <code>POST</code> otherwise.
              *
              * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener)
              */
            public JsonObjectRequest(String url, JSONObject jsonRequest, Listener <JSONObject>
            listener, ErrorListener errorListener) {
                this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest,
                        listener, errorListener);
            }
            @Override
            protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
                try {
                    String jsonString = new String(response.data,
                            HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
                    return Response.success(new JSONObject(jsonString),
                            HttpHeaderParser.parseCacheHeaders(response));
                } catch (UnsupportedEncodingException e) {
                    return Response.error(new ParseError(e));
                } catch (JSONException je) {
                    return Response.error(new ParseError(je));
                }
            }
        }

6.5.2 Volley网络库的使用

为了使用方便,可以对Volley库提供的类和方法进行二次封装,代码如下所示。

        public class JsonRequest extends JsonObjectRequest {
            String cookieString

            public JsonRequest(String url, JSONObject jsonRequest,
                                    Response.Listener<JSONObject> listener, Response.ErrorListener
                                    errorListener) {
                super(url, jsonRequest, listener, errorListener);
            }

            //自定义向服务器发送的头字段数据
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                HashMap<String, String> headers = new HashMap<String, String>();
                //标示是Android APP向服务器发起请求
                headers.put("AppKey", "Android");

                //设置User-Agent的内容为APP的包名和版本信息,标示是哪个APP向服务器发起请求
                String packageName = context.getPackageName();
                PackageInfo info =
                context.getPackageManager().getPackageInfo(packageName, 0);
                userAgent = packageName + "/" + info.versionCode;
                headers.put("User-Agent",  userAgent );

                //把服务器返回给APPCookie信息,添加到APP发给服务器的头信息中,标示访问服务器的客户身份
                  headers.put("Cookie", cookieString);
                return headers;
            }

            @Override
            protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
                try {
                    String jsonString = new String(response.data,
                                  HttpHeaderParser.parseCharset(response.headers,
                                    PROTOCOL_CHARSET));

                    //获取服务器返回给APPCookie信息
                    Map<String, String> headers = response.headers;
                    cookieString = headers.get("Set-Cookie");

                    //解析服务器返回的cookie信息,从中获取SessionID
                    String sessionId = parseVooleyCookie(cookies);

                    return Response.success(new JSONObject(jsonString),
                            HttpHeaderParser.parseCacheHeaders(response));
                } catch (UnsupportedEncodingException e) {
                    return Response.error(new ParseError(e));
                } catch (JSONException je) {
                    return Response.error(new ParseError(je));
                }
            }

            //解析cookie数据
            public String parseVooleyCookie(String cookie) {
                StringBuilder sb = new StringBuilder();
                String[] cookievalues = cookie.split("; ");
                for (int j = 0; j < cookievalues.length; j++) {
                    String[] keyPair = cookievalues[j].split("/");
                    for (int i = 0; i < keyPair.length; i++) {
                        if (keyPair[0].contains("session_id")) {
                            sb.append(keyPair[0]);
                            sb.append("; ");
                            break;
                        }
                    }
                }
                return sb.toString();
            }
        }

使用OkHttp3作为Volley库的HTTP协议栈,需要实现HttpStack里定义的接口方法。协议栈的具体代码如下:

    public class OkHttp3Stack implements HttpStack {
        private final OkHttpClient mClient;

        public OkHttp3Stack(OkHttpClient client) {
            this.mClient = client;
        }

        @Override
        public HttpResponse performRequest(Request<? > request, Map<String, String>
        additionalHeaders)
                throws IOException, AuthFailureError {
            int timeoutMs = request.getTimeoutMs();
            OkHttpClient client = mClient.newBuilder()
                    .readTimeout(timeoutMs, TimeUnit.MILLISECONDS)
                    .connectTimeout(timeoutMs, TimeUnit.MILLISECONDS)
                    .writeTimeout(timeoutMs, TimeUnit.MILLISECONDS)
                    .build();

            okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder();
            Map<String, String> headers = request.getHeaders();
            for (final String name : headers.keySet()) {
                okHttpRequestBuilder.addHeader(name, headers.get(name));
            }

            for (final String name : additionalHeaders.keySet()) {
                okHttpRequestBuilder.addHeader(name, additionalHeaders.get(name));
            }

            setConnectionParametersForRequest(okHttpRequestBuilder, request);

            okhttp3.Request okhttp3Request = okHttpRequestBuilder.url(request.getUrl()).build();
            Response okHttpResponse = client.newCall(okhttp3Request).execute();

            StatusLine responseStatus = new BasicStatusLine
                    (
                              parseProtocol(okHttpResponse.protocol()),
                              okHttpResponse.code(),
                              okHttpResponse.message()
                    );

            BasicHttpResponse response = new BasicHttpResponse(responseStatus);
            response.setEntity(entityFromOkHttpResponse(okHttpResponse));

            Headers responseHeaders = okHttpResponse.headers();
            for (int i = 0, len = responseHeaders.size(); i < len; i++) {
                final String name = responseHeaders.name(i), value = responseHeaders.value(i);
                if (name ! = null) {
                    response.addHeader(new BasicHeader(name, value));
                }
            }
            return response;
        }

        private static HttpEntity entityFromOkHttpResponse(Response response) throws IOException {
            BasicHttpEntity entity = new BasicHttpEntity();
            ResponseBody body = response.body();

            entity.setContent(body.byteStream());
            entity.setContentLength(body.contentLength());
            entity.setContentEncoding(response.header("Content-Encoding"));

            if (body.contentType() ! = null) {
                entity.setContentType(body.contentType().type());
            }
            return entity;
        }

        @SuppressWarnings("deprecation")
        private static void setConnectionParametersForRequest
                (okhttp3.Request.Builder builder, Request<? > request)
                throws IOException, AuthFailureError {
            switch (request.getMethod()) {
                case Request.Method.DEPRECATED_GET_OR_POST:
                    byte[] postBody = request.getPostBody();
                    if (postBody ! = null) {
                        builder.post(RequestBody.create
                                (MediaType.parse(request.getPostBodyContentType()), postBody));
                    }
                    break;

                case Request.Method.GET:
                    builder.get();
                    break;

                case Request.Method.DELETE:
                    builder.delete();
                    break;

                case Request.Method.POST:
                    builder.post(createRequestBody(request));
                    break;

                case Request.Method.PUT:
                    builder.put(createRequestBody(request));
                    break;

                case Request.Method.HEAD:
                    builder.head();
                    break;

                case Request.Method.OPTIONS:
                    builder.method("OPTIONS", null);
                    break;

                case Request.Method.TRACE:
                    builder.method("TRACE", null);
                    break;

                case Request.Method.PATCH:
                    builder.patch(createRequestBody(request));
                    break;

                default:
                    throw new IllegalStateException("Unknown method type.");
            }
        }

        private static RequestBody createRequestBody(Request request) throws AuthFailureError {
            final byte[] body = request.getBody();
            if (body == null) return null;

            return RequestBody.create(MediaType.parse(request.getBodyContentType()), body);
        }

        private static ProtocolVersion parseProtocol(final Protocol protocol) {
            switch (protocol) {
                case HTTP_1_0:
                    return new ProtocolVersion("HTTP", 1, 0);
                case HTTP_1_1:
                    return new ProtocolVersion("HTTP", 1, 1);
                case SPDY_3:
                    return new ProtocolVersion("SPDY", 3, 1);
                case HTTP_2:
                    return new ProtocolVersion("HTTP", 2, 0);
            }

            throw new IllegalAccessError("Unkwown protocol");
        }
    }

还需增加一个类,提供其他模块调用Volley库的接口方法。

    public class NetworkManager {
        private static NetworkManager mInstance;
        private RequestQueue mRequestQueue;
        private static Context mCtx;

        private NetworkManager(Context context) {
            mCtx = context;
            mRequestQueue = getRequestQueue();
        }
        //使用单列模式创建类的实例
        public static synchronized NetworkManager getInstance(Context context) {
            if (mInstance == null) {
                mInstance = new NetworkManager(context);
            }
            return mInstance;
        }

        public RequestQueue getRequestQueue() {
            if (mRequestQueue == null) {
                //设置使用OkHttp3作为HTTP协议栈
                mRequestQueue = Volley.newRequestQueue(mCtx, new
    OkHttp3Stack(new OkHttpClient()));
            }
            return mRequestQueue;
        }

        private <T> Request<T> add(Request<T> request) {
            return mRequestQueue.add(request); //添加请求到队列
        }

        /**
          * 创建JSON格式的请求数据
          *
          */
        public void JsonRequest(Object tag, String url, JSONObject jsonObject,
        Response.Listener<JSONObject> listener,
                                      Response.ErrorListener errorListener) {
            JsonRequest jsonRequest;
            jsonRequest = new JsonRequest(url, jsonObject, listener, errorListener);
            jsonRequest.setTag(tag);
            add(jsonRequest);

        }

        /**
          * 取消请求
          *
          */
        public void cancel(Object tag) {
              mRequestQueue.cancelAll(tag);
        }
    }

函数调用方式如下:

        NetworkManager.getInstance(EamApplication.getContext()).JsonRequest(TAG, baseUrl +
        url, jsonObject,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject jsonObject) {

                        Log.v(TAG, "response json对象: " + jsonObject.toString());
                    }
                }, new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.e(TAG, error.getMessage(), error);
                    }
                });

在创建网络对象实例时,使用Application级别的Context,保证实例的生命周期与APP的生命周期一样。