Android Volley解析(一)之GET、POST請求篇
一、 Volley 的地位
自2013年Google I/O 大會上,Google 推出 Volley 之後,一直到至今,由於其使用簡單、程式碼輕量、通訊速度快、併發量大等特點,倍受開發者們的青睞。
先看兩張圖,讓圖片告訴我們 Volley 的用處;
第一張 Volley 的經典圖
通過上圖,我們可以發現 Volley適合網路通訊頻繁操作,並能同時實現多個網路通訊。
第二張圖
我們在以前在 ListView 的 item 中如果有網路請求,一般都是通過Task 非同步任務來完成,並在完成之後通知 Adapter 更新資料。而Volley 不需要這麼麻煩,因為裡面已經為我們封裝好了處理的執行緒,網路請求,快取的獲取,資料的回掉都是對應不同的執行緒。
二、Volley使用步驟及基本分析
volley 的使用遵循以下四步:
1、獲取請求隊裡RequestQueue
RequestQueue mRequestQueue = Vollay.newRequestQueue(Context context) ;
2、啟動請求佇列
mRequestQueue.start();
以上這兩步通常也歸為一步
3、獲取請求Request
Request mRequest = new ObjectRequest(…) ;
ObjectRequest需要根據自己請求返回的資料來定製,繼承之抽象類Request,Vollay 已經為我們實現了 StringRequest、JsonArrayRequest、JsonObjectRequest、ImageRequest請求;
4、把請求新增到請求佇列中
mRequestQueue.add(mRequest);
說明:在一個專案中,請求佇列不需要出現多個,一般整個專案中共用同一個mRequestQueue,因為請求佇列啟動的時候會做以下事情
/**
* Starts the dispatchers in this queue.
*/
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.
//啟動網路請求處理執行緒,預設為5個,可以自己設定 size
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
//儲存網路請求執行緒
mDispatchers[i] = networkDispatcher;
//啟動網路請求處理執行緒
networkDispatcher.start();
}
}
啟動一個快取mCacheDispatcher執行緒,用來讀取快取資料,啟動若干個網路請求mDispatchers執行緒,用來實現網路通訊。
mCacheDispatcher執行緒的 run 方法
@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();
//列印 log 資訊
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");
Log.i("CacheDispatcher", "沒有快取資料:" + request.getUrl());
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);
Log.i("CacheDispatcher", "快取資料過期:" + request.getUrl());
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
// 已經獲取到了有效的快取資料,回撥給 request 的parseNetworkResponse,需要自己根據需求來解析資料
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.
Log.i("CacheDispatcher", "獲取快取資料:" + request.getUrl());
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;
}
}
}
mDispatchers執行緒的 run 方法
@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;
}
// Tag the request (if API >= 14)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
}
// 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));
}
}
}
這兩個執行緒處理型別基本相同,都是採用迴圈的方法,在佇列中獲取請求,有請求則執行相應的請求,沒有則阻塞在下面兩行程式碼中
//阻塞執行緒的執行
//快取執行緒阻塞的地方
final Request request = mCacheQueue.take();
//網路請求阻塞的地方
request = mQueue.take();
所以我們一般只需要根據不同的介面,例項化不同的請求 Request,往佇列中新增 即可,它首先判斷請求是否需要快取,如果不需要,直接新增到網路請求的佇列中,結束下面的操作,如果需要快取,則把請求新增到快取佇列中,具體看程式碼。
public Request add(Request 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;
}
}
所以如果需要快取的話,一開始會從mCacheQueue.take()會得到執行,當不符合要求的時候,請求會新增到真正的網路請求佇列中,以下是不符合要求的程式碼
//沒有快取
if (entry == null) {
request.addMarker("cache-miss");
Log.i("CacheDispatcher", "沒有快取資料:" + request.getUrl());
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);
Log.i("CacheDispatcher", "快取資料過期:" + request.getUrl());
mNetworkQueue.put(request);
continue;
}
如果快取不符合要求,網路執行緒終止阻塞得到執行;
我們一般習慣用法是在 Application 中全域性初始化RequestQueue mRequestQueue,並啟動它,讓整個應用都能獲取到。具體運用將會在下面用到。
三、Volley 實戰 GET 請求和 POST 請求
/**
* Created by gyzhong on 15/3/3.
*/
public class TestBean {
@Expose
private int id ;
@Expose
private String name ;
@Expose
private int download ;
@Expose
private int version ;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getDownload() {
return download;
}
public void setDownload(int download) {
this.download = download;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
1、GET 請求
第一步:在 Application 中初始化RequestQueue,
//初始化請求佇列
private void initRequestQueue(){
//初始化 volley
VolleyUtil.initialize(mContext);
}
/**
* Created by gyzhong on 15/3/1.
*/
public class VolleyUtil {
private static RequestQueue mRequestQueue ;
public static void initialize(Context context){
if (mRequestQueue == null){
synchronized (VolleyUtil.class){
if (mRequestQueue == null){
mRequestQueue = Volley.newRequestQueue(context) ;
}
}
}
mRequestQueue.start();
}
public static RequestQueue getRequestQueue(){
if (mRequestQueue == null)
throw new RuntimeException("請先初始化mRequestQueue") ;
return mRequestQueue ;
}
}
第二步:定製 Request
先來分析介面所返回的資料,我們看到是一條 json 資料,雖然 Volley 中已經為我們定製好了JsonObjectRequest請求,但我們知道,在資料具體顯示的時候,是需要把 json 資料轉化為物件進行處理,所以這裡我們可以定製通用的物件請求。如何定製呢?
先看StringRequest的實現程式碼
//繼承Request<String>,String 為請求解析之後的資料
public class StringRequest extends Request<String> {
//正確資料回撥介面
private final Listener<String> mListener;
public StringRequest(int method, String url, Listener<String> listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
this(Method.GET, url, listener, errorListener);
}
//回撥解析之後的資料
@Override
protected void deliverResponse(String response) {
mListener.onResponse(response);
}
//解析資料,把網路請求,或者中快取中獲取的資料,解析成 String
@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
}
通過上面程式碼可知,StringRequest繼承了 Request 並實現了兩個抽象方法parseNetworkResponse()和 deliverResponse(),這兩個方法很好理解,parseNetworkResponse()把獲取到的資料解析成我們所定義的資料型別;deliverResponse()把所解析的資料通過回撥介面回撥給展示處。
為了簡化回撥介面,這裡把錯誤回撥Response.ErrorListener 和正確的資料回撥Response.Listener合併成一個ResponseListener
/**
* Created by gyzhong on 15/3/1.
* 簡化回撥介面
*/
public interface ResponseListener<T> extends Response.ErrorListener,Response.Listener<T> {
}
根據 StringRequest,如法炮製
/**
* Created by gyzhong on 15/3/1.
*/
public class GetObjectRequest<T> extends Request<T> {
/**
* 正確資料的時候回掉用
*/
private ResponseListener mListener ;
/*用來解析 json 用的*/
private Gson mGson ;
/*在用 gson 解析 json 資料的時候,需要用到這個引數*/
private Type mClazz ;
public GetObjectRequest(String url,Type type, ResponseListener listener) {
super(Method.GET, url, listener);
this.mListener = listener ;
mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() ;
mClazz = type ;
}
/**
* 這裡開始解析資料
* @param response Response from the network
* @return
*/
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
T result ;
String jsonString =
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
result = mGson.fromJson(jsonString,mClazz) ;
return Response.success(result,
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
}
}
/**
* 回撥正確的資料
* @param response The parsed response returned by
*/
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);
}
}
以上程式碼中在例項化 Gson 的時候用到的是mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation(),主要是用於過濾欄位用的.如果有疑問的同學可以參考我前面寫的一篇文章Gson 過濾欄位的幾種方法
第三步:獲取 request
Request request = new GetObjectRequest(url,new TypeToken<TestBean>(){}.getType(),listener) ;
1、url -> http://www.minongbang.com/test.php?test=minongbang
2、new TypeToken(){}.getType() ->為 gson 解析 json 資料所要的 type
3、listener -> 為我們定義的ResponseListener回撥介面
第四步:新增請求到佇列中
VolleyUtil.getRequestQueue().add(request) ;
所以,此介面的程式碼即為
/**
* Minong 測試資料get網路請求介面
* @param value 要搜尋的關鍵字
* @param listener 回撥介面,包含錯誤回撥和正確的資料回撥
*/
public static void getObjectMiNongApi(String value,ResponseListener listener){
String url ;
try {
url = Constant.MinongHost +"?test="+ URLEncoder.encode(value, "utf-8") ;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
url = Constant.MinongHost +"?test="+ URLEncoder.encode(value) ;
}
Request request = new GetObjectRequest(url,new TypeToken<TestBean>(){}.getType(),listener) ;
VolleyUtil.getRequestQueue().add(request) ;
}
第五步:程式碼測試
public class GetRequestActivity extends ActionBarActivity {
/*資料顯示的View*/
private TextView mIdTxt,mNameTxt,mDownloadTxt,mLogoTxt,mVersionTxt ;
/*彈出等待對話方塊*/
private ProgressDialog mDialog ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_get);
mIdTxt = (TextView) findViewById(R.id.id_id) ;
mNameTxt = (TextView) findViewById(R.id.id_name) ;
mDownloadTxt = (TextView) findViewById(R.id.id_download) ;
mLogoTxt = (TextView) findViewById(R.id.id_logo) ;
mVersionTxt = (TextView) findViewById(R.id.id_version) ;
mDialog = new ProgressDialog(this) ;
mDialog.setMessage("get請求中...");
mDialog.show();
/*請求網路獲取資料*/
MiNongApi.getObjectMiNongApi("minongbang",new ResponseListener<TestBean>() {
@Override
public void onErrorResponse(VolleyError error) {
mDialog.dismiss();
}
@Override
public void onResponse(TestBean response) {
mDialog.dismiss();
/*顯示資料*/
mIdTxt.setText(response.getId()+"");
mNameTxt.setText(response.getName());
mDownloadTxt.setText(response.getDownload()+"");
mLogoTxt.setText(response.getLogo());
mVersionTxt.setText(response.getVersion()+"");
}
});
}
}
測試效果圖如下:
可以看到和我們在瀏覽器中請求的資料一模一樣!
2、POST請求
因為在講 get 請求的時候花了很大篇幅講原理,所以在 post 請求的時候,需要注意的東西相對來說比較少, 不管是 get 請求還是 post 請求,實現步驟是不會變。 這裡post 請求,我們也是用http://www.minongbang.com/test.php?test=minongbang這個 api 來測試!
在前面我們已經講到了,在同一個應用中共用同一個 RequestQueue,所以第一步可以省略,因為我們已經實現過了。這裡直接到定製Request,我們在學習網路程式設計的時候就已經知道,用 GET方式請求,請求的資料是直接跟在 URL的後面用”?”去分開了,如果有多個數據則用”&”分開。而 POST則把資料直接封裝在HTTP的包體中,兩者各有優缺點,自己衡量著用。
因為 api 介面還是同一個,所以返回的資料型別肯定是一樣的,在解析資料的時候就可以和 GetObjectRequest 複用,所以 PostObjectRequest 的實現可以通過繼承GetObjectRequest的方式,也可以直接拷貝一份出來,為了更好的區分,我這裡就直接拷貝一份,然後再稍加修改。
/**
* Created by gyzhong on 15/3/1.
*/
public class PostObjectRequest<T> extends Request<T> {
/**
* 正確資料的時候回掉用
*/
private ResponseListener mListener ;
/*用來解析 json 用的*/
private Gson mGson ;
/*在用 gson 解析 json 資料的時候,需要用到這個引數*/
private Type mClazz ;
/*請求 資料通過引數的形式傳入*/
private Map<String,String> mParams;
//需要傳入引數,並且請求方式不能再為 get,改為 post
public PostObjectRequest(String url, Map<String,String> params,Type type, ResponseListener listener) {
super(Method.POST, url, listener);
this.mListener = listener ;
mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() ;
mClazz = type ;
setShouldCache(false);
mParams = params ;
}
/**
* 這裡開始解析資料
* @param response Response from the network
* @return
*/
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
T result ;
String jsonString =
new String(response.data, HttpHeaderParser.parseCharset(response.headers));
Log.v("zgy", "====jsonString===" + jsonString);
result = mGson.fromJson(jsonString,mClazz) ;
return Response.success(result,
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
}
}
/**
* 回撥正確的資料
* @param response The parsed response returned by
*/
@Override
protected void deliverResponse(T response) {
mListener.onResponse(response);
}
//關鍵程式碼就在這裡,在 Volley 的網路操作中,如果判斷請求方式為 Post 則會通過此方法來獲取 param,所以在這裡返回我們需要的引數,
@Override
protected Map<String, String> getParams() throws AuthFailureError {
return mParams;
}
}
再來看看 api 介面怎麼實現,
/*
* *Minong 測試資料post網路請求介面
* @param value 測試資料
* @param listener 回撥介面,包含錯誤回撥和正確的資料回撥
*/
public static void postObjectMinongApi(String value,ResponseListener listener){
Map<String,String> param = new HashMap<String,String>() ;
param.put("test",value) ;
Request request = new PostObjectRequest(Constant.MinongHost,param,new TypeToken<TestBean>(){}.getType(),listener);
VolleyUtil.getRequestQueue().add(request) ;
}
跟 get 請求還是很相似的,只是在例項化 Request 的時候多傳入了一個param引數,並且 url 不能再是包含請求資料的 url。
介面 api測試程式碼
public class PostRequestActivity extends ActionBarActivity {
/*資料顯示的View*/
private TextView mIdTxt,mNameTxt,mDownloadTxt,mLogoTxt,mVersionTxt ;
/*彈出等待對話方塊*/
private ProgressDialog mDialog ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_get);
mIdTxt = (TextView) findViewById(R.id.id_id) ;
mNameTxt = (TextView) findViewById(R.id.id_name) ;
mDownloadTxt = (TextView) findViewById(R.id.id_download) ;
mLogoTxt = (TextView) findViewById(R.id.id_logo) ;
mVersionTxt = (TextView) findViewById(R.id.id_version) ;
mDialog = new ProgressDialog(this) ;
mDialog.setMessage("post請求中...");
mDialog.show();
/*請求網路獲取資料*/
MiNongApi.postObjectMinongApi("minongbang",new ResponseListener<TestBean>() {
@Override
public void onErrorResponse(VolleyError error) {
mDialog.dismiss();
}
@Override
public void onResponse(TestBean response) {
mDialog.dismiss();
/*顯示資料*/
mIdTxt.setText(response.getId()+"");
mNameTxt.setText(response.getName());
mDownloadTxt.setText(response.getDownload()+"");
mLogoTxt.setText(response.getLogo());
mVersionTxt.setText(response.getVersion()+"");
}
});
}
}
測試資料顯示跟 get 請求完全相同;ok,以上就是 Volley GET請求和 POST請求的全部內容!接下來又到了總結的時候
四、總結
1、volley 適用於輕量高併發的網路請求,這裡補充一個知識點,因為 Volley 請求網路的資料全部儲存在記憶體中,所以 volley 不適合請求較大的資料,比如下載檔案,下載大圖片等。
2、volley 的使用遵循四個步驟
a、RequestQueue mRequestQueue = Vollay.newRequestQueue(Context context) ;
b、mRequestQueue.start()
c、Request mRequest = new ObjectRequst(…)
d、mRequestQueue.add(mRequest)
3、同一個程式中最好共用一個 RequestQueue。
4、可以根據介面的放回資料型別定製任意的 Request,volley 已經預設為我們實現了 StringRequest、JsonArrayRequest、JsonObjectRequest、ImageRequest四個請求型別。
最後如果覺得有用請繼續關注我的 blog,我將會在下篇 blog 中講解,Volley解析(二)之表單提交篇。