Android volley 完全解析

Android Volley完全解析(一),初識Volley的基本用法

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/17482095

1. Volley簡介

我們平時在開發Android應用的時候不可避免地都需要用到網絡技術,而多數情況下應用程序都會使用HTTP協議來發送和接收網絡數據。android系統中主要提供了兩種方式來進行HTTP通信,HttpURLConnection和HttpClient,幾乎在任何項目的代碼中我們都能看到這兩個類的身影,使用率非常高。

不過HttpURLConnection和HttpClient的用法還是稍微有些複雜的,如果不進行適當封裝的話,很容易就會寫出不少重複代碼。於是乎,一些Android網絡通信框架也就應運而生,比如說AsyncHttpClient,它把HTTP所有的通信細節全部封裝在了內部,我們只需要簡單調用幾行代碼就可以完成通信操作了。再比如Universal-Image-Loader,它使得在界面上顯示網絡圖片的操作變得極度簡單,開發者不用關心如何從網絡上獲取圖片,也不用關心開啓線程、回收圖片資源等細節,Universal-Image-Loader已經把一切都做好了。

Android開發團隊也是意識到了有必要將HTTP的通信操作再進行簡單化,於是在2013年Google I/O大會上推出了一個新的網絡通信框架——Volley。Volley可是說是把AsyncHttpClient和Universal-Image-Loader的優點集於了一身,既可以像AsyncHttpClient一樣非常簡單地進行HTTP通信,也可以像Universal-Image-Loader一樣輕鬆加載網絡上的圖片。除了簡單易用之外,Volley在性能方面也進行了大幅度的調整,它的設計目標就是非常適合去進行數據量不大,但通信頻繁的網絡操作,而對於大數據量的網絡操作,比如說下載文件等,Volley的表現就會非常糟糕。

下圖所示的這些應用都是屬於數據量不大,但網絡通信頻繁的,因此非常適合使用Volley。

2. 下載Volley

介紹了這麼多理論的東西,下面我們就準備開始進行實戰了,首先需要將Volley的jar包準備好,如果你的電腦上裝有Git,可以使用如下命令下載Volley的源碼:

[plain] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. git clone https://android.googlesource.com/platform/frameworks/volley  
下載完成後將它導入到你的Eclipse工程裏,然後再導出一個jar包就可以了。如果你的電腦上沒有Git,那麼也可以直接使用我導出好的jar包,下載地址是:http://download.csdn.net/detail/sinyu890807/7152015 。

新建一個Android項目,將volley.jar文件複製到libs目錄下,這樣準備工作就算是做好了。

3. StringRequest的用法

前面已經說過,Volley的用法非常簡單,那麼我們就從最基本的HTTP通信開始學習吧,即發起一條HTTP請求,然後接收HTTP響應。首先需要獲取到一個RequestQueue對象,可以調用如下方法獲取到:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. RequestQueue mQueue = Volley.newRequestQueue(context);  
注意這裏拿到的RequestQueue是一個請求隊列對象,它可以緩存所有的HTTP請求,然後按照一定的算法併發地發出這些請求。RequestQueue內部的設計就是非常合適高併發的,因此我們不必爲每一次HTTP請求都創建一個RequestQueue對象,這是非常浪費資源的,基本上在每一個需要和網絡交互的Activity中創建一個RequestQueue對象就足夠了。

接下來爲了要發出一條HTTP請求,我們還需要創建一個StringRequest對象,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. StringRequest stringRequest = new StringRequest("http://www.baidu.com",  
  2.                         new Response.Listener<String>() {  
  3.                             @Override  
  4.                             public void onResponse(String response) {  
  5.                                 Log.d("TAG", response);  
  6.                             }  
  7.                         }, new Response.ErrorListener() {  
  8.                             @Override  
  9.                             public void onErrorResponse(VolleyError error) {  
  10.                                 Log.e("TAG", error.getMessage(), error);  
  11.                             }  
  12.                         });  
可以看到,這裏new出了一個StringRequest對象,StringRequest的構造函數需要傳入三個參數,第一個參數就是目標服務器的URL地址,第二個參數是服務器響應成功的回調,第三個參數是服務器響應失敗的回調。其中,目標服務器地址我們填寫的是百度的首頁,然後在響應成功的回調裏打印出服務器返回的內容,在響應失敗的回調裏打印出失敗的詳細信息。

最後,將這個StringRequest對象添加到RequestQueue裏面就可以了,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. mQueue.add(stringRequest);  

另外,由於Volley是要訪問網絡的,因此不要忘記在你的AndroidManifest.xml中添加如下權限:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <uses-permission android:name="android.permission.INTERNET" />  

好了,就是這麼簡單,如果你現在運行一下程序,併發出這樣一條HTTP請求,就會看到LogCat中會打印出如下圖所示的數據。


沒錯,百度返回給我們的就是這樣一長串的HTML代碼,雖然我們看起來會有些吃力,但是瀏覽器卻可以輕鬆地對這段HTML代碼進行解析,然後將百度的首頁展現出來。

這樣的話,一個最基本的HTTP發送與響應的功能就完成了。你會發現根本還沒寫幾行代碼就輕易實現了這個功能,主要就是進行了以下三步操作:

1. 創建一個RequestQueue對象。

2. 創建一個StringRequest對象。

3. 將StringRequest對象添加到RequestQueue裏面。

不過大家都知道,HTTP的請求類型通常有兩種,GET和POST,剛纔我們使用的明顯是一個GET請求,那麼如果想要發出一條POST請求應該怎麼做呢?StringRequest中還提供了另外一種四個參數的構造函數,其中第一個參數就是指定請求類型的,我們可以使用如下方式進行指定:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener);  
可是這只是指定了HTTP請求方式是POST,那麼我們要提交給服務器的參數又該怎麼設置呢?很遺憾,StringRequest中並沒有提供設置POST參數的方法,但是當發出POST請求的時候,Volley會嘗試調用StringRequest的父類——Request中的getParams()方法來獲取POST參數,那麼解決方法自然也就有了,我們只需要在StringRequest的匿名類中重寫getParams()方法,在這裏設置POST參數就可以了,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener) {  
  2.     @Override  
  3.     protected Map<String, String> getParams() throws AuthFailureError {  
  4.         Map<String, String> map = new HashMap<String, String>();  
  5.         map.put("params1""value1");  
  6.         map.put("params2""value2");  
  7.         return map;  
  8.     }  
  9. };  
你可能會說,每次都這樣用起來豈不是很累?連個設置POST參數的方法都沒有。但是不要忘記,Volley是開源的,只要你願意,你可以自由地在裏面添加和修改任何的方法,輕鬆就能定製出一個屬於你自己的Volley版本。

4. JsonRequest的用法

學完了最基本的StringRequest的用法,我們再來進階學習一下JsonRequest的用法。類似於StringRequest,JsonRequest也是繼承自Request類的,不過由於JsonRequest是一個抽象類,因此我們無法直接創建它的實例,那麼只能從它的子類入手了。JsonRequest有兩個直接的子類,JsonObjectRequest和JsonArrayRequest,從名字上你應該能就看出它們的區別了吧?一個是用於請求一段JSON數據的,一個是用於請求一段JSON數組的。

至於它們的用法也基本上沒有什麼特殊之處,先new出一個JsonObjectRequest對象,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://m.weather.com.cn/data/101010100.html"null,  
  2.         new Response.Listener<JSONObject>() {  
  3.             @Override  
  4.             public void onResponse(JSONObject response) {  
  5.                 Log.d("TAG", response.toString());  
  6.             }  
  7.         }, new Response.ErrorListener() {  
  8.             @Override  
  9.             public void onErrorResponse(VolleyError error) {  
  10.                 Log.e("TAG", error.getMessage(), error);  
  11.             }  
  12.         });  
