從前在學校用的最多的網絡請求框架就是AsyncHttpClient,用久了發現這樣一個問題,就是代碼復用太難,基本上做不到,所以有一段時間又回歸了HttpURLConnection和HttpClient,再后來又學習了OKHttp的使用,雖然這幾種網絡請求各有各的優勢,但是原理基本上都是一樣的,在android6.0中Google已經不提供HttpClient的API了,所以從長遠的角度來考慮,推薦大家多用OKHttp,關于OKHttp的使用可以參見[OKHttp的簡單使用](http://blog.csdn.net/u012702547/article/details/49045765)。除了上面說的這幾種通信方式,Google在2013年(好早呀
)的I/O大會上還推出了一個網絡請求框架Volley,這和AsyncHttpClient的使用非常像,之前一直沒有總結過Volley的使用,周末有時間總結一下,與大家分享。
Volley適用于交互頻繁但是數據量小的網絡請求,比如我們在上一篇博客中介紹的新聞列表,這種情況下使用Volley就是非常合適的,但是對于一些數據量大的網絡請求,比如下載,Volley就顯得略有力不從心。
Volley是一個開源項目,我們可以在GitHub上獲得它的源代碼,地址[https://github.com/mcxiaoke/android-volley](https://github.com/mcxiaoke/android-volley),拿到之后我們可以將之打包成jar包使用,也可以直接將源碼拷貝到我們的項目中使用,個人推薦第二種方式,這樣發生錯誤的時候方便我們調試,同時也有利于我們修改源碼,定制我們自己的Volley。如果要拷貝源碼,我們只需要將“android-volley-master\android-volley-master\src\main\java”這個文件夾下的com包拷貝到我們的項目中即可。
# 1.請求字符數據
Volley的使用,我們要先獲得一個隊列,我們的所有請求將會放在這個隊列中一個一個執行:
~~~
RequestQueue mQueue = Volley.newRequestQueue(this);
~~~
獲得一個請求隊列只需要一個參數,就是Context,這里因為在MainActivity發起請求,所以直接用this。字符型數據的請求,我們使用StringRequest:
~~~
StringRequest sr = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.i("lenve", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
~~~
StringRequest一共需要三個參數,第一個是我們要請求的http地址,第二個是數據調用成功的回調函數,第三個參數是數據調用失敗的回調函數。我們可以在回調函數中直接更新UI,Volley的這個特點和AsyncHttpClient還是非常相似的,關于這里邊的原理我們后邊有時間可以詳細介紹。獲得一個StringRequest對象的實例之后,我們把它添加到隊列之中,這樣就可以請求到網絡數據了:
~~~
mQueue.add(sr);
~~~
嗯,就是這么簡單。那我們不禁有疑問了,剛才這個請求時get請求還是post請求?我們如何自己設置網絡請求方式?我們看一下這個構造方法的源碼:
~~~
/**
* Creates a new GET request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
~~~
原來這個構造方法調用了另外一個構造方法,并且第一個參數傳遞了Method.GET,也就是說上面的請求其實是一個GET請求,那么我們如果要使用POST請求就直接通過下面這個構造方法來實現就可以了:
~~~
/**
* Creates a new request with the given method.
*
* @param method the request {@link Method} to use
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
~~~
那么使用了POST請求之后我們該怎么樣來傳遞參數呢?我們看到StringRequest中并沒有類似的方法來讓我們完成工作,但是StringRequest繼承自Request,我們看看Request中有沒有,很幸運,我們找到了:
~~~
/**
* Returns a Map of parameters to be used for a POST or PUT request. Can throw
* {@link AuthFailureError} as authentication may be required to provide these values.
*
* <p>Note that you can directly override {@link #getBody()} for custom data.</p>
*
* @throws AuthFailureError in the event of auth failure
*/
protected Map<String, String> getParams() throws AuthFailureError {
return null;
}
~~~
看注釋我們大概就明白這個方法是干什么的了,它將POST請求或者PUT請求需要的參數封裝成一個Map對象,那么我們如果在POST請求中需要傳參的話,直接重寫這個方法就可以了,代碼如下:
~~~
StringRequest sr = new StringRequest("http://www.baidu.com",
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.i("lenve", response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
}) {
/**
* 重寫getParams(),可以自己組裝post要提交的參數
*/
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> map = new HashMap<String, String>();
map.put("params1", "value1");
map.put("params1", "value1");
return map;
}
};
~~~
好了,請求字符數據就是這么簡單。
# 2.請求JSON數據
關于json數據的請求,Volley已經給我們提供了相關的類了:
~~~
JsonObjectRequest jsonReq = new JsonObjectRequest(HTTPURL,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
JSONObject jo = response.getJSONObject("paramz");
JSONArray ja = jo.getJSONArray("feeds");
for (int i = 0; i < ja.length(); i++) {
JSONObject jo1 = ja.getJSONObject(i)
.getJSONObject("data");
Log.i("lenve", jo1.getString("subject"));
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
mQueue.add(jsonReq);
~~~
我們看看打印出來的結果:

OK,沒問題,就是這么簡單,以此類推,你應該也就會使用JSONArray了。
# 3.使用ImageLoader加載圖片
Volley提供的另外一個非常好用的工具就是ImageLoader,這個網絡請求圖片的工具類,帶給我們許多方便,首先它會自動幫助我們把圖片緩存在本地,如果本地有圖片,它就不會從網絡獲取圖片,如果本地沒有緩存圖片,它就會從網絡獲取圖片,同時如果本地緩存的數據超過我們設置的最大緩存界限,它會自動移除我們在最近用的比較少的圖片。我們看一下代碼:
~~~
ImageLoader il = new ImageLoader(mQueue, new BitmapCache());
ImageListener listener = ImageLoader.getImageListener(iv,
R.drawable.ic_launcher, R.drawable.ic_launcher);
il.get(IMAGEURL, listener);
~~~
先實例化一個ImageLoader,實例化ImageLoader需要兩個參數,第一個就是我們前文說的網絡請求隊列,第二個參數就是一個圖片緩存類,這個類要我們自己來實現,其實只需要實現ImageCache接口就可以了,里面具體的緩存邏輯由我們自己定義,我們使用Google提供的LruCache來實現圖片的緩存。然后就是我們需要一個listener,獲得這個listener需要三個參數,第一個是我們要加載網絡圖片的ImageView,第二個參數是默認圖片,第三個參數是加載失敗時候顯示的圖片。最后通過get方法來實現圖片的加載,通過源碼追蹤我們發現這個get方法會來到這樣一個方法中:
~~~
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
// only fulfill requests that were initiated from the main thread.
throwIfNotOnMainThread();
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
// The bitmap did not exist in the cache, fetch it!
ImageContainer imageContainer =
new ImageContainer(null, requestUrl, cacheKey, imageListener);
// Update the caller to let them know that they should use the default bitmap.
imageListener.onResponse(imageContainer, true);
// Check to see if a request is already in-flight.
BatchedImageRequest request = mInFlightRequests.get(cacheKey);
if (request != null) {
// If it is, add this request to the list of listeners.
request.addContainer(imageContainer);
return imageContainer;
}
// The request is not already in flight. Send the new request to the network and
// track it.
Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
cacheKey);
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
~~~
我們看到在這個方法中會先判斷本地緩存中是否有我們需要的圖片,如果有的話直接從本地加載,沒有才會請求網絡數據,這正是使用ImageLoader的方便之處。
# 4.使用NetworkImageView加載網絡圖片
NetworkImageView和前面說的ImageLoader其實非常相像,不同的是如果使用NetworkImageView的話,我們的控件就不是ImageView了,而是NetworkImageView,我們看看布局文件:
~~~
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/iv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
~~~
在MainActivity中拿到它:
~~~
NetworkImageView niv = (NetworkImageView) this.findViewById(R.id.iv2);
~~~
設置網絡請求參數:
~~~
niv.setDefaultImageResId(R.drawable.ic_launcher);
ImageLoader il = new ImageLoader(mQueue, new BitmapCache());
niv.setImageUrl(IMAGEURL, il);
~~~
首先設置默認圖片,然后是一個ImageLoader,這個ImageLoader和前文說的一模一樣,最后就是設置請求圖片的URL地址和一個ImageLoader,我們基本可以判斷NetworkImageView也會自動幫我們處理圖片的緩存問題。事實上的確如此,我們通過追蹤setImageUrl這個方法的源碼,發現它最終也會執行到我們在上面貼出來的那個方法中:
~~~
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight, ScaleType scaleType) {
....
}
~~~
沒錯,NetworkImageView和ImageLoader最終都會到達這個方法,所以說這兩個東東其實非常像,那么在實際開發中究竟用哪個要視情況而定。
# 5.ImageRequest的使用
ImageRequest也是用來加載網絡圖片的,用法與請求字符串數據和請求json數據差不多:
~~~
ImageRequest ir = new ImageRequest(IMAGEURL, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
iv3.setImageBitmap(response);
}
}, 200, 200, ScaleType.CENTER, Bitmap.Config.ARGB_8888,
new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
mQueue.add(ir);
~~~
ImageRequest一共需要七個參數,第一個是要請求的圖片的IP地址,第二個請求成功的回調函數,第三個參數和第四個參數表示允許圖片的最大寬高,如果圖片的大小超出了設置會自動縮小,縮小方法依照第五個參數的設定,第三個和第四個參數如果設置為0則不會對圖片的大小做任何處理(原圖顯示),第六個參數表示繪圖的色彩模式,第七個參數是請求失敗時的回調函數。這個和前面兩種加載圖片的方式比較起來還是稍微有點麻煩。
# 6.定制自己的Request
上面介紹了Volley可以實現的幾種請求,但是畢竟還是比較有限,而我們在項目中遇到的情況可能是各種各樣的,比如服務端如果傳給我們的是一個XML數據,那么我們該怎樣使用Volley?Volley的強大之處除了上文我們說的之外,還在于它是開源的,我們可以根據自己的需要來定制Volley。那么我們就看看怎么定制XMLRequest,在定制已之前我們先看看JSONRequest是怎么實現的?
~~~
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 requestBody A {@link String} 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, String requestBody,
Listener<JSONObject> listener, ErrorListener errorListener) {
super(method, url, requestBody, listener,
errorListener);
}
/**
* Creates a new request.
* @param url URL to fetch the JSON from
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonObjectRequest(String url, Listener<JSONObject> listener, ErrorListener errorListener) {
super(Method.GET, url, null, listener, errorListener);
}
/**
* Creates a new request.
* @param method the HTTP method to use
* @param url URL to fetch the JSON from
* @param listener Listener to receive the JSON response
* @param errorListener Error listener, or null to ignore errors.
*/
public JsonObjectRequest(int method, String url, Listener<JSONObject> listener, ErrorListener errorListener) {
super(method, url, null, listener, errorListener);
}
/**
* 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));
}
}
}
~~~
哈,原來這么簡單,光是構造方法就有五個,不過構造方法都很簡單,我們就不多說,核心的功能在parseNetworkResponse方法中,這里調用成功的時候返回一個Response泛型,泛型里邊的東西是一個JSONObject,其實也很簡單,我們如果要處理XML,那么直接在重寫parseNetworkResponse方法,在調用成功的時候直接返回一個Response泛型,這個泛型中是一個XmlPullParser對象,哈哈,很簡單吧,同時,結合StringRequest的實現方式,我們實現了XMLRequest的代碼:
~~~
public class XMLRequest extends Request<XmlPullParser> {
private Listener<XmlPullParser> mListener;
public XMLRequest(String url, Listener<XmlPullParser> mListener,
ErrorListener listener) {
this(Method.GET, url, mListener, listener);
}
public XMLRequest(int method, String url,
Listener<XmlPullParser> mListener, ErrorListener listener) {
super(method, url, listener);
this.mListener = mListener;
}
@Override
protected Response<XmlPullParser> parseNetworkResponse(
NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
XmlPullParser parser = factory.newPullParser();
parser.setInput(new StringReader(parsed));
return Response.success(parser,
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
} catch (XmlPullParserException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void deliverResponse(XmlPullParser response) {
if (mListener != null) {
mListener.onResponse(response);
}
}
@Override
protected void onFinish() {
super.onFinish();
mListener = null;
}
}
~~~
就是這么簡單,在parseNetworkResponse方法中我們先拿到一個xml的字符串,再將這個字符串轉為一個XmlPullParser對象,最后返回一個Response泛型,這個泛型中是一個XmlPullParser對象。然后我們就可以使用這個XMLRequest了:
~~~
XMLRequest xr = new XMLRequest(XMLURL,
new Response.Listener<XmlPullParser>() {
@Override
public void onResponse(XmlPullParser parser) {
try {
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
break;
case XmlPullParser.START_TAG:
String tagName = parser.getName();
if ("city".equals(tagName)) {
Log.i("lenve",
new String(parser.nextText()));
}
break;
case XmlPullParser.END_TAG:
break;
}
eventType = parser.next();
}
} catch (XmlPullParserException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
mQueue.add(xr);
~~~
用法和JSONRequest的用法一致。會自定義XMLRequest,那么照貓畫虎也可以自定義一個GsonRequest,各種各樣的數據類型我們都可以自己定制了。
好了,關于Volley的使用我們就介紹到這里,有問題歡迎留言討論。
[Demo下載https://github.com/lenve/VolleyDemo](https://github.com/lenve/VolleyDemo)