Volley
Volley 是 Google 推出的 Android 異步網絡請求框架和圖片加載框架。在 Google I/O 2013 大會上發佈。
從名字由來和配圖中無數急促的火箭可以看出 Volley 的特點:特別適合數據量小,通信頻繁的網絡操作
1. Volley引入的背景
在以前,我們可能面臨如下很多麻煩的問題。
比如以前從網上下載圖片的步驟可能是這樣的流程:
- 在ListAdapter#getView()裏開始圖像的讀取。
- 通過AsyncTask等機制使用HttpURLConnection從服務器去的圖片資源
- 在AsyncTask#onPostExecute()裏設置相應ImageView的屬性。
而在Volley下,只需要一個函數即可,詳細見後面的例子。
再比如,屏幕旋轉的時候,有時候會導致再次從網絡取得數據。爲了避免這種不必要的網絡訪問,我們可能需要自己寫很多針對各種情況的處理,比如cache什麼的。
再有,比如ListView的時候,我們滾動過快,可能導致有些網絡請求返回的時候,早已經滾過了當時的位置,根本沒必要顯示在list裏了,雖然我們可以通過ViewHolder來保持url等來實現防止兩次取得,但是那些已經沒有必須要的數據,還是會浪費系統的各種資源。
2、Volley提供的功能
簡單來說,它提供瞭如下的便利功能:
- JSON,圖像等的異步下載;
- 網絡請求的排序(scheduling)
- 網絡請求的優先級處理
- 緩存
- 多級別取消請求
- 和Activity和生命週期的聯動(Activity結束時同時取消所有網絡請求)
2.1 使用前的準備
引入Volley非常簡單,首先,從git庫先克隆一個下來:
- git clone https://android.googlesource.com/platform/frameworks/volley
然後編譯爲jar包,再在自己的工程裏import進來。
注意,這個庫要求最低SDK版本爲Froyo,即至少要設置android:minSdkVersion爲8以上。
3. Volley的架構設計
Volley使用了線程池來作爲基礎結構,主要分爲主線程,cache線程和network線程。主線程和cache線程都只有一個,而NetworkDispatcher線程可以有多個,這樣能解決比並行問題。如下圖:
4、Volley 中的概念
簡單介紹一些概念,在詳細設計
中會仔細介紹。
Volley 的調用比較簡單,通過 newRequestQueue(…) 函數新建並啓動一個請求隊列RequestQueue
後,只需要往這個RequestQueue
不斷 add Request 即可。
Volley:Volley 對外暴露的 API,通過 newRequestQueue(…) 函數新建並啓動一個請求隊列RequestQueue
。
Request:表示一個請求的抽象類。StringRequest
、JsonRequest
、ImageRequest
都是它的子類,表示某種類型的請求。
RequestQueue:表示請求隊列,裏面包含一個CacheDispatcher
(用於處理走緩存請求的調度線程)、NetworkDispatcher
數組(用於處理走網絡請求的調度線程),一個ResponseDelivery
(返回結果分發接口),通過 start() 函數啓動時會啓動CacheDispatcher
和NetworkDispatchers
。
CacheDispatcher:一個線程,用於調度處理走緩存的請求。啓動後會不斷從緩存請求隊列中取請求處理,隊列爲空則等待,請求處理結束則將結果傳遞給ResponseDelivery
去執行後續處理。當結果未緩存過、緩存失效或緩存需要刷新的情況下,該請求都需要重新進入NetworkDispatcher
去調度處理。
NetworkDispatcher:一個線程,用於調度處理走網絡的請求。啓動後會不斷從網絡請求隊列中取請求處理,隊列爲空則等待,請求處理結束則將結果傳遞給ResponseDelivery
去執行後續處理,並判斷結果是否要進行緩存。
ResponseDelivery:返回結果分發接口,目前只有基於ExecutorDelivery
的在入參 handler 對應線程內進行分發。
HttpStack:處理 Http 請求,返回請求結果。目前 Volley 中有基於 HttpURLConnection 的HurlStack
和 基於 Apache HttpClient 的HttpClientStack
。
Network:調用HttpStack
處理請求,並將結果轉換爲可被ResponseDelivery
處理的NetworkResponse
。
Cache:緩存請求結果,Volley 默認使用的是基於 sdcard 的DiskBasedCache
。NetworkDispatcher
得到請求結果後判斷是否需要存儲在 Cache,CacheDispatcher
會從 Cache 中取緩存結果。
5、Volley的簡單使用
1.1聲明RequestQueue
- private RequestQueue mRequestQueue;
- mRequestQueue = Volley.newRequestQueue(this);
1.2 聲明並使用Request
- JsonObjectRequest jr = new JsonObjectRequest(Request.Method.GET,url,null,new Response.Listener<JSONObject>() {
- @Override
- public void onResponse(JSONObject response) {
- Log.i(TAG,response.toString());
- parseJSON(response);
- va.notifyDataSetChanged();
- pd.dismiss();
- }
- },new Response.ErrorListener() {
- @Override
- public void onErrorResponse(VolleyError error) {
- Log.i(TAG,error.getMessage());
- }
- });
- mRequestQueue.add(jr);
JsonObjectRequest:返回JSON對象。
JsonArrayRequest:返回JsonArray。
StringRequest:返回String,這樣可以自己處理數據,更加靈活。
另外可以繼承Request<T>自定義Request。
1.3取消Request
1)可以針對某些個request做取消操作:
- @Override
- ublic void onStop() {
- for (Request <?> req : mRequestQueue) {
- req.cancel();
- }
- @Override
- protected void onStop() {
- // TODO Auto-generated method stub
- super.onStop();
- mRequestQueue.cancelAll(this);
- }
- @Override
- protected void onStop() {
- // TODO Auto-generated method stub
- super.onStop();
- mRequestQueue.cancelAll( new RequestFilter() {});
- or
- mRequestQueue.cancelAll(new Object());
- }
2、圖片加載
2.1使用ImageRequest下載圖片
- singleImg=(ImageView)findViewById(R.id.volley_img_single_imgeview);
- ImageRequest imgRequest=new ImageRequest(url, new Response.Listener<Bitmap>() {
- @Override
- public void onResponse(Bitmap arg0) {
- // TODO Auto-generated method stub
- singleImg.setImageBitmap(arg0);
- }
- }, 300, 200, Config.ARGB_8888, new ErrorListener(){
- @Override
- public void onErrorResponse(VolleyError arg0) {
- // TODO Auto-generated method stub
- }
- });
- mRequestQueue.add(imgRequest);
2.2使用ImageLoader
ImageLoader這個類需要一個Request的實例以及一個ImageCache的實例。圖片通過一個URL和一個ImageListener實例的get()方法就可以被加載。從哪裏,ImageLoader會檢查ImageCache,而且如果緩存裏沒有圖片就會從網絡上獲取。
Volley的ImageCache接口允許你使用你喜歡的L1緩存實現。不幸的是Volley沒有提供默認的實現。在I/O的介紹中展示了BitmapLruCache的一點代碼片段,但是Volley這個庫本身並不包含任何相關的實現。
ImageCache接口有兩個方法,getBitmap(String url)和putBitmap(String url, Bitmap bitmap).這兩個方法足夠簡單直白,他們可以添加任何的緩存實現。
- RequestQueue mRequestQueue = Volley.newRequestQueue(this);
- final LruCache<String, Bitmap> mImageCache = new LruCache<String, Bitmap>(
- 20);
- ImageCache imageCache = new ImageCache() {
- @Override
- public void putBitmap(String key, Bitmap value) {
- mImageCache.put(key, value);
- }
- @Override
- public Bitmap getBitmap(String key) {
- return mImageCache.get(key);
- }
- };
- ImageLoader mImageLoader = new ImageLoader(mRequestQueue, imageCache);
- // imageView是一個ImageView實例
- // ImageLoader.getImageListener的第二個參數是默認的圖片resource id
- // 第三個參數是請求失敗時候的資源id,可以指定爲0
- ImageListener listener = ImageLoader
- .getImageListener(imageView, android.R.drawable.ic_menu_rotate,
- android.R.drawable.ic_delete);
- mImageLoader.get(url, listener);
2.3 使用NetworkImageView
- <span style="font-size:12px;">public class NetWorkImageView extends ImageView</span><span style="font-size: 18px;">
- </span>
- public void setImageUrl(String url,ImageLoader imageLoader){}
核心方法:
- private void loadImageIfNecessary(final boolean isInlayoutPass){}
內部實現和ImageLoader類似,都是通過ImageContainer中new一個ImageListener,在ImageListener,只是做了Url的空判斷,如果Url爲null,則調用ImageContainer.cancelRequest();取消請求。
覆寫方法:
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- //onLayout時重新請求
- loadImageIfNecessary(true);
- }
- @Override
- protected void onDetachedFromWindow() {
- //銷燬View的時候Release操作
- if (mImageContainer != null) {
- // If the view was bound to an image request, cancel it and clear
- // out the image from the view.
- mImageContainer.cancelRequest();
- setImageBitmap(null);
- // also clear out the container so we can reload the image if necessary.
- mImageContainer = null;
- }
- super.onDetachedFromWindow();
- }
- //drawable狀態改變重繪
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- invalidate();
- }
- /**
- * Sets the default image resource ID to be used for this view until the attempt to load it
- * completes.
- */
- public void setDefaultImageResId(int defaultImage) {
- mDefaultImageId = defaultImage;
- }
- /**
- * Sets the error image resource ID to be used for this view in the event that the image
- * requested fails to load.
- */
- public void setErrorImageResId(int errorImage) {
- mErrorImageId = errorImage;
- }
6 核心類功能介紹
6.2.1 Volley.java
這個和 Volley 框架同名的類,其實是個工具類,作用是構建一個可用於添加網絡請求的RequestQueue
對象。
(1). 主要函數
Volley.java 有兩個重載的靜態方法。
public static RequestQueue newRequestQueue(Context context)
public static RequestQueue newRequestQueue(Context context, HttpStack stack)
第一個方法的實現調用了第二個方法,傳 HttpStack 參數爲 null。
第二個方法中,如果 HttpStatck 參數爲 null,則如果系統在 Gingerbread 及之後(即 API Level >= 9),採用基於 HttpURLConnection 的 HurlStack,如果小於 9,採用基於 HttpClient 的 HttpClientStack。
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
得到了 HttpStack,然後通過它構造一個代表網絡(Network)的具體實現BasicNetwork
。
接着構造一個代表緩存(Cache)的基於 Disk 的具體實現DiskBasedCache
。
最後將網絡(Network)對象和緩存(Cache)對象傳入構建一個 RequestQueue,啓動這個 RequestQueue,並返回。
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
我們平時大多采用
Volly.newRequestQueue(context)
的默認實現,構建RequestQueue。
通過源碼可以看出,我們可以拋開 Volley 工具類構建自定義的RequestQueue,採用自定義的HttpStatck
,採用自定義的Network
實現,採用自定義的Cache實現等來構建RequestQueue
。
優秀框架的高可拓展性的魅力來源於此啊
(2). HttpURLConnection 和 AndroidHttpClient(HttpClient 的封裝)如何選擇及原因:
在 Froyo(2.2) 之前,HttpURLConnection 有個重大 Bug,調用 close() 函數會影響連接池,導致連接複用失效,所以在 Froyo 之前使用 HttpURLConnection 需要關閉 keepAlive。
另外在 Gingerbread(2.3) HttpURLConnection 默認開啓了 gzip 壓縮,提高了 HTTPS 的性能,Ice Cream Sandwich(4.0) HttpURLConnection 支持了請求結果緩存。
再加上 HttpURLConnection 本身 API 相對簡單,所以對 Android 來說,在 2.3 之後建議使用 HttpURLConnection,之前建議使用 AndroidHttpClient。
(3). 關於 User Agent
通過代碼我們發現如果是使用 AndroidHttpClient,Volley 還會將請求頭中的 User-Agent 字段設置爲 App 的 ${packageName}/${versionCode},如果異常則使用 "volley/0",不過這個獲取 User-Agent 的操作應該放到 if else 內部更合適。而對於 HttpURLConnection 卻沒有任何操作,爲什麼呢?
如果用 Fiddler 或 Charles 對數據抓包我們會發現,我們會發現 HttpURLConnection 默認是有 User-Agent 的,類似:
Dalvik/1.6.0 (Linux; U; Android 4.1.1; Google Nexus 4 - 4.1.1 - API 16 - 768x1280_1 Build/JRO03S)
經常用 WebView 的同學會也許會發現似曾相識,是的,WebView 默認的 User-Agent 也是這個。實際在請求發出之前,會檢測 User-Agent 是否爲空,如果不爲空,則加上系統默認 User-Agent。在 Android 2.1 之後,我們可以通過
String userAgent = System.getProperty("http.agent");
得到系統默認的 User-Agent,Volley 如果希望自定義 User-Agent,可在自定義 Request 中重寫 getHeaders() 函數
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
// self-defined user agent
Map<String, String> headerMap = new HashMap<String, String>();
headerMap.put("User-Agent", "android-open-project-analysis/1.0");
return headerMap;
}
6.2.2 Request.java
代表一個網絡請求的抽象類。我們通過構建一個Request
類的非抽象子類(StringRequest、JsonRequest、ImageRequest或自定義)對象,並將其加入到·RequestQueue·中來完成一次網絡請求操作。
Volley 支持 8 種 Http 請求方式 GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, PATCH
Request 類中包含了請求 url,請求請求方式,請求 Header,請求 Body,請求的優先級等信息。
因爲是抽象類,子類必須重寫的兩個方法。
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
子類重寫此方法,將網絡返回的原生字節內容,轉換成合適的類型。此方法會在工作線程中被調用。
abstract protected void deliverResponse(T response);
子類重寫此方法,將解析成合適類型的內容傳遞給它們的監聽回調。
以下兩個方法也經常會被重寫
public byte[] getBody()
重寫此方法,可以構建用於 POST、PUT、PATCH 請求方式的 Body 內容。
protected Map<String, String> getParams()
在上面getBody
函數沒有被重寫情況下,此方法的返回值會被 key、value 分別編碼後拼裝起來轉換爲字節碼作爲 Body 內容。
6.2.3 RequestQueue.java
Volley 框架的核心類,將請求Request加入到一個運行的RequestQueue
中,來完成請求操作。
(1). 主要成員變量
RequestQueue 中維護了兩個基於優先級的 Request 隊列,緩存請求隊列和網絡請求隊列。
放在緩存請求隊列中的 Request,將通過緩存獲取數據;放在網絡請求隊列中的 Request,將通過網絡獲取數據。
private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<?>>();
private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<Request<?>>();
維護了一個正在進行中,尚未完成的請求集合。
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
維護了一個等待請求的集合,如果一個請求正在被處理並且可以被緩存,後續的相同 url 的請求,將進入此等待隊列。
private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<String, Queue<Request<?>>>();
(2). 啓動隊列
創建出 RequestQueue 以後,調用 start 方法,啓動隊列。
/**
* 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.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
start 方法中,開啓一個緩存調度線程CacheDispatcher
和 n 個網絡調度線程NetworkDispatcher
,這裏 n 默認爲4,存在優化的餘地,比如可以根據 CPU 核數以及網絡類型計算更合適的併發數。
緩存調度線程不斷的從緩存請求隊列中取出 Request 去處理,網絡調度線程不斷的從網絡請求隊列中取出 Request 去處理。
(3). 加入請求
public <T> Request<T> add(Request<T> request);
流程圖如下:
(4). 請求完成
void finish(Request<?> request)
Request 請求結束
(1). 首先從正在進行中請求集合
mCurrentRequests
中移除該請求。
(2). 然後查找請求等待集合mWaitingRequests
中是否存在等待的請求,如果存在,則將等待隊列移除,並將等待隊列所有的請求添加到緩存請求隊列中,讓緩存請求處理線程CacheDispatcher
自動處理。
(5). 請求取消
public void cancelAll(RequestFilter filter)
public void cancelAll(final Object tag)
取消當前請求集合中所有符合條件的請求。
filter 參數表示可以按照自定義的過濾器過濾需要取消的請求。
tag 表示按照Request.setTag
設置好的 tag 取消請求,比如同屬於某個 Activity 的。
6.2.4 CacheDispatcher.java
一個線程,用於調度處理走緩存的請求。啓動後會不斷從緩存請求隊列中取請求處理,隊列爲空則等待,請求處理結束則將結果傳遞給ResponseDelivery
去執行後續處理。當結果未緩存過、緩存失效或緩存需要刷新的情況下,該請求都需要重新進入NetworkDispatcher
去調度處理。
(1). 成員變量
BlockingQueue<Request<?>> mCacheQueue
緩存請求隊列
BlockingQueue<Request<?>> mNetworkQueue
網絡請求隊列
Cache mCache
緩存類,代表了一個可以獲取請求結果,存儲請求結果的緩存
ResponseDelivery mDelivery
請求結果傳遞類
(2). 處理流程圖
6.2.5 NetworkDispatcher.java
一個線程,用於調度處理走網絡的請求。啓動後會不斷從網絡請求隊列中取請求處理,隊列爲空則等待,請求處理結束則將結果傳遞給 ResponseDelivery 去執行後續處理,並判斷結果是否要進行緩存。
(1). 成員變量
BlockingQueue<Request<?>> mQueue
網絡請求隊列
Network mNetwork
網絡類,代表了一個可以執行請求的網絡
Cache mCache
緩存類,代表了一個可以獲取請求結果,存儲請求結果的緩存
ResponseDelivery mDelivery
請求結果傳遞類,可以傳遞請求的結果或者錯誤到調用者
(2). 處理流程圖
6.2.6 Cache.java
緩存接口,代表了一個可以獲取請求結果,存儲請求結果的緩存。
(1). 主要方法:
public Entry get(String key);
通過 key 獲取請求的緩存實體
public void put(String key, Entry entry);
存入一個請求的緩存實體
public void remove(String key);
移除指定的緩存實體
public void clear();
清空緩存
(2). 代表緩存實體的內部類 Entry
成員變量和方法
byte[] data
請求返回的數據(Body 實體)
String etag
Http 響應首部中用於緩存新鮮度驗證的 ETag
long serverDate
Http 響應首部中的響應產生時間
long ttl
緩存的過期時間
long softTtl
緩存的新鮮時間
Map<String, String> responseHeaders
響應的 Headers
boolean isExpired()
判斷緩存是否過期,過期緩存不能繼續使用
boolean refreshNeeded()
判斷緩存是否新鮮,不新鮮的緩存需要發到服務端做新鮮度的檢測
6.2.7 DiskBasedCache.java
繼承 Cache 類,基於 Disk 的緩存實現類。
(1). 主要方法:
public synchronized void initialize()
初始化,掃描緩存目錄得到所有緩存數據摘要信息放入內存。
public synchronized Entry get(String key)
從緩存中得到數據。先從摘要信息中得到摘要信息,然後讀取緩存數據文件得到內容。
public synchronized void put(String key, Entry entry)
將數據存入緩存內。先檢查緩存是否會滿,會則先刪除緩存中部分數據,然後再新建緩存文件。
private void pruneIfNeeded(int neededSpace)
檢查是否能再分配 neededSpace 字節的空間,如果不能則刪除緩存中部分數據。
public synchronized void clear()
清空緩存。public synchronized void remove(String key)
刪除緩存中某個元素。
(2). CacheHeader 類
CacheHeader 是緩存文件摘要信息,存儲在緩存文件的頭部,與上面的Cache.Entry
相似。
6.2.8 NoCache.java
繼承 Cache 類,不做任何操作的緩存實現類,可將它作爲構建RequestQueue
的參數以實現一個不帶緩存的請求隊列。
6.2.9 Network.java
代表網絡的接口,處理網絡請求。
唯一的方法,用於執行特定請求。
public NetworkResponse performRequest(Request<?> request) throws VolleyError;
6.2.10 NetworkResponse.java
Network
中方法 performRequest 的返回值,Request
的 parseNetworkResponse(…) 方法入參,是 Volley 中用於內部 Response 轉換的一級。
封裝了網絡請求響應的 StatusCode,Headers 和 Body 等。
(1). 成員變量
int statusCode
Http 響應狀態碼
byte[] data
Body 數據
Map<String, String> headers
響應 Headers
boolean notModified
表示是否爲 304 響應
long networkTimeMs
請求耗時
(2). Volley 的內部 Response 轉換流程圖
從上到下表示從得到數據後一步步的處理,箭頭旁的註釋表示該步處理後的實體類。
6.2.11 BasicNetwork.java
實現 Network,Volley 中默認的網絡接口實現類。調用HttpStack
處理請求,並將結果轉換爲可被ResponseDelivery
處理的NetworkResponse
。
主要實現了以下功能:
(1). 利用 HttpStack 執行網絡請求。
(2). 如果 Request 中帶有實體信息,如 Etag,Last-Modify 等,則進行緩存新鮮度的驗證,並處理 304(Not Modify)響應。
(3). 如果發生超時,認證失敗等錯誤,進行重試操作,直到成功、拋出異常(不滿足重試策略等)結束。
6.2.12 HttpStack.java
用於處理 Http 請求,返回請求結果的接口。目前 Volley 中的實現有基於 HttpURLConnection 的 HurlStack 和 基於 Apache HttpClient 的 HttpClientStack。
唯一方法,執行請求
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError;
執行 Request 代表的請求,第二個參數表示發起請求之前,添加額外的請求 Headers。
6.2.13 HttpClientStack.java
實現 HttpStack 接口,利用 Apache 的 HttpClient 進行各種請求方式的請求。
基本就是 org.apache.http 包下面相關類的常見用法,不做詳解,不過與下面 HttpURLConnection 做下對比就能發現 HttpURLConnection 的 API 相對簡單的多。
6.2.14 HurlStack.java
實現 HttpStack 接口,利用 Java 的 HttpURLConnection 進行各種請求方式的請求。
6.2.15 Response.java
封裝了經過解析後的數據,用於傳輸。並且有兩個內部接口 Listener 和 ErrorListener 分別可表示請求失敗和成功後的回調。
Response 的構造函數被私有化,而通過兩個函數名更易懂的靜態方法構建對象。
6.2.16 ByteArrayPool.java
byte[] 的回收池,用於 byte[] 的回收再利用,減少了內存的分配和回收。主要通過一個元素長度從小到大排序的ArrayList
作爲 byte[] 的緩存,另有一個按使用時間先後排序的ArrayList
屬性用於緩存滿時清理元素。
public synchronized void returnBuf(byte[] buf)
將用過的 byte[] 回收,根據 byte[] 長度按照從小到大的排序將 byte[] 插入到緩存中合適位置。
public synchronized byte[] getBuf(int len)
獲取長度不小於 len 的 byte[],遍歷緩存,找出第一個長度大於傳入參數len
的 byte[],並返回;如果最終沒有合適的byte[],new 一個返回。
private synchronized void trim()
當緩存的 byte 超過預先設置的大小時,按照先進先出的順序刪除最早的 byte[]。
6.2.17 PoolingByteArrayOutputStream.java
繼承ByteArrayOutputStream,原始 ByteArrayOutputStream 中用於接受寫入 bytes 的 buf,每次空間不足時便會 new 更大容量的 byte[],而 PoolingByteArrayOutputStream 使用了 ByteArrayPool 作爲 Byte[] 緩存來減少這種操作,從而提高性能。
6.2.18 HttpHeaderParser.java
Http header 的解析工具類,在 Volley 中主要作用是用於解析 Header 從而判斷返回結果是否需要緩存,如果需要返回 Header 中相關信息。
有三個方法
public static long parseDateAsEpoch(String dateStr)
解析時間,將 RFC1123 的時間格式,解析成 epoch 時間
public static String parseCharset(Map<String, String> headers)
解析編碼集,在 Content-Type 首部中獲取編碼集,如果沒有找到,默認返回 ISO-8859-1
public static Cache.Entry parseCacheHeaders(NetworkResponse response)
比較重要的方法,通過網絡響應中的緩存控制 Header 和 Body 內容,構建緩存實體。如果 Header 的 Cache-Control 字段含有no-cache
或no-store
表示不緩存,返回 null。
(1). 根據 Date 首部,獲取響應生成時間
(2). 根據 ETag 首部,獲取響應實體標籤
(3). 根據 Cache-Control 和 Expires 首部,計算出緩存的過期時間,和緩存的新鮮度時間
兩點需要說明下:
1.沒有處理Last-Modify
首部,而是處理存儲了Date
首部,並在後續的新鮮度驗證時,使用Date
來構建If-Modified-Since
。這與 Http 1.1 的語義有些違背。
2.計算過期時間,Cache-Control 首部優先於 Expires 首部。
6.2.19 RetryPolicy.java
重試策略接口
有三個方法:
public int getCurrentTimeout();
獲取當前請求用時(用於Log)
public int getCurrentRetryCount();
獲取已經重試的次數(用於Log)
public void retry(VolleyError error) throws VolleyError;
確定是否重試,參數爲這次異常的具體信息。在請求異常時此接口會被調用,可在此函數實現中拋出傳入的異常表示停止重試。
6.2.20 DefaultRetryPolicy.java
實現 RetryPolicy,Volley 默認的重試策略實現類。主要通過在 retry(…) 函數中判斷重試次數是否達到上限確定是否繼續重試。
其中mCurrentTimeoutMs
變量表示已經重試次數。
mBackoffMultiplier
表示每次重試之前的 timeout 該乘以的因子。
mCurrentTimeoutMs
變量表示當前重試的 timeout 時間,會以mBackoffMultiplier
作爲因子累計前幾次重試的 timeout。
6.2.21 ResponseDelivery.java
請求結果的傳輸接口,用於傳遞請求結果或者請求錯誤。
有三個方法:
public void postResponse(Request<?> request, Response<?> response);
此方法用於傳遞請求結果,request
和 response
參數分別表示請求信息和返回結果信息。
public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
此方法用於傳遞請求結果,並在完成傳遞後執行 Runnable。
public void postError(Request<?> request, VolleyError error);
此方法用於傳輸請求錯誤。
6.2.22 ExecutorDelivery.java
請求結果傳輸接口具體實現類。
在 Handler 對應線程中傳輸緩存調度線程或者網絡調度線程中產生的請求結果或請求錯誤,會在請求成功的情況下調用 Request.deliverResponse(…) 函數,失敗時調用 Request.deliverError(…) 函數。
6.2.23 StringRequest.java
繼承 Request 類,代表了一個返回值爲 String 的請求。將網絡返回的結果數據解析爲 String 類型。通過構造函數的 listener 傳參,支持請求成功後的 onResponse(…) 回調。
6.2.24 JsonRequest.java
抽象類,繼承自 Request,代表了 body 爲 JSON 的請求。提供了構建 JSON 請求參數的方法。
6.2.25 JsonObjectRequest.java
繼承自 JsonRequest,將網絡返回的結果數據解析爲 JSONObject 類型。
6.2.26 JsonArrayRequest.java
繼承自 JsonRequest,將網絡返回的結果數據解析爲 JSONArray 類型。
6.2.27 ImageRequest.java
繼承 Request 類,代表了一個返回值爲 Image 的請求。將網絡返回的結果數據解析爲 Bitmap 類型。
可以設置圖片的最大寬度和最大高度,並計算出合適尺寸返回。每次最多解析一張圖片防止 OOM。
6.2.28 ImageLoader.java
封裝了 ImageRequst 的方便使用的圖片加載工具類。
1.可以設置自定義的
ImageCache
,可以是內存緩存,也可以是 Disk 緩存,將獲取的圖片緩存起來,重複利用,減少請求。
2.可以定義圖片請求過程中顯示的圖片和請求失敗後顯示的圖片。
3.相同請求(相同地址,相同大小)只發送一個,可以避免重複請求。
// TODO
6.2.29 NetworkImageView.java
利用 ImageLoader,可以加載網絡圖片的 ImageView
有三個公開的方法:
public void setDefaultImageResId(int defaultImage)
設置默認圖片,加載圖片過程中顯示。
public void setErrorImageResId(int errorImage)
設置錯誤圖片,加載圖片失敗後顯示。
public void setImageUrl(String url, ImageLoader imageLoader)
設置網絡圖片的 Url 和 ImageLoader,將利用這個 ImageLoader 去獲取網絡圖片。
如果有新的圖片加載請求,會把這個ImageView上舊的加載請求取消。
6.2.30 ClearCacheRequest.java
用於人爲清空 Http 緩存的請求。
添加到 RequestQueue 後能很快執行,因爲優先級很高,爲Priority.IMMEDIATE
。並且清空緩存的方法mCache.clear()
寫在了isCanceled()
方法體中,能最早的得到執行。
ClearCacheRequest 的寫法不敢苟同,目前看來唯一的好處就是可以將清空緩存操作也當做一個請求。而在isCanceled()
中做清空操作本身就造成了歧義,不看源碼沒人知道在NetworkDispatcher
run 方法循環的過程中,isCanceled()
這個讀操作竟然做了可能造成緩存被清空。只能跟源碼的解釋一樣當做一個 Hack 操作。
6.2.31 Authenticator.java
身份認證接口,用於基本認證或者摘要認證。這個類是 Volley 用於和身份驗證打通的接口,比如 OAuth,不過目前的使用不是特別廣泛和 Volley 的內部結合也不是特別緊密。
6.2.32 AndroidAuthenticator.java
繼承 Authenticator,基於 Android AccountManager 的認證交互實現類。
6.2.33 VolleyLog.java
Volley 的 Log 工具類。
6.2.34 VolleyError.java
Volley 中所有錯誤異常的父類,繼承自 Exception,可通過此類設置和獲取 NetworkResponse 或者請求的耗時。
6.2.35 AuthFailureError.java
繼承自 VolleyError,代表請求認證失敗錯誤,如 RespondeCode 的 401 和 403。
6.2.36 NetworkError.java
繼承自 VolleyError,代表網絡錯誤。
6.2.37 ParseError.java
繼承自 VolleyError,代表內容解析錯誤。
6.2.38 ServerError.java
繼承自 VolleyError,代表服務端錯誤。
6.2.39 TimeoutError.java
繼承自 VolleyError,代表請求超時錯誤。
6.2.40 NoConnectionError.java
繼承自NetworkError,代表無法建立連接錯誤。
7. 雜談
7.1 關於 Http 緩存
Volley 構建了一套相對完整的符合 Http 語義的緩存機制。
優點和特點
(1). 根據Cache-Control
和Expires
首部來計算緩存的過期時間。如果兩個首部都存在情況下,以Cache-Control
爲準。
(2). 利用If-None-Match
和If-Modified-Since
對過期緩存或者不新鮮緩存,進行請求再驗證,並處理 304 響應,更新緩存。
(3). 默認的緩存實現,將緩存以文件的形式存儲在 Disk,程序退出後不會丟失。
我個人認爲的不足之處
緩存的再驗證方面,在構建If-Modified-Since
請求首部時,Volley 使用了服務端響應的Date
首部,沒有使用Last-Modified
首部。整個框架沒有使用Last-Modified
首部。這與 Http 語義不符。
private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
// If there's no cache entry, we're done.
if (entry == null) {
return;
}
if (entry.etag != null) {
headers.put("If-None-Match", entry.etag);
}
if (entry.serverDate > 0) {
Date refTime = new Date(entry.serverDate);
headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
}
服務端根據請求時通過If-Modified-Since
首部傳過來的時間,判斷資源文件是否在If-Modified-Since
時間 以後有改動,如果有改動,返回新的請求結果。如果沒有改動,返回 304 not modified。
Last-Modified
代表了資源文件的最後修改時間。通常使用這個首部構建If-Modified-Since
的時間。
Date
代表了響應產生的時間,正常情況下Date
時間在Last-Modified
時間之後。也就是Date
>=Last-Modified
。
通過以上原理,既然Date
>=Last-Modified
。那麼我利用Date
構建,也是完全正確的。
可能的問題出在服務端的 Http 實現上,如果服務端完全遵守 Http 語義,採用時間比較的方式來驗證If-Modified-Since
,判斷服務器資源文件修改時間是不是在If-Modified-Since
之後。那麼使用Date
完全正確。
可是有的服務端實現不是比較時間,而是直接的判斷服務器資源文件修改時間,是否和If-Modified-Since
所傳時間相等。這樣使用Date
就不能實現正確的再驗證,因爲Date
的時間總不會和服務器資源文件修改時間相等。
儘管使用Date
可能出現的不正確情況,歸結於服務端沒有正確的實現 Http 語義。
但我還是希望Volley也能完全正確的實現Http語義,至少同時處理Last-Modified
和Date
,並且優先使用Last-Modified
。
7.2 Bug
(1). BasicNetwork.performRequest(…)
如下代碼:
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
……
while (true) {
……
try {
……
} catch (IOException e) {
int statusCode = 0;
NetworkResponse networkResponse = null;
……
if (responseContents != null) {
……
} else {
throw new NetworkError(networkResponse);
}
}
}
}
BasicNetwork.performRequest(…) 最後的
throw new NetworkError(networkResponse);
應該是
throw new NetworkError(e);
更合理。