可以看到,這裏我們填寫的URL地址是http://m.weather.com.cn/data/101010100.html,這是中國天氣網提供的一個查詢天氣信息的接口,響應的數據就是以JSON格式返回的,然後我們在onResponse()方法中將返回的數據打印出來。

最後再將這個JsonObjectRequest對象添加到RequestQueue裏就可以了,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. mQueue.add(jsonObjectRequest);  
這樣當HTTP通信完成之後,服務器響應的天氣信息就會回調到onResponse()方法中,並打印出來。現在運行一下程序,發出這樣一條HTTP請求,就會看到LogCat中會打印出如下圖所示的數據。


由此可以看出,服務器返回給我們的數據確實是JSON格式的,並且onResponse()方法中攜帶的參數也正是一個JSONObject對象,之後只需要從JSONObject對象取出我們想要得到的那部分數據就可以了。

你應該發現了吧,JsonObjectRequest的用法和StringRequest的用法基本上是完全一樣的,Volley的易用之處也在這裏體現出來了,會了一種就可以讓你舉一反三,因此關於JsonArrayRequest的用法相信已經不需要我再去講解了吧。

好了,關於Volley的基本用法就講到這裏,下篇文章中我會帶領大家繼續探究Volley。



Android Volley完全解析(二),使用Volley加載網絡圖片

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/17482165

在上一篇文章中,我們瞭解了Volley到底是什麼,以及它的基本用法。本篇文章中我們即將學習關於Volley更加高級的用法,如何你還沒有看過我的上一篇文章的話,建議先去閱讀Android Volley完全解析(一),初識Volley的基本用法

在上篇文章中有提到過,Volley是將AsyncHttpClient和Universal-Image-Loader的優點集成於一身的一個框架。我們都知道,Universal-Image-Loader具備非常強大的加載網絡圖片的功能,而使用Volley,我們也可以實現基本類似的效果,並且在性能上也豪不遜色於Universal-Image-Loader,下面我們就來具體學習一下吧。

1. ImageRequest的用法

前面我們已經學習過了StringRequest和JsonRequest的用法,並且總結出了它們的用法都是非常類似的,基本就是進行以下三步操作即可:

1. 創建一個RequestQueue對象。

2. 創建一個Request對象。

3. 將Request對象添加到RequestQueue裏面。

其中,StringRequest和JsonRequest都是繼承自Request的,所以它們的用法纔會如此類似。那麼不用多說,今天我們要學習的ImageRequest,相信你從名字上就已經猜出來了,它也是繼承自Request的,因此它的用法也是基本相同的,首先需要獲取到一個RequestQueue對象,可以調用如下方法獲取到:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. RequestQueue mQueue = Volley.newRequestQueue(context);  
接下來自然要去new出一個ImageRequest對象了,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. ImageRequest imageRequest = new ImageRequest(  
  2.         "http://developer.android.com/images/home/aw_dac.png",  
  3.         new Response.Listener<Bitmap>() {  
  4.             @Override  
  5.             public void onResponse(Bitmap response) {  
  6.                 imageView.setImageBitmap(response);  
  7.             }  
  8.         }, 00, Config.RGB_565, new Response.ErrorListener() {  
  9.             @Override  
  10.             public void onErrorResponse(VolleyError error) {  
  11.                 imageView.setImageResource(R.drawable.default_image);  
  12.             }  
  13.         });  
可以看到,ImageRequest的構造函數接收六個參數,第一個參數就是圖片的URL地址,這個沒什麼需要解釋的。第二個參數是圖片請求成功的回調,這裏我們把返回的Bitmap參數設置到ImageView中。第三第四個參數分別用於指定允許圖片最大的寬度和高度,如果指定的網絡圖片的寬度或高度大於這裏的最大值,則會對圖片進行壓縮,指定成0的話就表示不管圖片有多大,都不會進行壓縮。第五個參數用於指定圖片的顏色屬性,Bitmap.Config下的幾個常量都可以在這裏使用,其中ARGB_8888可以展示最好的顏色屬性,每個圖片像素佔據4個字節的大小,而RGB_565則表示每個圖片像素佔據2個字節大小。第六個參數是圖片請求失敗的回調,這裏我們當請求失敗時在ImageView中顯示一張默認圖片。

最後將這個ImageRequest對象添加到RequestQueue裏就可以了,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. mQueue.add(imageRequest);  
現在如果運行一下程序,並嘗試發出這樣一條網絡請求,很快就能看到網絡上的圖片在ImageView中顯示出來了,如下圖所示:


2. ImageLoader的用法

如果你覺得ImageRequest已經非常好用了,那我只能說你太容易滿足了 ^_^。實際上,Volley在請求網絡圖片方面可以做到的還遠遠不止這些,而ImageLoader就是一個很好的例子。ImageLoader也可以用於加載網絡上的圖片,並且它的內部也是使用ImageRequest來實現的,不過ImageLoader明顯要比ImageRequest更加高效,因爲它不僅可以幫我們對圖片進行緩存,還可以過濾掉重複的鏈接,避免重複發送請求。

由於ImageLoader已經不是繼承自Request的了,所以它的用法也和我們之前學到的內容有所不同,總結起來大致可以分爲以下四步:

1. 創建一個RequestQueue對象。

2. 創建一個ImageLoader對象。

3. 獲取一個ImageListener對象。

4. 調用ImageLoader的get()方法加載網絡上的圖片。

下面我們就來按照這個步驟,學習一下ImageLoader的用法吧。首先第一步的創建RequestQueue對象我們已經寫過很多遍了,相信已經不用再重複介紹了,那麼就從第二步開始學習吧,新建一個ImageLoader對象,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. ImageLoader imageLoader = new ImageLoader(mQueue, new ImageCache() {  
  2.     @Override  
  3.     public void putBitmap(String url, Bitmap bitmap) {  
  4.     }  
  5.   
  6.     @Override  
  7.     public Bitmap getBitmap(String url) {  
  8.         return null;  
  9.     }  
  10. });  
可以看到,ImageLoader的構造函數接收兩個參數,第一個參數就是RequestQueue對象,第二個參數是一個ImageCache對象,這裏我們先new出一個空的ImageCache的實現即可。

接下來需要獲取一個ImageListener對象,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. ImageListener listener = ImageLoader.getImageListener(imageView,  
  2.         R.drawable.default_image, R.drawable.failed_image);  
我們通過調用ImageLoader的getImageListener()方法能夠獲取到一個ImageListener對象,getImageListener()方法接收三個參數,第一個參數指定用於顯示圖片的ImageView控件,第二個參數指定加載圖片的過程中顯示的圖片,第三個參數指定加載圖片失敗的情況下顯示的圖片。

最後,調用ImageLoader的get()方法來加載圖片,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. imageLoader.get("http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg", listener);  

get()方法接收兩個參數,第一個參數就是圖片的URL地址,第二個參數則是剛剛獲取到的ImageListener對象。當然,如果你想對圖片的大小進行限制,也可以使用get()方法的重載,指定圖片允許的最大寬度和高度,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. imageLoader.get("http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",  
  2.                 listener, 200200);  

現在運行一下程序並開始加載圖片,你將看到ImageView中會先顯示一張默認的圖片,等到網絡上的圖片加載完成後,ImageView則會自動顯示該圖,效果如下圖所示。


雖然現在我們已經掌握了ImageLoader的用法,但是剛纔介紹的ImageLoader的優點卻還沒有使用到。爲什麼呢?因爲這裏創建的ImageCache對象是一個空的實現,完全沒能起到圖片緩存的作用。其實寫一個ImageCache也非常簡單,但是如果想要寫一個性能非常好的ImageCache,最好就要藉助Android提供的LruCache功能了,如果你對LruCache還不瞭解,可以參考我之前的一篇博客Android高效加載大圖、多圖解決方案,有效避免程序OOM

