在2013年Google I/O大會上,Android開發團隊公佈了一個新的網絡通訊框架:Volley。它適合處理通訊頻繁的網絡操做,但對於每一次通訊的數據量則有較爲苛刻的限制。本文將介紹該通訊框架的用法(包括使用現成和自定義的Request),並從源碼的角度探究其工做機制。android
目前,Android系統中用於實現HTTP通訊的方式主要有HttpURLConnection和HttpClient兩個類[1],而封裝了它們的框架主要有AsyncHttpClient和Universal-Image-Loader等。Volley庫將HTTP通訊進一步簡單化,它的用法以下:web
首先,因爲HTTP通訊勢必要訪問網絡,咱們須要在Android項目的Manifest.xml文件中添加訪問網絡的許可[2]:算法
<uses-permission android:name="android.permission.INTERNET" /> json
接下來,要先創建一個請求隊列對象(RequestQueue):api
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;
}
};
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方法解析請求結果,並派髮結果。