在2013年Google I/O大会上,Android开发团队公布了一个新的网络通信框架:Volley。它适合处理通信频繁的网络操作,但对于每一次通信的数据量则有较为苛刻的限制。本文将介绍该通信框架的用法(包括使用现成和自定义的Request),并从源码的角度探究其工作机制。
目前,Android系统中用于实现HTTP通信的方式主要有HttpURLConnection和HttpClient两个类[1],而封装了它们的框架主要有AsyncHttpClient和Universal-Image-Loader等。Volley库将HTTP通信进一步简单化,它的用法如下:
首先,由于HTTP通信势必要访问网络,我们需要在Android项目的Manifest.xml文件中添加访问网络的许可[2]:
<uses-permission android:name="android.permission.INTERNET" />
接下来,要先建立一个请求队列对象(RequestQueue):
RequestQueue mQueue = Volley.newRequestQueue(context);
这里的RequestQueue可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求(具体算法将在接下来的部分中有进一步的讲解)。由于RequestQueue能够并发处理请求,我们只需要在每一个进行网络通信的Activity中建立一个RequestQueue对象即可。
Android系统的网络通信能够处理多种媒体形式[3],而RequestQueue所能处理的具体请求也可根据数据类型分为StringRequest、JSONRequest、ImageRequest和自定义的Request等几种。接下来,本文将逐一讲解上述网络请求类的用法:
1:StringRequest
StringRequest可以向服务器端发出请求,并将数据以String的形式返回。假设我们需要创建一个请求,向百度请求其首页的网页HTML源代码,则代码如下所示:
StringRequest stringRequest = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
contentText.setText(response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG", error.getMessage(), error);
}
});
在这段代码中,我们创建了一个新的StringRequest对象。该对象的构造方法有三个参数,分别是目标服务器地址、响应成功时的回调和响应失败时的回调。其中目标服务器地址是百度首页;响应成功时的回调方法将contentText的内容设为该Request返回的文字内容;响应失败时的回调方法打印具体的错误信息。
在创建完StringRequest对象后,我们需要将其添加到先前创建的RequestQueue对象中,以发送该请求。这部分的代码很简单,只有一行:
mQueue.add(stringRequest);
接下来,只需运行程序,即可在屏幕上观察到服务器端返回的数据:
HTTP协议定义了多种请求类型[4],通常我们关心的只有GET请求和POST请求。在以上的例子中,我们发出的是一个GET请求。而如果要发出一个POST请求,我们就需要调用另外的构造方法。在该方法中,我们可以指定请求类型为POST,并且重写Request(各种具体Request共同的父类)中的getParams()方法,在其中输入提交给服务器端的参数。代码如下所示:
StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener) {
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> map = new HashMap<String, String>();
map.put("params1", "value1");
map.put("params2", "value2");
return map;
}
};
2.JSONRequest
JSON是一种轻量级的数据交换格式,它基于JavaScript的一个子集,但采用了完全独立于语言的文本格式。这些特性使JSON成为理想的数据交换语言,易于人阅读和编写,同时也易于机器解析和生成[5]。而顾名思义,JsonRequest就是用于和服务器端交换JSON格式的数据的。JsonRequest有两个子类(JsonObjectRequest和JsonArrayRequest),分别用来处理单个的JSON对象和JSON的数组。
JsonObjectRequest的用法与上述的StringRequest类似,创建一个对象,在其构造方法中指定服务器URL地址和响应成功和失败时的回调,再把它加入请求队列中即可。代码如下:
RequestQueue mRequestQueue = Volley.newRequestQueue(this);
String url = "http://pipes.yahooapis.com/pipes/pipe.run?_id=giWz8Vc33BG6rQEQo_NLYQ&_render=json";
JsonObjectRequest jr = new JsonObjectRequest(Request.Method.GET,url,null,new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try{
JSONObject valueobj = response.getJSONObject("value");
JSONArray jsonArray = valueobj.getJSONArray("items");
for(int i=0;i<jsonArray.length();i++){
JSONObject item = jsonArray.getJSONObject(i);
String description = item.getString("description");
adapter.add(description);
}
}catch (JSONException e) {
throw new RuntimeException(e);
}
}
},new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
error.printStackTrace();
}
});
mRequestQueue.add(jr);
在该例中,我们使用了一个雅虎提供的查询新闻的接口,从中获取了JSON格式的当天的新闻。获取新闻之后,我们在成功响应的回调方法中打印了返回的JSON数据,并取出了其中的一部分放在adapter中,从而实现将新闻内容显示在屏幕上的效果。
3.ImageRequest
ImageRequest可用于从服务器端获取图片。它作为Request的一个子类,也具有和上述Request具体类型相类似的使用方法。在创建RequestQueue之后,即可调用其构造方法。
mImageView= (ImageView) findViewById(R.id.webImage);
newRequestQueue = Volley.newRequestQueue(ImageRequestActivity.this);
ImageRequest p_w_picpathRequest = new ImageRequest("http://imgt6.bdstatic.com/it/u=2,887966933&fm=19&gp=0.jpg",
new Response.Listener<Bitmap>()
{
@Override
public void onResponse(Bitmap response)
{
mImageView.setImageBitmap(response);
}
}, 0, 0, Bitmap.Config.RGB_565, null);
newRequestQueue.add(p_w_picpathRequest);
可以看到,ImageRequest的构造函数接收六个参数,这比StringRequest和JsonRequest稍多一些:
第一个参数就是图片的URL地址,这和其它的Request中的URL参数作用相同;
第二个参数是图片请求成功的回调,这里我们将服务器返回的Bitmap设置到mImageView中;
第三和第四个参数分别用于指定允许图片最大的宽度和高度;
第五个参数用于指定图片的颜色属性;
第六个参数是图片请求失败的回调,这里我们什么都不做。
最后,我们同样需要将这个请求输入到请求队列中。具体代码和其它类型的Request是相同的。运行上述代码后,我们即可看到我们的app从网络上取得了如下的图片,并把它显示在mImageView的位置:
4.自定义Request
除上述各类型以外,Volley还支持自定义类型的Request。接下来,本文将以软件学院学生服务平台中的代码为例,说明自定义Request的用法。
在服务平台的“校园活动”页面中,用户可以看到最新的校园活动信息。为了从服务器端或获取相应信息,我们就需要使用自定义的Request。代码如下所示:
CampusEventResponse activityResponse = new CampusEventResponse(getActivity(),
!isLoadMore) {
@Override
public void onResponse(JSONObject result) {
super.onResponse(result);
Lgr.i(result.toString());
Message msg = mHandler.obtainMessage();
try {
msg.what = result.getInt("status");
if (isLoadMore) {
isMoreData = result.getJSONArray("body").length() == 0 ? false : true;
}
} catch (JSONException e) {
e.printStackTrace();
}
mHandler.sendMessage(msg);
}
@Override
public Object onErrorStatus(CSTStatusInfo statusInfo) {
return super.onErrorStatus(statusInfo);
}
@Override
public void onErrorResponse(VolleyError error) {
super.onErrorResponse(error);
}
};
EventRequest eventRequest = new EventRequest(CSTRequest.Method.GET,
mEventCategory.getSubUrl(), null,
activityResponse).setPage(mCurrentPage).setPageSize(DEFAULT_PAGE_SIZE);
mEngine.requestJson(eventRequest);
其中,CampusEventResponse是Response的一个派生类。在服务器端返回数据后,其中的onResponse方法将会得到执行。而本例中生成的CampusEventResponse对象,则被作为一个参数,输入EventRequest的构造方法中,起到监听器的作用。
EventRequest就是一种自定义的Request,它的代码如下所示:
public class EventRequest extends CSTJsonRequest {
private String subUrl;
private int page;
private int pageSize;
private String keywords;
private boolean hasParams = false;
public EventRequest(int method, String subUrl,
Map<String, String> params,
CSTResponse<JSONObject> response) {
super(method, subUrl, params, response);
this.subUrl = subUrl;
}
@Override
public String getUrl() {
if (hasParams) {
StringBuilder sb = new StringBuilder();
sb.append("?");
try {
if (page > 0) {
sb.append("page=").append(URLEncoder.encode("" + page, "UTF-8")).append("&");
}
if (pageSize > 0) {
sb.append("pageSize=").append(URLEncoder.encode("" + pageSize, "UTF-8"))
.append("&");
}
if (keywords != null) {
sb.append("keywords=").append(URLEncoder.encode(keywords, "UTF-8")).append("&");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
sb.deleteCharAt(sb.length() - 1);
return super.getUrl() + sb.toString();
}
return super.getUrl();
}
public EventRequest setPage(int page) {
this.page = page;
hasParams = true;
return this;
}
public EventRequest setPageSize(int pageSize) {
this.pageSize = pageSize;
hasParams = true;
return this;
}
public EventRequest setKeywords(String keywords) {
this.keywords = keywords;
hasParams = true;
return this;
}
}
我们可以看到,它继承了CSTJsonRequest类(同样是一个自定义Request类,接下来会有讲解)。EventRequest除了构造方法之外,还重写了Request类中的getURL方法。该方法可以根据Page、PageSize和keywords三个变量构造出所需的URL。此外,该类中还有相应的方法可用于设置上述三个变量。
CSTJsonRequest是EventRequest的父类,它的代码如下所示:
public class CSTJsonRequest extends CSTRequest<JSONObject> {
private static final String BASE_URL = "http://www.cst.zju.edu.cn/isst";
public CSTJsonRequest(int method, String subUrl, Map<String, String> params,
CSTResponse<JSONObject> response) {
super(method, BASE_URL + subUrl, params, response);
}
@Override
protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
try {
String jsonString = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
CSTHttpUtil.refreshCookies(BASE_URL, response.headers);
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));
}
}
}
可以看到,该类继承了CSTRequest类,并在构造方法中调用了其父类的构造方法。
另外,它实现了parseNetworkResponse方法,用于解析从服务器端返回的JSON数据。若成功响应,则返回Response.success方法,并将解析出的包含JSON内容的String作为参数输入其中。如果解析不成功,则执行Response.error方法,并将抛出的异常对象作为参数。
综上所述,为了从服务器端请求相应数据,需要建立一个Response派生类对象,并在其onResponse中指定成功返回时执行的内容。然后将该对象作为监听器,传入自定义Request对象的构造方法中,最后将Request对象加入请求队列。其中自定义Request的父类(同样也是Request的派生类)中重写了parseNetworkResponse方法,将服务器返回的数据转化为一个String。然后Response.success方法被调用,该String被重新转化为一个JSON对象并向上传递。
4.Volley框架源码解析
Volley的官方文档中附有一张Volley的工作流程图,如下图所示。
从这张工作流程图中我们可以看到:在一个请求被加入到缓存队列中以后,它将被CacheDispatcher分配到一个合适的去向。如果在cache中已经存有该请求所对应的数据,那么就通过cache线程从cache中读取数据,将其解析后返回主线程;如果在cache中未能找到相应数据,则启动network线程,将其通过NetworkDispatcher发送到网络,从服务器端取回数据,写到cache中,并将其解析后返回给主线程。接下来,本文将对Volley请求的源码作一初步分析,并在此过程中深入说明上图所示的工作机制。
在使用Volley时,如先前的例子所示,我们总是需要先建立一个RequestQueue。在上文中,我们使用的代码是
Volley.newRequestQueue(context);
该方法的代码如下所示:
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
可以看到:该方法内只有一行代码,它调用了newRequestQueue(),并传入了context和null作为参数。而newRequestQueue()方法的源代码如下:
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 {
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
可以看到,该方法所创建的stack取决于用户的系统版本:如果大于等于9,则这里会在第12行创建一个新的HurlStack;反之,则会将一个HttpClientStack对象赋给stack。接下来,在用stack作为参数创建一个Network对象(网络线程其实是调用 Network对象去实现跟网络进行沟通的)之后,就可以调用RequestQueue的构造方法了。在该方法中,cache文件和Network对象被作为参数传递进去。最后,调用RequestQueue的start()方法,即可启动该请求队列。
其中,HurlStack和HttpClientStack是网络访问的最低层实现。由于android的网络访问在2.2以前是用的阿帕奇的网络访问框架,2.2以后用的是HttpConnectUrl,volley兼容2.2以下的android版本,所以它需要针对版本来提供不同的实现。HurlStack对应的是HttpConnectUrl,而HttpClientStack对应的是阿帕奇的网络访问框架。
其中的Network是一个接口,这里具体的实现是BasicNetwork,它的主要功能就是利用performRequest方法处理网络通信过程中的具体细节。该方法的代码如下所示:
public class BasicNetwork implements Network {
……
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
Map<String, String> responseHeaders = new HashMap<String, String>();
try {
// Gather headers.
Map<String, String> headers = new HashMap<String, String>();
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);
StatusLine statusLine = httpResponse.getStatusLine();
int statusCode = statusLine.getStatusCode();
responseHeaders = convertHeaders(httpResponse.getAllHeaders());
// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry() == null ? null : request.getCacheEntry().data,
responseHeaders, true);
}
// Some responses such as 204s do not have content. We must check.
if (httpResponse.getEntity() != null) {
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusLine);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
} catch (Exception e) {
……
}
}
}
}
其中的第14行调用了performRequest()方法,而这里的HttpStack就是在一开始调用newRequestQueue()方法时创建的实例,取决于系统版本,它既可能是HurlStack,也可能是一个HttpClientStack。在得到数据之后,服务器返回的数据会被组装成一个NetworkResponse对象,并作为该方法的返回值。
而RequestQueue的start()方法内部的代码如下:
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
在本方法中,上述代码创建了一个CacheDispatcher实例和若干个(默认情况下为四个)networkDispatcher实例,并调用了它们的start()方法。其中,CacheDispatcher和networkDispatcher都是Thread的子类,它们分别是用于处理缓存和网络请求的线程。
接下来,我们在构建自己的Request之后,就需要将它们添加到网络请求队列中了。RequestQueue的add()方法在本文中已经出现多次。它的内容如下:
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
add方法有以下几个步骤:
1:判断当前的Request是否使用缓存。如不使用,则将请求加入mNetworkQueue并直接返回;如使用,则判断之前是否有执行相同的请求且还没有返回结果。
2:如果上一步最后的判断是true,将此请求加入mWaitingRequests队列,不再重复请求,在上一个请求返回时直接发送结果。
3:如果第1步最后的判断是false,将请求加入缓存队列mCacheQueue,并将其CacheKey加入mWaitingRequests中。
可见,add方法并没有执行任何实际的请求操作。实际的操作是由CacheDispatcher和NetworkDispatcher这两个类完成的。其中CacheDispatcher是Thread的子类,具体代码如下:
public class CacheDispatcher extends Thread {
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
}
上述代码主要完成以下工作:
1:从mCacheQueue取出请求。
2:检查请求所对应的CacheKey在缓存中是否存在
3:如果不存在,就加到mNetworkQueue中,并继续取下一个请求。如果存在,判断是否过期。
4:如果过期,就加入网络队列mNetworkQueue中,并继续取下一个请求。如果没过期,判断是否需要刷新。
5:如果不需要刷新,直接派发结果。如果需要刷新,调用mDelivery.postResponse派发结果,并将请求加入网络队列重新请求最新数据。
而用于处理网络请求的NetworkDispatcher代码如下所示:
public class NetworkDispatcher extends Thread {
……
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache if applicable.
// 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();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
mDelivery.postError(request, new VolleyError(e));
}
}
}
}
NetworkDispatcher的工作原理与CacheDispatcher相似。当start方法被调用后,它将不停地从mNetworkQueue取出请求,然后通过Network接口向网络发送请求。
在取回请求结果后,如果服务器返回304,并且结果已经通过缓存派发了,那么什么也不做。否则调用Request的parseNetworkResponse方法解析请求结果,并派发结果。