這裏我們新建一個BitmapCache並實現了ImageCache接口,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class BitmapCache implements ImageCache {  
  2.   
  3.     private LruCache<String, Bitmap> mCache;  
  4.   
  5.     public BitmapCache() {  
  6.         int maxSize = 10 * 1024 * 1024;  
  7.         mCache = new LruCache<String, Bitmap>(maxSize) {  
  8.             @Override  
  9.             protected int sizeOf(String key, Bitmap bitmap) {  
  10.                 return bitmap.getRowBytes() * bitmap.getHeight();  
  11.             }  
  12.         };  
  13.     }  
  14.   
  15.     @Override  
  16.     public Bitmap getBitmap(String url) {  
  17.         return mCache.get(url);  
  18.     }  
  19.   
  20.     @Override  
  21.     public void putBitmap(String url, Bitmap bitmap) {  
  22.         mCache.put(url, bitmap);  
  23.     }  
  24.   
  25. }  
可以看到,這裏我們將緩存圖片的大小設置爲10M。接着修改創建ImageLoader實例的代碼,第二個參數傳入BitmapCache的實例,如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());  
這樣我們就把ImageLoader的功能優勢充分利用起來了。

3. NetworkImageView的用法

除了以上兩種方式之外,Volley還提供了第三種方式來加載網絡圖片,即使用NetworkImageView。不同於以上兩種方式,NetworkImageView是一個自定義控制,它是繼承自ImageView的,具備ImageView控件的所有功能,並且在原生的基礎之上加入了加載網絡圖片的功能。NetworkImageView控件的用法要比前兩種方式更加簡單,大致可以分爲以下五步:

1. 創建一個RequestQueue對象。

2. 創建一個ImageLoader對象。

3. 在佈局文件中添加一個NetworkImageView控件。

4. 在代碼中獲取該控件的實例。

5. 設置要加載的圖片地址。

其中,第一第二步和ImageLoader的用法是完全一樣的,因此這裏我們就從第三步開始學習了。首先修改佈局文件中的代碼,在裏面加入NetworkImageView控件,如下所示:

[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="fill_parent"  
  4.     android:orientation="vertical" >  
  5.   
  6.     <Button  
  7.         android:id="@+id/button"  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:text="Send Request" />  
  11.       
  12.     <com.android.volley.toolbox.NetworkImageView   
  13.         android:id="@+id/network_image_view"  
  14.         android:layout_width="200dp"  
  15.         android:layout_height="200dp"  
  16.         android:layout_gravity="center_horizontal"  
  17.         />  
  18.   
  19. </LinearLayout>  
接着在Activity獲取到這個控件的實例,這就非常簡單了,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. networkImageView = (NetworkImageView) findViewById(R.id.network_image_view);  
得到了NetworkImageView控件的實例之後,我們可以調用它的setDefaultImageResId()方法、setErrorImageResId()方法和setImageUrl()方法來分別設置加載中顯示的圖片,加載失敗時顯示的圖片,以及目標圖片的URL地址,如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. networkImageView.setDefaultImageResId(R.drawable.default_image);  
  2. networkImageView.setErrorImageResId(R.drawable.failed_image);  
  3. networkImageView.setImageUrl("http://img.my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",  
  4.                 imageLoader);  
其中,setImageUrl()方法接收兩個參數,第一個參數用於指定圖片的URL地址,第二個參數則是前面創建好的ImageLoader對象。

好了,就是這麼簡單,現在重新運行一下程序,你將看到和使用ImageLoader來加載圖片一模一樣的效果,這裏我就不再截圖了。

這時有的朋友可能就會問了,使用ImageRequest和ImageLoader這兩種方式來加載網絡圖片,都可以傳入一個最大寬度和高度的參數來對圖片進行壓縮,而NetworkImageView中則完全沒有提供設置最大寬度和高度的方法,那麼是不是使用NetworkImageView來加載的圖片都不會進行壓縮呢?

其實並不是這樣的,NetworkImageView並不需要提供任何設置最大寬高的方法也能夠對加載的圖片進行壓縮。這是由於NetworkImageView是一個控件,在加載圖片的時候它會自動獲取自身的寬高,然後對比網絡圖片的寬度,再決定是否需要對圖片進行壓縮。也就是說,壓縮過程是在內部完全自動化的,並不需要我們關心,NetworkImageView會始終呈現給我們一張大小剛剛好的網絡圖片,不會多佔用任何一點內存,這也是NetworkImageView最簡單好用的一點吧。

當然了,如果你不想對圖片進行壓縮的話,其實也很簡單,只需要在佈局文件中把NetworkImageView的layout_width和layout_height都設置成wrap_content就可以了,這樣NetworkImageView就會將該圖片的原始大小展示出來,不會進行任何壓縮。

這樣我們就把使用Volley來加載網絡圖片的用法都學習完了,今天的講解也就到此爲止,下一篇文章中我會帶大家繼續探究Volley的更多功能。



Android Volley完全解析(三),定製自己的Request

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/17612763

經過前面兩篇文章的學習,我們已經掌握了Volley各種Request的使用方法,包括StringRequest、JsonRequest、ImageRequest等。其中StringRequest用於請求一條普通的文本數據,JsonRequest(JsonObjectRequest、JsonArrayRequest)用於請求一條JSON格式的數據,ImageRequest則是用於請求網絡上的一張圖片。

可是Volley提供給我們的Request類型就只有這麼多,而我們都知道,在網絡上傳輸的數據通常有兩種格式,JSON和XML,那麼如果想要請求一條XML格式的數據該怎麼辦呢?其實很簡單,Volley提供了非常強的擴展機制,使得我們可以很輕鬆地定製出任意類型的Request,這也就是本篇文章的主題了。

在開始之前還是友情提醒一下,如果你還沒有閱讀過我前面兩篇關於Volley的文章,建議先去閱讀一下Android Volley完全解析(一),初識Volley的基本用法Android Volley完全解析(二),使用Volley加載網絡圖片

1. 自定義XMLRequest

下面我們準備自定義一個XMLRequest,用於請求一條XML格式的數據。那麼該從哪裏開始入手呢?額,好像是有些無從下手。遇到這種情況,我們應該去參考一下Volley的源碼,看一看StringRequest是怎麼實現的,然後就可以模仿着寫出XMLRequest了。首先看下StringRequest的源碼,如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. /** 
  2.  * A canned request for retrieving the response body at a given URL as a String. 
  3.  */  
  4. public class StringRequest extends Request<String> {  
  5.     private final Listener<String> mListener;  
  6.   
  7.     /** 
  8.      * Creates a new request with the given method. 
  9.      * 
  10.      * @param method the request {@link Method} to use 
  11.      * @param url URL to fetch the string at 
  12.      * @param listener Listener to receive the String response 
  13.      * @param errorListener Error listener, or null to ignore errors 
  14.      */  
  15.     public StringRequest(int method, String url, Listener<String> listener,  
  16.             ErrorListener errorListener) {  
  17.         super(method, url, errorListener);  
  18.         mListener = listener;  
  19.     }  
  20.   
  21.     /** 
  22.      * Creates a new GET request. 
  23.      * 
  24.      * @param url URL to fetch the string at 
  25.      * @param listener Listener to receive the String response 
  26.      * @param errorListener Error listener, or null to ignore errors 
  27.      */  
  28.     public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {  
  29.         this(Method.GET, url, listener, errorListener);  
  30.     }  
  31.   
  32.     @Override  
  33.     protected void deliverResponse(String response) {  
  34.         mListener.onResponse(response);  
  35.     }  
  36.   
  37.     @Override  
  38.     protected Response<String> parseNetworkResponse(NetworkResponse response) {  
  39.         String parsed;  
  40.         try {  
  41.             parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));  
  42.         } catch (UnsupportedEncodingException e) {  
  43.             parsed = new String(response.data);  
  44.         }  
  45.         return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));  
  46.     }  
  47. }  

可以看到,StringRequest的源碼很簡練,根本就沒幾行代碼,我們一起來分析下。首先StringRequest是繼承自Request類的,Request可以指定一個泛型類,這裏指定的當然就是String了,接下來StringRequest中提供了兩個有參的構造函數,參數包括請求類型,請求地址,以及響應回調等,由於我們已經很熟悉StringRequest的用法了,相信這幾個參數的作用都不用再解釋了吧。但需要注意的是,在構造函數中一定要調用super()方法將這幾個參數傳給父類,因爲HTTP的請求和響應都是在父類中自動處理的。

另外,由於Request類中的deliverResponse()和parseNetworkResponse()是兩個抽象方法,因此StringRequest中需要對這兩個方法進行實現。deliverResponse()方法中的實現很簡單,僅僅是調用了mListener中的onResponse()方法,並將response內容傳入即可,這樣就可以將服務器響應的數據進行回調了。parseNetworkResponse()方法中則應該對服務器響應的數據進行解析,其中數據是以字節的形式存放在NetworkResponse的data變量中的,這裏將數據取出然後組裝成一個String,並傳入Response的success()方法中即可。

瞭解了StringRequest的實現原理,下面我們就可以動手來嘗試實現一下XMLRequest了,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class XMLRequest extends Request<XmlPullParser> {  
  2.   
  3.     private final Listener<XmlPullParser> mListener;  
  4.   
  5.     public XMLRequest(int method, String url, Listener<XmlPullParser> listener,  
  6.             ErrorListener errorListener) {  
  7.         super(method, url, errorListener);  
  8.         mListener = listener;  
  9.     }  
  10.   
  11.     public XMLRequest(String url, Listener<XmlPullParser> listener, ErrorListener errorListener) {  
  12.         this(Method.GET, url, listener, errorListener);  
  13.     }  
  14.   
  15.     @Override  
  16.     protected Response<XmlPullParser> parseNetworkResponse(NetworkResponse response) {  
  17.         try {  
  18.             String xmlString = new String(response.data,  
  19.                     HttpHeaderParser.parseCharset(response.headers));  
  20.             XmlPullParserFactory factory = XmlPullParserFactory.newInstance();  
  21.             XmlPullParser xmlPullParser = factory.newPullParser();  
  22.             xmlPullParser.setInput(new StringReader(xmlString));  
  23.             return Response.success(xmlPullParser, HttpHeaderParser.parseCacheHeaders(response));  
  24.         } catch (UnsupportedEncodingException e) {  
  25.             return Response.error(new ParseError(e));  
  26.         } catch (XmlPullParserException e) {  
  27.             return Response.error(new ParseError(e));  
  28.         }  
  29.     }  
  30.   
  31.     @Override  
  32.     protected void deliverResponse(XmlPullParser response) {  
  33.         mListener.onResponse(response);  
  34.     }  
  35.   
  36. }  

可以看到,其實並沒有什麼太多的邏輯,基本都是仿照StringRequest寫下來的,XMLRequest也是繼承自Request類的,只不過這裏指定的泛型類是XmlPullParser,說明我們準備使用Pull解析的方式來解析XML。在parseNetworkResponse()方法中,先是將服務器響應的數據解析成一個字符串,然後設置到XmlPullParser對象中,在deliverResponse()方法中則是將XmlPullParser對象進行回調。

好了,就是這麼簡單,下面我們嘗試使用這個XMLRequest來請求一段XML格式的數據。http://flash.weather.com.cn/wmaps/xml/china.xml這個接口會將中國所有的省份數據以XML格式進行返回,如下所示:

[html] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. <china dn="day" slick-uniqueid="3">  
  2. <city quName="黑龍江" pyName="heilongjiang" cityname="哈爾濱" state1="0" state2="0" stateDetailed="晴" tem1="18" tem2="6" windState="西北風3-4級轉西風小於3級"/>  
  3. <city quName="吉林" pyName="jilin" cityname="長春" state1="0" state2="0" stateDetailed="晴" tem1="19" tem2="6" windState="西北風3-4級轉小於3級"/>  
  4. <city quName="遼寧" pyName="liaoning" cityname="瀋陽" state1="0" state2="0" stateDetailed="晴" tem1="21" tem2="7" windState="東北風3-4級"/>  
  5. <city quName="海南" pyName="hainan" cityname="海口" state1="1" state2="1" stateDetailed="多雲" tem1="30" tem2="24" windState="微風"/>  
  6. <city quName="內蒙古" pyName="neimenggu" cityname="呼和浩特" state1="0" state2="0" stateDetailed="晴" tem1="19" tem2="5" windState="東風3-4級"/>  
  7. <city quName="新疆" pyName="xinjiang" cityname="烏魯木齊" state1="0" state2="0" stateDetailed="晴" tem1="22" tem2="10" windState="微風轉東南風小於3級"/>  
  8. <city quName="西藏" pyName="xizang" cityname="拉薩" state1="1" state2="7" stateDetailed="多雲轉小雨" tem1="18" tem2="4" windState="微風"/>  
  9. <city quName="青海" pyName="qinghai" cityname="西寧" state1="0" state2="1" stateDetailed="晴轉多雲" tem1="18" tem2="2" windState="微風"/>  
  10. <city quName="寧夏" pyName="ningxia" cityname="銀川" state1="0" state2="0" stateDetailed="晴" tem1="19" tem2="8" windState="微風"/>  
  11. <city quName="甘肅" pyName="gansu" cityname="蘭州" state1="0" state2="0" stateDetailed="晴" tem1="21" tem2="6" windState="微風"/>  
  12. <city quName="河北" pyName="hebei" cityname="石家莊" state1="0" state2="0" stateDetailed="晴" tem1="25" tem2="12" windState="北風小於3級"/>  
  13. <city quName="河南" pyName="henan" cityname="鄭州" state1="0" state2="0" stateDetailed="晴" tem1="24" tem2="13" windState="微風"/>  
  14. <city quName="湖北" pyName="hubei" cityname="武漢" state1="0" state2="0" stateDetailed="晴" tem1="24" tem2="12" windState="微風"/>  
  15. <city quName="湖南" pyName="hunan" cityname="長沙" state1="2" state2="1" stateDetailed="陰轉多雲" tem1="20" tem2="15" windState="北風小於3級"/>  
  16. <city quName="山東" pyName="shandong" cityname="濟南" state1="1" state2="1" stateDetailed="多雲" tem1="20" tem2="10" windState="北風3-4級轉小於3級"/>  
  17. <city quName="江蘇" pyName="jiangsu" cityname="南京" state1="2" state2="2" stateDetailed="陰" tem1="19" tem2="13" windState="西北風4-5級轉3-4級"/>  
  18. <city quName="安徽" pyName="anhui" cityname="合肥" state1="2" state2="1" stateDetailed="陰轉多雲" tem1="20" tem2="12" windState="西北風轉北風3-4級"/>  
  19. <city quName="山西" pyName="shanxi" cityname="太原" state1="0" state2="0" stateDetailed="晴" tem1="22" tem2="8" windState="微風"/>  
  20. <city quName="陝西" pyName="sanxi" cityname="西安" state1="1" state2="0" stateDetailed="多雲轉晴" tem1="21" tem2="9" windState="東北風小於3級"/>  
  21. <city quName="四川" pyName="sichuan" cityname="成都" state1="1" state2="1" stateDetailed="多雲" tem1="26" tem2="15" windState="南風小於3級"/>  
  22. <city quName="雲南" pyName="yunnan" cityname="昆明" state1="7" state2="7" stateDetailed="小雨" tem1="21" tem2="13" windState="微風"/>  
  23. <city quName="貴州" pyName="guizhou" cityname="貴陽" state1="1" state2="3" stateDetailed="多雲轉陣雨" tem1="21" tem2="11" windState="東風小於3級"/>  
  24. <city quName="浙江" pyName="zhejiang" cityname="杭州" state1="3" state2="1" stateDetailed="陣雨轉多雲" tem1="22" tem2="14" windState="微風"/>  
  25. <city quName="福建" pyName="fujian" cityname="福州" state1="1" state2="2" stateDetailed="多雲轉陰" tem1="28" tem2="18" windState="微風"/>  
  26. <city quName="江西" pyName="jiangxi" cityname="南昌" state1="2" state2="1" stateDetailed="陰轉多雲" tem1="23" tem2="15" windState="北風3-4級轉微風"/>  
  27. <city quName="廣東" pyName="guangdong" cityname="廣州" state1="3" state2="2" stateDetailed="陣雨轉陰" tem1="26" tem2="20" windState="微風"/>  
  28. <city quName="廣西" pyName="guangxi" cityname="南寧" state1="3" state2="3" stateDetailed="陣雨" tem1="23" tem2="19" windState="東北風小於3級"/>  
  29. <city quName="北京" pyName="beijing" cityname="北京" state1="0" state2="0" stateDetailed="晴" tem1="26" tem2="10" windState="微風"/>  
  30. <city quName="天津" pyName="tianjin" cityname="天津" state1="1" state2="0" stateDetailed="多雲轉晴" tem1="22" tem2="13" windState="東北風3-4級轉小於3級"/>  
  31. <city quName="上海" pyName="shanghai" cityname="上海" state1="7" state2="1" stateDetailed="小雨轉多雲" tem1="20" tem2="16" windState="西北風3-4級"/>  
  32. <city quName="重慶" pyName="chongqing" cityname="重慶" state1="1" state2="3" stateDetailed="多雲轉陣雨" tem1="21" tem2="14" windState="微風"/>  
  33. <city quName="香港" pyName="xianggang" cityname="香港" state1="3" state2="1" stateDetailed="陣雨轉多雲" tem1="26" tem2="22" windState="微風"/>  
  34. <city quName="澳門" pyName="aomen" cityname="澳門" state1="3" state2="1" stateDetailed="陣雨轉多雲" tem1="27" tem2="22" windState="東北風3-4級轉微風"/>  
  35. <city quName="臺灣" pyName="taiwan" cityname="臺北" state1="9" state2="7" stateDetailed="大雨轉小雨" tem1="28" tem2="21" windState="微風"/>  
  36. <city quName="西沙" pyName="xisha" cityname="西沙" state1="3" state2="3" stateDetailed="陣雨" tem1="30" tem2="26" windState="東北風4-5級"/>  
  37. <city quName="南沙" pyName="nanshadao" cityname="南沙" state1="1" state2="1" stateDetailed="多雲" tem1="32" tem2="27" windState="東風4-5級"/>  
  38. <city quName="釣魚島" pyName="diaoyudao" cityname="釣魚島" state1="7" state2="1" stateDetailed="小雨轉多雲" tem1="23" tem2="19" windState="西南風3-4級轉北風5-6級"/>  
  39. </china>  
確定了訪問接口後,我們只需要在代碼中按照以下的方式來使用XMLRequest即可:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. XMLRequest xmlRequest = new XMLRequest(  
  2.         "http://flash.weather.com.cn/wmaps/xml/china.xml",  
  3.         new Response.Listener<XmlPullParser>() {  
  4.             @Override  
  5.             public void onResponse(XmlPullParser response) {  
  6.                 try {  
  7.                     int eventType = response.getEventType();  
  8.                     while (eventType != XmlPullParser.END_DOCUMENT) {  
  9.                         switch (eventType) {  
  10.                         case XmlPullParser.START_TAG:  
  11.                             String nodeName = response.getName();  
  12.                             if ("city".equals(nodeName)) {  
  13.                                 String pName = response.getAttributeValue(0);  
  14.                                 Log.d("TAG""pName is " + pName);  
  15.                             }  
  16.                             break;  
  17.                         }  
  18.                         eventType = response.next();  
  19.                     }  
  20.                 } catch (XmlPullParserException e) {  
  21.                     e.printStackTrace();  
  22.                 } catch (IOException e) {  
  23.                     e.printStackTrace();  
  24.                 }  
  25.             }  
  26.         }, new Response.ErrorListener() {  
  27.             @Override  
  28.             public void onErrorResponse(VolleyError error) {  
  29.                 Log.e("TAG", error.getMessage(), error);  
  30.             }  
  31.         });  
  32. mQueue.add(xmlRequest);  
可以看到,這裏XMLRequest的用法和StringRequest幾乎是一模一樣的,我們先創建出一個XMLRequest的實例,並把服務器接口地址傳入,然後在onResponse()方法中解析響應的XML數據,並把每個省的名字打印出來,最後將這個XMLRequest添加到RequestQueue當中。

現在運行一下代碼,觀察控制檯日誌,就可以看到每個省的名字都從XML中解析出來了,如下圖所示。

2. 自定義GsonRequest

JsonRequest的數據解析是利用Android本身自帶的JSONObject和JSONArray來實現的,配合使用JSONObject和JSONArray就可以解析出任意格式的JSON數據。不過也許你會覺得使用JSONObject還是太麻煩了,還有很多方法可以讓JSON數據解析變得更加簡單,比如說GSON。遺憾的是,Volley中默認並不支持使用自家的GSON來解析數據,不過沒有關係,通過上面的學習,相信你已經知道了自定義一個Request是多麼的簡單,那麼下面我們就來舉一反三一下,自定義一個GsonRequest。

首先我們需要把gson的jar包添加到項目當中,jar包的下載地址是:https://code.google.com/p/google-gson/downloads/list 。

接着定義一個GsonRequest繼承自Request,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class GsonRequest<T> extends Request<T> {  
  2.   
  3.     private final Listener<T> mListener;  
  4.   
  5.     private Gson mGson;  
  6.   
  7.     private Class<T> mClass;  
  8.   
  9.     public GsonRequest(int method, String url, Class<T> clazz, Listener<T> listener,  
  10.             ErrorListener errorListener) {  
  11.         super(method, url, errorListener);  
  12.         mGson = new Gson();  
  13.         mClass = clazz;  
  14.         mListener = listener;  
  15.     }  
  16.   
  17.     public GsonRequest(String url, Class<T> clazz, Listener<T> listener,  
  18.             ErrorListener errorListener) {  
  19.         this(Method.GET, url, clazz, listener, errorListener);  
  20.     }  
  21.   
  22.     @Override  
  23.     protected Response<T> parseNetworkResponse(NetworkResponse response) {  
  24.         try {  
  25.             String jsonString = new String(response.data,  
  26.                     HttpHeaderParser.parseCharset(response.headers));  
  27.             return Response.success(mGson.fromJson(jsonString, mClass),  
  28.                     HttpHeaderParser.parseCacheHeaders(response));  
  29.         } catch (UnsupportedEncodingException e) {  
  30.             return Response.error(new ParseError(e));  
  31.         }  
  32.     }  
  33.   
  34.     @Override  
  35.     protected void deliverResponse(T response) {  
  36.         mListener.onResponse(response);  
  37.     }  
  38.   
  39. }  
可以看到,GsonRequest是繼承自Request類的,並且同樣提供了兩個構造函數。在parseNetworkResponse()方法中,先是將服務器響應的數據解析出來,然後通過調用Gson的fromJson方法將數據組裝成對象。在deliverResponse方法中仍然是將最終的數據進行回調。

那麼下面我們就來測試一下這個GsonRequest能不能夠正常工作吧,調用http://www.weather.com.cn/data/sk/101010100.html這個接口可以得到一段JSON格式的天氣數據,如下所示:

[plain] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. {"weatherinfo":{"city":"北京","cityid":"101010100","temp":"19","WD":"南風","WS":"2級","SD":"43%","WSE":"2","time":"19:45","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}  
接下來我們使用對象的方式將這段JSON字符串表示出來。新建一個Weather類,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class Weather {  
  2.   
  3.     private WeatherInfo weatherinfo;  
  4.   
  5.     public WeatherInfo getWeatherinfo() {  
  6.         return weatherinfo;  
  7.     }  
  8.   
  9.     public void setWeatherinfo(WeatherInfo weatherinfo) {  
  10.         this.weatherinfo = weatherinfo;  
  11.     }  
  12.   
  13. }  
Weather類中只是引用了WeatherInfo這個類。接着新建WeatherInfo類,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class WeatherInfo {  
  2.   
  3.     private String city;  
  4.   
  5.     private String temp;  
  6.   
  7.     private String time;  
  8.   
  9.     public String getCity() {  
  10.         return city;  
  11.     }  
  12.   
  13.     public void setCity(String city) {  
  14.         this.city = city;  
  15.     }  
  16.   
  17.     public String getTemp() {  
  18.         return temp;  
  19.     }  
  20.   
  21.     public void setTemp(String temp) {  
  22.         this.temp = temp;  
  23.     }  
  24.   
  25.     public String getTime() {  
  26.         return time;  
  27.     }  
  28.   
  29.     public void setTime(String time) {  
  30.         this.time = time;  
  31.     }  
  32.   
  33. }  
WeatherInfo類中含有city、temp、time這幾個字段。下面就是如何調用GsonRequest了,其實也很簡單,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. GsonRequest<Weather> gsonRequest = new GsonRequest<Weather>(  
  2.         "http://www.weather.com.cn/data/sk/101010100.html", Weather.class,  
  3.         new Response.Listener<Weather>() {  
  4.             @Override  
  5.             public void onResponse(Weather weather) {  
  6.                 WeatherInfo weatherInfo = weather.getWeatherinfo();  
  7.                 Log.d("TAG""city is " + weatherInfo.getCity());  
  8.                 Log.d("TAG""temp is " + weatherInfo.getTemp());  
  9.                 Log.d("TAG""time is " + weatherInfo.getTime());  
  10.             }  
  11.         }, new Response.ErrorListener() {  
  12.             @Override  
  13.             public void onErrorResponse(VolleyError error) {  
  14.                 Log.e("TAG", error.getMessage(), error);  
  15.             }  
  16.         });  
  17. mQueue.add(gsonRequest);  
可以看到,這裏onResponse()方法的回調中直接返回了一個Weather對象,我們通過它就可以得到WeatherInfo對象,接着就能從中取出JSON中的相關數據了。現在運行一下代碼,觀察控制檯日誌,打印數據如下圖所示:

這樣的話,XMLRequest和GsonRequest的功能就基本都實現了,我們也是藉助這兩個例子深刻地理解了自定義Request的方法,對Volley的認識也是更加深入了。好了,本篇文章就到此結束,下篇文章中我們將對Volley進行更深層次的研究



Android Volley完全解析(四),帶你從源碼的角度理解Volley

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/17656437

經過前三篇文章的學習,Volley的用法我們已經掌握的差不多了,但是對於Volley的工作原理,恐怕有很多朋友還不是很清楚。因此,本篇文章中我們就來一起閱讀一下Volley的源碼,將它的工作流程整體地梳理一遍。同時,這也是Volley系列的最後一篇文章了。

其實,Volley的官方文檔中本身就附有了一張Volley的工作流程圖,如下圖所示。


多數朋友突然看到一張這樣的圖,應該會和我一樣,感覺一頭霧水吧?沒錯,目前我們對Volley背後的工作原理還沒有一個概念性的理解,直接就來看這張圖自然會有些吃力。不過沒關係,下面我們就去分析一下Volley的源碼,之後再重新來看這張圖就會好理解多了。

說起分析源碼,那麼應該從哪兒開始看起呢?這就要回顧一下Volley的用法了,還記得嗎,使用Volley的第一步,首先要調用Volley.newRequestQueue(context)方法來獲取一個RequestQueue對象,那麼我們自然要從這個方法開始看起了,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public static RequestQueue newRequestQueue(Context context) {  
  2.     return newRequestQueue(context, null);  
  3. }  
這個方法僅僅只有一行代碼,只是調用了newRequestQueue()的方法重載,並給第二個參數傳入null。那我們看下帶有兩個參數的newRequestQueue()方法中的代碼,如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public static RequestQueue newRequestQueue(Context context, HttpStack stack) {  
  2.     File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);  
  3.     String userAgent = "volley/0";  
  4.     try {  
  5.         String packageName = context.getPackageName();  
  6.         PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);  
  7.         userAgent = packageName + "/" + info.versionCode;  
  8.     } catch (NameNotFoundException e) {  
  9.     }  
  10.     if (stack == null) {  
  11.         if (Build.VERSION.SDK_INT >= 9) {  
  12.             stack = new HurlStack();  
  13.         } else {  
  14.             stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));  
  15.         }  
  16.     }  
  17.     Network network = new BasicNetwork(stack);  
  18.     RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);  
  19.     queue.start();  
  20.     return queue;  
  21. }  
可以看到,這裏在第10行判斷如果stack是等於null的,則去創建一個HttpStack對象,這裏會判斷如果手機系統版本號是大於9的,則創建一個HurlStack的實例,否則就創建一個HttpClientStack的實例。實際上HurlStack的內部就是使用HttpURLConnection進行網絡通訊的,而HttpClientStack的內部則是使用HttpClient進行網絡通訊的,這裏爲什麼這樣選擇呢?可以參考我之前翻譯的一篇文章Android訪問網絡,使用HttpURLConnection還是HttpClient?

創建好了HttpStack之後,接下來又創建了一個Network對象,它是用於根據傳入的HttpStack對象來處理網絡請求的,緊接着new出一個RequestQueue對象,並調用它的start()方法進行啓動,然後將RequestQueue返回,這樣newRequestQueue()的方法就執行結束了。

那麼RequestQueue的start()方法內部到底執行了什麼東西呢?我們跟進去瞧一瞧:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public void start() {  
  2.     stop();  // Make sure any currently running dispatchers are stopped.  
  3.     // Create the cache dispatcher and start it.  
  4.     mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);  
  5.     mCacheDispatcher.start();  
  6.     // Create network dispatchers (and corresponding threads) up to the pool size.  
  7.     for (int i = 0; i < mDispatchers.length; i++) {  
  8.         NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,  
  9.                 mCache, mDelivery);  
  10.         mDispatchers[i] = networkDispatcher;  
  11.         networkDispatcher.start();  
  12.     }  
  13. }  
這裏先是創建了一個CacheDispatcher的實例,然後調用了它的start()方法,接着在一個for循環裏去創建NetworkDispatcher的實例,並分別調用它們的start()方法。這裏的CacheDispatcher和NetworkDispatcher都是繼承自Thread的,而默認情況下for循環會執行四次,也就是說當調用了Volley.newRequestQueue(context)之後,就會有五個線程一直在後臺運行,不斷等待網絡請求的到來,其中CacheDispatcher是緩存線程,NetworkDispatcher是網絡請求線程。

得到了RequestQueue之後,我們只需要構建出相應的Request,然後調用RequestQueue的add()方法將Request傳入就可以完成網絡請求操作了,那麼不用說,add()方法的內部肯定有着非常複雜的邏輯,我們來一起看一下:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public <T> Request<T> add(Request<T> request) {  
  2.     // Tag the request as belonging to this queue and add it to the set of current requests.  
  3.     request.setRequestQueue(this);  
  4.     synchronized (mCurrentRequests) {  
  5.         mCurrentRequests.add(request);  
  6.     }  
  7.     // Process requests in the order they are added.  
  8.     request.setSequence(getSequenceNumber());  
  9.     request.addMarker("add-to-queue");  
  10.     // If the request is uncacheable, skip the cache queue and go straight to the network.  
  11.     if (!request.shouldCache()) {  
  12.         mNetworkQueue.add(request);  
  13.         return request;  
  14.     }  
  15.     // Insert request into stage if there's already a request with the same cache key in flight.  
  16.     synchronized (mWaitingRequests) {  
  17.         String cacheKey = request.getCacheKey();  
  18.         if (mWaitingRequests.containsKey(cacheKey)) {  
  19.             // There is already a request in flight. Queue up.  
  20.             Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);  
  21.             if (stagedRequests == null) {  
  22.                 stagedRequests = new LinkedList<Request<?>>();  
  23.             }  
  24.             stagedRequests.add(request);  
  25.             mWaitingRequests.put(cacheKey, stagedRequests);  
  26.             if (VolleyLog.DEBUG) {  
  27.                 VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);  
  28.             }  
  29.         } else {  
  30.             // Insert 'null' queue for this cacheKey, indicating there is now a request in  
  31.             // flight.  
  32.             mWaitingRequests.put(cacheKey, null);  
  33.             mCacheQueue.add(request);  
  34.         }  
  35.         return request;  
  36.     }  
  37. }  
可以看到,在第11行的時候會判斷當前的請求是否可以緩存,如果不能緩存則在第12行直接將這條請求加入網絡請求隊列,可以緩存的話則在第33行將這條請求加入緩存隊列。在默認情況下,每條請求都是可以緩存的,當然我們也可以調用Request的setShouldCache(false)方法來改變這一默認行爲。

OK,那麼既然默認每條請求都是可以緩存的,自然就被添加到了緩存隊列中,於是一直在後臺等待的緩存線程就要開始運行起來了,我們看下CacheDispatcher中的run()方法,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class CacheDispatcher extends Thread {  
  2.   
  3.     ……  
  4.   
  5.     @Override  
  6.     public void run() {  
  7.         if (DEBUG) VolleyLog.v("start new dispatcher");  
  8.         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  9.         // Make a blocking call to initialize the cache.  
  10.         mCache.initialize();  
  11.         while (true) {  
  12.             try {  
  13.                 // Get a request from the cache triage queue, blocking until  
  14.                 // at least one is available.  
  15.                 final Request<?> request = mCacheQueue.take();  
  16.                 request.addMarker("cache-queue-take");  
  17.                 // If the request has been canceled, don't bother dispatching it.  
  18.                 if (request.isCanceled()) {  
  19.                     request.finish("cache-discard-canceled");  
  20.                     continue;  
  21.                 }  
  22.                 // Attempt to retrieve this item from cache.  
  23.                 Cache.Entry entry = mCache.get(request.getCacheKey());  
  24.                 if (entry == null) {  
  25.                     request.addMarker("cache-miss");  
  26.                     // Cache miss; send off to the network dispatcher.  
  27.                     mNetworkQueue.put(request);  
  28.                     continue;  
  29.                 }  
  30.                 // If it is completely expired, just send it to the network.  
  31.                 if (entry.isExpired()) {  
  32.                     request.addMarker("cache-hit-expired");  
  33.                     request.setCacheEntry(entry);  
  34.                     mNetworkQueue.put(request);  
  35.                     continue;  
  36.                 }  
  37.                 // We have a cache hit; parse its data for delivery back to the request.  
  38.                 request.addMarker("cache-hit");  
  39.                 Response<?> response = request.parseNetworkResponse(  
  40.                         new NetworkResponse(entry.data, entry.responseHeaders));  
  41.                 request.addMarker("cache-hit-parsed");  
  42.                 if (!entry.refreshNeeded()) {  
  43.                     // Completely unexpired cache hit. Just deliver the response.  
  44.                     mDelivery.postResponse(request, response);  
  45.                 } else {  
  46.                     // Soft-expired cache hit. We can deliver the cached response,  
  47.                     // but we need to also send the request to the network for  
  48.                     // refreshing.  
  49.                     request.addMarker("cache-hit-refresh-needed");  
  50.                     request.setCacheEntry(entry);  
  51.                     // Mark the response as intermediate.  
  52.                     response.intermediate = true;  
  53.                     // Post the intermediate response back to the user and have  
  54.                     // the delivery then forward the request along to the network.  
  55.                     mDelivery.postResponse(request, response, new Runnable() {  
  56.                         @Override  
  57.                         public void run() {  
  58.                             try {  
  59.                                 mNetworkQueue.put(request);  
  60.                             } catch (InterruptedException e) {  
  61.                                 // Not much we can do about this.  
  62.                             }  
  63.                         }  
  64.                     });  
  65.                 }  
  66.             } catch (InterruptedException e) {  
  67.                 // We may have been interrupted because it was time to quit.  
  68.                 if (mQuit) {  
  69.                     return;  
  70.                 }  
  71.                 continue;  
  72.             }  
  73.         }  
  74.     }  
  75. }  
代碼有點長,我們只挑重點看。首先在11行可以看到一個while(true)循環,說明緩存線程始終是在運行的,接着在第23行會嘗試從緩存當中取出響應結果,如何爲空的話則把這條請求加入到網絡請求隊列中,如果不爲空的話再判斷該緩存是否已過期,如果已經過期了則同樣把這條請求加入到網絡請求隊列中,否則就認爲不需要重發網絡請求,直接使用緩存中的數據即可。之後會在第39行調用Request的parseNetworkResponse()方法來對數據進行解析,再往後就是將解析出來的數據進行回調了,這部分代碼我們先跳過,因爲它的邏輯和NetworkDispatcher後半部分的邏輯是基本相同的,那麼我們等下合併在一起看就好了,先來看一下NetworkDispatcher中是怎麼處理網絡請求隊列的,代碼如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class NetworkDispatcher extends Thread {  
  2.     ……  
  3.     @Override  
  4.     public void run() {  
  5.         Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);  
  6.         Request<?> request;  
  7.         while (true) {  
  8.             try {  
  9.                 // Take a request from the queue.  
  10.                 request = mQueue.take();  
  11.             } catch (InterruptedException e) {  
  12.                 // We may have been interrupted because it was time to quit.  
  13.                 if (mQuit) {  
  14.                     return;  
  15.                 }  
  16.                 continue;  
  17.             }  
  18.             try {  
  19.                 request.addMarker("network-queue-take");  
  20.                 // If the request was cancelled already, do not perform the  
  21.                 // network request.  
  22.                 if (request.isCanceled()) {  
  23.                     request.finish("network-discard-cancelled");  
  24.                     continue;  
  25.                 }  
  26.                 addTrafficStatsTag(request);  
  27.                 // Perform the network request.  
  28.                 NetworkResponse networkResponse = mNetwork.performRequest(request);  
  29.                 request.addMarker("network-http-complete");  
  30.                 // If the server returned 304 AND we delivered a response already,  
  31.                 // we're done -- don't deliver a second identical response.  
  32.                 if (networkResponse.notModified && request.hasHadResponseDelivered()) {  
  33.                     request.finish("not-modified");  
  34.                     continue;  
  35.                 }  
  36.                 // Parse the response here on the worker thread.  
  37.                 Response<?> response = request.parseNetworkResponse(networkResponse);  
  38.                 request.addMarker("network-parse-complete");  
  39.                 // Write to cache if applicable.  
  40.                 // TODO: Only update cache metadata instead of entire record for 304s.  
  41.                 if (request.shouldCache() && response.cacheEntry != null) {  
  42.                     mCache.put(request.getCacheKey(), response.cacheEntry);  
  43.                     request.addMarker("network-cache-written");  
  44.                 }  
  45.                 // Post the response back.  
  46.                 request.markDelivered();  
  47.                 mDelivery.postResponse(request, response);  
  48.             } catch (VolleyError volleyError) {  
  49.                 parseAndDeliverNetworkError(request, volleyError);  
  50.             } catch (Exception e) {  
  51.                 VolleyLog.e(e, "Unhandled exception %s", e.toString());  
  52.                 mDelivery.postError(request, new VolleyError(e));  
  53.             }  
  54.         }  
  55.     }  
  56. }  
同樣地,在第7行我們看到了類似的while(true)循環,說明網絡請求線程也是在不斷運行的。在第28行的時候會調用Network的performRequest()方法來去發送網絡請求,而Network是一個接口,這裏具體的實現是BasicNetwork,我們來看下它的performRequest()方法,如下所示:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public class BasicNetwork implements Network {  
  2.     ……  
  3.     @Override  
  4.     public NetworkResponse performRequest(Request<?> request) throws VolleyError {  
  5.         long requestStart = SystemClock.elapsedRealtime();  
  6.         while (true) {  
  7.             HttpResponse httpResponse = null;  
  8.             byte[] responseContents = null;  
  9.             Map<String, String> responseHeaders = new HashMap<String, String>();  
  10.             try {  
  11.                 // Gather headers.  
  12.                 Map<String, String> headers = new HashMap<String, String>();  
  13.                 addCacheHeaders(headers, request.getCacheEntry());  
  14.                 httpResponse = mHttpStack.performRequest(request, headers);  
  15.                 StatusLine statusLine = httpResponse.getStatusLine();  
  16.                 int statusCode = statusLine.getStatusCode();  
  17.                 responseHeaders = convertHeaders(httpResponse.getAllHeaders());  
  18.                 // Handle cache validation.  
  19.                 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {  
  20.                     return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,  
  21.                             request.getCacheEntry() == null ? null : request.getCacheEntry().data,  
  22.                             responseHeaders, true);  
  23.                 }  
  24.                 // Some responses such as 204s do not have content.  We must check.  
  25.                 if (httpResponse.getEntity() != null) {  
  26.                   responseContents = entityToBytes(httpResponse.getEntity());  
  27.                 } else {  
  28.                   // Add 0 byte response as a way of honestly representing a  
  29.                   // no-content request.  
  30.                   responseContents = new byte[0];  
  31.                 }  
  32.                 // if the request is slow, log it.  
  33.                 long requestLifetime = SystemClock.elapsedRealtime() - requestStart;  
  34.                 logSlowRequests(requestLifetime, request, responseContents, statusLine);  
  35.                 if (statusCode < 200 || statusCode > 299) {  
  36.                     throw new IOException();  
  37.                 }  
  38.                 return new NetworkResponse(statusCode, responseContents, responseHeaders, false);  
  39.             } catch (Exception e) {  
  40.                 ……  
  41.             }  
  42.         }  
  43.     }  
  44. }  

這段方法中大多都是一些網絡請求細節方面的東西,我們並不需要太多關心,需要注意的是在第14行調用了HttpStack的performRequest()方法,這裏的HttpStack就是在一開始調用newRequestQueue()方法是創建的實例,默認情況下如果系統版本號大於9就創建的HurlStack對象,否則創建HttpClientStack對象。前面已經說過,這兩個對象的內部實際就是分別使用HttpURLConnection和HttpClient來發送網絡請求的,我們就不再跟進去閱讀了,之後會將服務器返回的數據組裝成一個NetworkResponse對象進行返回。

在NetworkDispatcher中收到了NetworkResponse這個返回值後又會調用Request的parseNetworkResponse()方法來解析NetworkResponse中的數據,以及將數據寫入到緩存,這個方法的實現是交給Request的子類來完成的,因爲不同種類的Request解析的方式也肯定不同。還記得我們在上一篇文章中學習的自定義Request的方式嗎?其中parseNetworkResponse()這個方法就是必須要重寫的。

在解析完了NetworkResponse中的數據之後,又會調用ExecutorDelivery的postResponse()方法來回調解析出的數據,代碼如下所示:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {  
  2.     request.markDelivered();  
  3.     request.addMarker("post-response");  
  4.     mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));  
  5. }  
其中,在mResponsePoster的execute()方法中傳入了一個ResponseDeliveryRunnable對象,就可以保證該對象中的run()方法就是在主線程當中運行的了,我們看下run()方法中的代碼是什麼樣的:
[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. private class ResponseDeliveryRunnable implements Runnable {  
  2.     private final Request mRequest;  
  3.     private final Response mResponse;  
  4.     private final Runnable mRunnable;  
  5.   
  6.     public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {  
  7.         mRequest = request;  
  8.         mResponse = response;  
  9.         mRunnable = runnable;  
  10.     }  
  11.   
  12.     @SuppressWarnings("unchecked")  
  13.     @Override  
  14.     public void run() {  
  15.         // If this request has canceled, finish it and don't deliver.  
  16.         if (mRequest.isCanceled()) {  
  17.             mRequest.finish("canceled-at-delivery");  
  18.             return;  
  19.         }  
  20.         // Deliver a normal response or error, depending.  
  21.         if (mResponse.isSuccess()) {  
  22.             mRequest.deliverResponse(mResponse.result);  
  23.         } else {  
  24.             mRequest.deliverError(mResponse.error);  
  25.         }  
  26.         // If this is an intermediate response, add a marker, otherwise we're done  
  27.         // and the request can be finished.  
  28.         if (mResponse.intermediate) {  
  29.             mRequest.addMarker("intermediate-response");  
  30.         } else {  
  31.             mRequest.finish("done");  
  32.         }  
  33.         // If we have been provided a post-delivery runnable, run it.  
  34.         if (mRunnable != null) {  
  35.             mRunnable.run();  
  36.         }  
  37.    }  
  38. }  

代碼雖然不多,但我們並不需要行行閱讀,抓住重點看即可。其中在第22行調用了Request的deliverResponse()方法,有沒有感覺很熟悉?沒錯,這個就是我們在自定義Request時需要重寫的另外一個方法,每一條網絡請求的響應都是回調到這個方法中,最後我們再在這個方法中將響應的數據回調到Response.Listener的onResponse()方法中就可以了。

好了,到這裏我們就把Volley的完整執行流程全部梳理了一遍,你是不是已經感覺已經很清晰了呢?對了,還記得在文章一開始的那張流程圖嗎,剛纔還不能理解,現在我們再來重新看下這張圖:


其中藍色部分代表主線程,綠色部分代表緩存線程,橙色部分代表網絡線程。我們在主線程中調用RequestQueue的add()方法來添加一條網絡請求,這條請求會先被加入到緩存隊列當中,如果發現可以找到相應的緩存結果就直接讀取緩存並解析,然後回調給主線程。如果在緩存中沒有找到結果,則將這條請求加入到網絡請求隊列中,然後處理髮送HTTP請求,解析響應結果,寫入緩存,並回調主線程。

怎麼樣,是不是感覺現在理解這張圖已經變得輕鬆簡單了?好了,到此爲止我們就把Volley的用法和源碼全部學習完了,相信你已經對Volley非常熟悉並可以將它應用到實際項目當中了,那麼Volley完全解析系列的文章到此結束,感謝大家有耐心看到最後。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章