【安卓筆記】Volley全方位解析,帶你從原始碼的角度徹底理解
轉載請宣告原出處(blog.csdn.net/chdjj),謝謝!
--------------------------
參考資料
1.http://www.codekk.com/open-source-project-analysis/detail/Android/grumoon/Volley%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90
2.http://blog.csdn.net/guolin_blog/article/details/17656437
部分配圖及文字來自上面連結中的文章,謝謝原作者的辛勤勞動!
-----------------------
花了兩天時間看完了Volley的原始碼,我覺得有必要總結一下,故有此文。
主要特點:
(1). 擴充套件性強。Volley 中大多是基於介面的設計,可配置性強。
(2). 一定程度符合 Http 規範,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的處理,請求頭的處理,快取機制的支援等。並支援重試及優先順序定義。
(3). 預設 Android2.3 及以上基於 HttpURLConnection,2.3 以下基於 HttpClient 實現。
(2). 一定程度符合 Http 規範,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的處理,請求頭的處理,快取機制的支援等。並支援重試及優先順序定義。
(3). 預設 Android2.3 及以上基於 HttpURLConnection,2.3 以下基於 HttpClient 實現。
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。
(4). 提供簡便的圖片載入工具。
(4). 提供簡便的圖片載入工具。
(一).基本使用
(a)各種Request的使用
volley使用及其簡單,我們只需要建立一個RequestQueue請求佇列,然後往佇列裡面扔http請求即可,volley會
不斷從佇列裡面取出請求然後交給一堆工作執行緒處理。這裡的http請求是通過Request類來封裝的,我們只用建立Request物件,然後提供諸如url之類的引數即可。網路操作全部是在子執行緒中處理的,我們不必擔心阻塞UI執行緒。
網路請求的結果會非同步返回給我們,我們只需要處理Request的回撥即可。
Request本身是一個抽象類,不能直接建立例項,volley為我們實現了一些Request,比如StringRequest是普通http請求,JsonRequest可以封裝json資料,並將服務端的返回資料封裝成JsonObject,ImageRequest可以請求一張網路圖片,並將服務端的返回資料封裝成Bitmap。
Request的使用分為三步:1.建立RequestQueue佇列;2.建立Request物件,並加入佇列中;3.處理回撥事件。
建立RequestQueue很簡單,呼叫Volley類的靜態方法newRequestQueue,並指定Context即可:
private RequestQueue mQueue = null;
// create request queue...
mQueue = Volley.newRequestQueue(this);//this代表當前的上下文
1.StringRequest
StringRequest的預設請求方式為GET,使用其他請求方式可以用其另一種過載形式。
String url = "http://192.168.56.1:8080";
StringRequest request = new StringRequest(url,new Response.Listener<String>()
{
@Override
public void onResponse(String response)//success callbacks
{
//handle it
}
}, new Response.ErrorListener()//error callbacks
{
@Override
public void onErrorResponse(VolleyError error)
{
error.printStackTrace();
}
});
//add request to queue...
mQueue.add(request);
Response.Listener處理請求成功時的回撥。Response.ErrorListener處理失敗時的回撥。程式碼很簡單,不過多介紹。
2.JsonRequest
這裡的JsonObject是android內建的org.json庫,而不是自家的Gson,這點需要注意。
Map<String,String> params = new HashMap<String,String>();
params.put("name","zhangsan");
params.put("age","17");
JSONObject jsonRequest = new JSONObject(params);
Log.i(TAG,jsonRequest.toString());
//如果json資料為空則是get請求,否則是post請求
//如果jsonrequest不為null,volley會將jsonObject物件轉化為json字串原封不動的發給伺服器,並不會轉成k-v對,因為volley不知道應該如何轉化
String url = "http://192.168.56.1:8080/volley_test/servlet/JsonServlet";
JsonObjectRequest request = new JsonObjectRequest(url, jsonRequest, new Response.Listener<JSONObject>()
{
@Override
public void onResponse(JSONObject response)
{
//handle it
}
},new Response.ErrorListener()
{
@Override
public void onErrorResponse(VolleyError error)
{
error.printStackTrace();
}
});
mQueue.add(request);
3.ImageRequest
ImageRequest可以控制圖片的寬高、照片品質。如果寬高比原始寬高小的話,將會進行壓縮。
ImageRequest request = new ImageRequest("http://192.168.56.1:8080/volley_test/image.jpg",new Response.Listener<Bitmap>()
{
@Override
public void onResponse(Bitmap response)
{
mImageView.setImageBitmap(response);
}
},0,0, Bitmap.Config.ARGB_8888,new Response.ErrorListener()
{//引數0 0 代表不壓縮
@Override
public void onErrorResponse(VolleyError error)
{
show(error.getMessage());
//可以去顯示預設圖片
}
});
mQueue.add(request);
當然,圖片載入還有另外兩種方式,我們放到下一部分再介紹。
4.新增請求頭
有時候我們需要為Request新增請求頭,這時候可以去重寫Request的getHeaders方法。
String url = "http://192.168.56.1:8080/volley_test/servlet/JsonServlet";
JsonObjectRequest request = new JsonObjectRequest(url, null,resplistener,errlistener)
{
//新增自定義請求頭
@Override
public Map<String, String> getHeaders() throws AuthFailureError
{
Map<String,String> map = new HashMap<String,String>();
map.put("header1","header1_val");
map.put("header2","header2_val");
return map;
}
};
5.新增post請求引數
新增Post請求引數可以重寫Request的GetParams方法,另需 修改請求引數為POST。
String url = "http://192.168.56.1:8080/volley_test/servlet/PostServlet";
StringRequest request = new StringRequest(Method.POST,url,listener, errorListener)
{
//post請求需要複寫getParams方法
@Override
protected Map<String, String> getParams() throws AuthFailureError
{
Map<String,String> map = new HashMap<String,String>();
map.put("KEY1","value1");
map.put("KEY2", "value2");
return map;
}
};
Request裡面還有一些getXXX方法,大家參考程式碼自己琢磨吧。
6.取消請求
當Activity銷燬時,我們可能需要去取消一些網路請求,這時候可以通過如下方式:
Request req = ...;
request.setTag("MAIN_ACTIVITY");
onDestroy()
{
...
mQueue.cancelAll("MAIN_ACTIVITY");
}
為屬於該Activity的請求全部加上Tag,然後需要銷燬的時候呼叫cancelAll傳入tag即可。
RequestQueue#cancelAll還有另一種過載形式,可以傳入RequestFilter,自己指定一個過濾策略。
如果我們需要幹掉所有請求,並且後續不再有網路請求,可以幹掉RequestQueue,呼叫其stop方法即可。
7.全域性共享RequestQueue
RequestQueue沒有必要每個Activity裡面都建立,全域性保有一個即可。這時候自然想到使用Application了。我們可以在Application裡面建立RequestQueue,並向外暴露get方法。程式碼很簡單,相信大家都會寫。
(b)圖片載入框架的使用
上面介紹了ImageRequest載入網路圖片,但是這還不夠精簡,volley另提供了ImageLoader和NetworkImageView。當然,它們內部都是使用ImageRequest。
1.ImageLoader
RequestQueue mQueue = ...;
ImageCache mCache = ...;
loader = new ImageLoader(mQueue,mImageCache);
ImageListener listener = ImageLoader.getImageListener(mImageView/*關聯的iamgeView*/,R.drawable.ic_launcher/*圖片載入時顯示*/, R.drawable.task_icon/*圖片載入失敗時顯示*/);
loader.get("http://192.168.56.1:8080/volley_test/image.jpg", listener, 0, 0);
首先建立RequestQueue不用多說,然後建立ImageLoader例項,同時需要傳入請求佇列和ImageCache。這個ImageCache是一個介面,我們需要自己實現,它是一個圖片快取,通常我們都是結合LRUCache作為記憶體快取使用,當然,如果你不想使用記憶體快取,那就給它一個空實現即可。緊接著,需要為ImageLoader繫結ImageView以及圖片載入時、圖片載入失敗時的圖片資源。最後呼叫get方法傳入url請求網路。
關於ImageCache快取:
public interface ImageCache {
public Bitmap getBitmap(String url);
public void putBitmap(String url, Bitmap bitmap);
}
可見,如果我們需要圖片快取,那麼實現getBitmap/putBitmap即可。每次呼叫ImageLoader#get方法時,都會首先從ImageCache中尋找(getBitmap),如果沒找到才將請求新增到佇列中。
應該怎樣實現ImageCache:
通常結合LRUCache。值得注意的是ImageCache需要做成單例,全域性共享。下面是ImageCache結合LRUCache的程式碼:
/**
* @author Rowandjj
*圖片快取需要做成單例,全域性共享
*/
private static class LruImageCache implements ImageCache
{
private LruImageCache(){}
private static LruImageCache instance = new LruImageCache();
public static final LruImageCache getInstance()
{
return instance;
}
private static final String TAG = "LruImageCache";
private final int maxSize = (int) (Runtime.getRuntime().maxMemory()/8);
private LruCache<String,Bitmap> mCacheMap = new LruCache<String,Bitmap>(maxSize)
{
protected int sizeOf(String key, Bitmap value)
{
return value.getRowBytes()*value.getHeight();
}
};
@Override
public Bitmap getBitmap(String url)
{
Bitmap bitmap = mCacheMap.get(url);
Log.i(TAG, "url = "+url+",cache:"+bitmap);
return bitmap;
}
@Override
public void putBitmap(String url, Bitmap bitmap)
{
Log.i(TAG, "put url = "+url);
mCacheMap.put(url, bitmap);
}
}
2.NetworkImageView
這是一個自定義控制元件,使用上跟ImaegView類似。
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/niv"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
>
ImageLoader loader = ...;
mNetImageView = findViewById(R.id.niv);
mNetImageView.setDefaultImageResId(R.drawable.ic_launcher);
mNetImageView.setErrorImageResId(R.drawable.task_icon);
mNetImageView.setImageUrl("http://192.168.56.1:8080/volley_test/image.jpg", loader);
程式碼很簡單,不過多介紹。
(c)定製request
volley是一個高度可擴充套件性的框架,我們可以在其基礎上,增加很多自己的東西,比如定製Request。
上面介紹了JsonRequest可以解析傳遞json資料,那麼下面我們定製一個XMLRequest去解析XML資料。
定製Request需重寫Request#parseNetworkResponse解析網路響應資料,Request#deliverResponse回撥Listener.onResponse。
public class XMLRequest extends Request<XmlPullParser>
{
private Listener<XmlPullParser> mListener;
public XMLRequest(int method, String url, Listener<XmlPullParser> listener,
ErrorListener errorListener)
{
super(method, url, errorListener);
mListener = listener;
}
public XMLRequest(String url, Listener<XmlPullParser> listener,
ErrorListener errorListener)
{
this(Method.GET, url, listener, errorListener);
}
@Override
protected Response<XmlPullParser> parseNetworkResponse(NetworkResponse response)
{
try
{
String xmlString = new String(response.data,HttpHeaderParser.parseCharset(response.headers));
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(xmlString));//將返回資料設定給解析器
return Response.success(parser,HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e)
{
return Response.error(new VolleyError(e));
} catch (XmlPullParserException e)
{
return Response.error(new VolleyError(e));
}
}
@Override
protected void deliverResponse(XmlPullParser response)
{
mListener.onResponse(response);
}
}
使用方式:/**
* xmlRequest 使用示例
*/
void test()
{
RequestQueue queue = Volley.newRequestQueue(context);
String url = "";
XMLRequest request = new XMLRequest(url,new Response.Listener<XmlPullParser>()
{
@Override
public void onResponse(XmlPullParser response)
{
int type = response.getEventType();
while(type != XmlPullParser.END_DOCUMENT)
{
switch (type)
{
case XmlPullParser.START_TAG:
break;
case XmlPullParser.END_TAG:
break;
default:
break;
}
response.next();
}
}
},new Response.ErrorListener()
{
@Override
public void onErrorResponse(VolleyError error)
{
}
});
}
其實除了定製自己的Request,我們還可以定製好多東西,比如RequestQueue,參看RequestQueue的構造器:public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery)
我們發現構造器需要指定快取策略(預設硬碟快取),執行緒池大小,結果分發策略等等。所有這些都是可配置的。volley這個框架寫的太牛逼了!
(二).原始碼分析
僅知道如何使用那是遠遠不夠的,我們應該分析原始碼看其內部實現原理,這樣才能夠進步。
1.主線
首先我們從整體上把握volley的工作流程,抓住其主線。
(1)請求佇列(RequestQueue)的建立
建立請求佇列的工作是從Volley#newRequestQueue開始的,這個方法內部會呼叫RequestQueue的構造器,同時指定一些基本配置,如快取策略為硬碟快取(DiskBasedCache),http請求方式為HttpURLConnection(level>9)和HttpClient(level<9),預設執行緒池大小為4。最後,呼叫RequestQueue#start啟動請求佇列。
看詳細程式碼:
//Volley.java
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
呼叫另一個工廠方法://volley.java
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
... ...
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
這裡指定了硬碟快取的位置為data/data/package_name/cache/volley/...,Network類(具體實現類是BasicNetwork)封裝了請求方式,並且根據當前API版本來選用不同的http工具。最後啟動了請求佇列。
下面看RequestQueue的構造器:
//RequestQueue.java
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
指定預設執行緒池大小為4。//RequestQueue.java
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
這裡的ResponseDelivery是請求結果的分發器(具體實現是ExecutorDelivery),內部將結果返回給主執行緒(根據程式碼中使用了Handler和UI執行緒的Looper大家就應該能猜到了),並處理回撥事件。
下面看請求佇列的開始後會發生什麼,檢視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和4個NetworkDispatcher個物件,然後分別啟動之。這個CacheDispatcher和NetworkDispatcher都是Thread的子類,其中CacheDispatcher處理走快取的請求,而4個NetworkDispatcher處理走網路的請求。CacheDispatcher通過構造器注入了快取請求佇列(mCacheQueue),網路請求佇列(mNetworkQueue),硬碟快取物件(DiskBasedCache),結果分發器(mDelivery)。之所以也注入網路請求佇列是因為一部分快取請求可能已經過期了,這時候需要重新從網路獲取。NetworkDispatcher除了快取請求佇列沒有注入,其他跟CacheDispatcher一樣。到這裡RequestQueue的任務就完成了,以後有請求都會交由這些dispatcher執行緒處理。
(2)請求的新增
請求的新增是通過RequestQueue#add完成的,add方法的邏輯是這樣的:
1.將請求加入mCurrentRequests集合
2.為請求新增序列號
3.判斷是否應該快取請求,如果不需要,加入網路請求佇列
4.如果有相同請求正在被處理,加入到相同請求等待佇列中,否則加入快取請求佇列。
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;
}
}
通過這一方法,請求就被分發到兩個佇列中分別供CacheDispatcher和NetworkDispatcher處理。
(3)請求的處理
請求的處理是由CacheDispatcher和NetworkDispatcher來完成的,它們的run方法通過一個死迴圈不斷去從各自的佇列中取出請求,進行處理,並將結果交由ResponseDelivery。兩者處理思想一致但是具體邏輯還是有點區別,我們分別看看。
1.走快取的請求
CacheDispatcher.java#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();
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 reques
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;
}
}
}
大體邏輯是這樣的,首先從佇列中取出請求,看其是否已被取消,若是則返回,否則繼續向下走。接著從硬碟快取中通過快取的鍵找到值(Cache.Entry),如果找不到,那麼將此請求加入網路請求佇列。否則對快取結果進行過期判斷(這個需要請求的頁面指定了Cache-Control或者Last-Modified/Expires等欄位,並且Cache-Control的優先順序比Expires更高。否則請求一定是過期的),如果過期了,則加入網路請求佇列。如果沒有過期,那麼通過request.parseNetworkResponse方法將硬碟快取中的資料封裝成Response物件(Request的parseNetworkResponse是抽象的,需要複寫)。最後進行新鮮度判斷,如果不需要重新整理,那麼呼叫ResponseDelivery結果分發器的postResponse分發結果。否則先將結果返回,再將請求交給網路請求佇列進行重新整理。【這段程式碼讀起來很爽,google工程師寫的太讚了!】關於ResponseDelivery的具體過程我們留到下節講。
2.走網路的請求
@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));
}
}
}
這裡的邏輯跟CacheDispatcher類似,也是構造Response物件,然後交由ResponseDelivery處理,但是這裡的Response物件是通過NetworkResponse轉化的,而這個NetworkResponse是從網路獲取的,這裡最核心的一行程式碼就是 NetworkResponse networkResponse = mNetwork.performRequest(request);
這個mNetwork是BasicNetwork物件,我們看其performRequest的實現: 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().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 (SocketTimeoutException e) {
attemptRetryOnException("socket", request, new TimeoutError());
} catch (ConnectTimeoutException e) {
attemptRetryOnException("connection", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode = 0;
NetworkResponse networkResponse = null;
if (httpResponse != null) {
statusCode = httpResponse.getStatusLine().getStatusCode();
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
if (responseContents != null) {
networkResponse = new NetworkResponse(statusCode, responseContents,
responseHeaders, false);
if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
statusCode == HttpStatus.SC_FORBIDDEN) {
attemptRetryOnException("auth",
request, new AuthFailureError(networkResponse));
} else {
// TODO: Only throw ServerError for 5xx status codes.
throw new ServerError(networkResponse);
}
} else {
throw new NetworkError(networkResponse);
}
}
}
}
這裡最核心的是這一句:httpResponse = mHttpStack.performRequest(request, headers);
它呼叫了HttpStack的performRequest,這個方法內部肯定會呼叫HttpURLConnection或者是HttpClient去請求網路。這裡我們就不必繼續向下跟原始碼了。
(4)請求結果的分發與處理
請求結果的分發處理是由ResponseDelivery實現類ExecutorDelivery完成的,ExecutorDelivery是在RequestQueue的構造器中被建立的,並且繫結了UI執行緒的Looper:
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
ExecutorDelivery內部有個自定義Executor,它僅僅是封裝了Handler,所有待分發的結果最終會通過handler.post方法交給UI執行緒。public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
下面看我們最關心的postResponse方法:@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
最終執行的是ResponseDeliveryRunnable這個Runnable:private class ResponseDeliveryRunnable implements Runnable {
private final Request mRequest;
private final Response mResponse;
private final Runnable mRunnable;
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
// If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
// If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
// If we have been provided a post-delivery runnable, run it.
if (mRunnable != null) {
mRunnable.run();
}
}
}
這裡我們看到了request.deliverResponse被呼叫了,這個方法通常會回撥Listener.onResponse。哈哈,到這裡,整個volley框架的主線就看完了!讀到這裡,我真是由衷覺得google工程師牛逼啊!
2.一些支線細節
(1)請求的取消
呼叫Request#cancel可以取消一個請求。cancel方法很簡單,僅將mCanceled變數置為true。而CacheDispatcher/NetworkDispatcher的run方法中在取到一個Request後會判斷是否請求取消了:
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
如果請求取消就呼叫Request#finish,finish方法內部將呼叫與之繫結的請求佇列的finish方法,該方法內部會將請求物件在佇列中移除。
(2)請求佇列的終止
呼叫RequestQueue#stop可以終止整個請求佇列,並終止快取請求執行緒與網路請求執行緒:
public void stop() {
if (mCacheDispatcher != null) {
mCacheDispatcher.quit();
}
for (int i = 0; i < mDispatchers.length; i++) {
if (mDispatchers[i] != null) {
mDispatchers[i].quit();
}
}
}
XXXDispatcher的quit方法會修改mQuit變數並呼叫interrupt使執行緒拋Interrupt異常,而Dispatcher捕獲到異常後會判斷mQuit變數最終while迴圈結束,執行緒退出。catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
(3)ImageLoader
ImageLoader是對ImageRequest的封裝,這裡重點關注下get方法:
public ImageContainer get(String requestUrl, ImageListener imageListener,
int maxWidth, int maxHeight) {
...
final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
// Try to look up the request in the cache of remote images.
Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
if (cachedBitmap != null) {
// Return the cached bitmap.
ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
imageListener.onResponse(container, true);
return container;
}
...
Request<?> newRequest =
new ImageRequest(requestUrl, new Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
onGetImageSuccess(cacheKey, response);
}
}, maxWidth, maxHeight,
Config.RGB_565, new ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
onGetImageError(cacheKey, error);
}
});
mRequestQueue.add(newRequest);
mInFlightRequests.put(cacheKey,
new BatchedImageRequest(newRequest, imageContainer));
return imageContainer;
}
首先會從快取中獲取如果沒有則構造ImageRequest並新增到請求佇列。
(4)關於快取
Volley的CacheDispatcher工作時需要指定快取策略,這個快取策略即Cache介面,這個介面有兩個實現類,DiskBasedCache和NoCache,預設使用DiskedBasedCache。它會將請求結果存入檔案中,以備複用。Volley是一個高度靈活的框架,快取是可以配置的。甚至你可以使用自己的快取策略。
可惜這個DiskBasedCache很多時候並不能被使用,因為CacheDispatcher即使從快取檔案中拿到了快取的資料,還需要看該資料是否過期,如果過期,將不使用快取資料。這就要求服務端的頁面可以被快取,這個是由Cache-Control和Expires等欄位決定的,服務端需要設定此欄位才能使資料可以被快取。否則快取始終是過期的,最終總是走的網路請求。
服務端假如是servlet寫的可以這樣做:
Servlet#doPost/doGet()
/*設定快取*/
resp.setDateHeader("Last-Modified",System.currentTimeMillis());
resp.setDateHeader("Expires", System.currentTimeMillis()+10*1000*60);
resp.setHeader("Cache-Control","max-age=10000");
resp.setHeader("Pragma","Pragma");
Cache-Control欄位的優先順序高於Expires。這個可以從HttpHeaderParser#parseCacheHeaders方法中看到。public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
long now = System.currentTimeMillis();
Map<String, String> headers = response.headers;
long serverDate = 0;
long serverExpires = 0;
long softExpire = 0;
long maxAge = 0;
boolean hasCacheControl = false;
String serverEtag = null;
String headerValue;
headerValue = headers.get("Date");
if (headerValue != null) {
serverDate = parseDateAsEpoch(headerValue);
}
headerValue = headers.get("Cache-Control");
if (headerValue != null) {
hasCacheControl = true;
String[] tokens = headerValue.split(",");
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.equals("no-cache") || token.equals("no-store")) {
return null;
} else if (token.startsWith("max-age=")) {
try {
maxAge = Long.parseLong(token.substring(8));
} catch (Exception e) {
}
} else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
maxAge = 0;
}
}
}
headerValue = headers.get("Expires");
if (headerValue != null) {
serverExpires = parseDateAsEpoch(headerValue);
}
serverEtag = headers.get("ETag");
// Cache-Control takes precedence over an Expires header, even if both exist and Expires
// is more restrictive.
if (hasCacheControl) {
softExpire = now + maxAge * 1000;
} else if (serverDate > 0 && serverExpires >= serverDate) {
// Default semantic for Expire header in HTTP specification is softExpire.
softExpire = now + (serverExpires - serverDate);
}
Cache.Entry entry = new Cache.Entry();
entry.data = response.data;
entry.etag = serverEtag;
entry.softTtl = softExpire;
entry.ttl = entry.softTtl;
entry.serverDate = serverDate;
entry.responseHeaders = headers;
return entry;
}
這個方法是由Request子類的parseNetworkResponse方法呼叫的:Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response))
另外,在使用圖片載入框架時還有個ImageCache,它是作為圖片載入的一級快取,跟上面的DiskedBasedCache沒有任何關係,大家不要混淆。ImageCache需要我們自己實現,通常結合LRUCache,具體第一部分已經介紹過了。
到這裡我們把整個Volley框架全部分析完了。最後貼上Volley的整體架構圖:
下面這幅圖也很好:
以上就是Volley的全部內容,謝謝大家看到最後!祝大家春節快樂!!
相關文章
- Android AsyncTask完全解析,帶你從原始碼的角度徹底理解Android原始碼
- Android ListView工作原理完全解析,帶你從原始碼的角度徹底理解AndroidView原始碼
- Android事件分發機制完全解析,帶你從原始碼的角度徹底理解(上)Android事件原始碼
- 筆記-runtime原始碼解析之讓你徹底瞭解底層原始碼筆記原始碼
- 從IL角度徹底理解回撥_委託_指標指標
- 徹底理解OkHttp - OkHttp 原始碼解析及OkHttp的設計思想HTTP原始碼
- 帶你徹底理解 Android 中的 Window 和 WindowManagerAndroid
- 一文帶你徹底理解 JavaScript 原型物件JavaScript原型物件
- 一題帶你徹底理解sleep()和wait()AI
- 12張圖帶你徹底理解分散式事務!!分散式
- ArrayList 從原始碼角度剖析底層原理原始碼
- 「從原始碼中學習」徹底理解Vue選項Props原始碼Vue
- MySQL半一致性讀原理解析-從原始碼角度解析MySql原始碼
- 線段樹 - 多組圖帶你從頭到尾徹底理解線段樹
- 遲到的Volley原始碼解析原始碼
- 帶你徹底弄懂Event LoopOOP
- 從原始碼角度,帶你研究什麼是三級快取原始碼快取
- 讓你徹底理解 “==”與 Equals
- Android EventBus原始碼解析 帶你深入理解EventBusAndroid原始碼
- 全方位徹底讀懂<你不知道的JavaScript(上)>--一篇六萬多字的讀書筆記JavaScript筆記
- Volley的原理解析
- 【Mybatis系列】從原始碼角度理解Mybatis的$和#的作用MyBatis原始碼
- 從原始碼的角度解析Mybatis的會話機制原始碼MyBatis會話
- 徹底理解安卓應用無響應機制安卓
- Android Volley框架原始碼解析Android框架原始碼
- 從原始碼徹底理解 Prometheus/VictoriaMetrics 中的 relabel/metric_configs 配置原始碼Prometheus
- 徹底理解原碼、補碼、反碼
- Android網路程式設計(四)從原始碼解析VolleyAndroid程式設計原始碼
- 從程式碼生成說起,帶你深入理解 mybatis generator 原始碼MyBatis原始碼
- 【Mybatis系列】從原始碼角度深度理解Mybatis的快取特性MyBatis原始碼快取
- Android 開源專案原始碼解析 -->Volley 原始碼解析(十五)Android原始碼
- 圖解|從根上徹底理解MySQL的索引圖解MySql索引
- 徹底理解synchronizedsynchronized
- 從原始碼的角度解析執行緒池執行原理原始碼執行緒
- 【JS基礎】從零開始帶你理解JavaScript閉包--我是如何徹底搞明白閉包的JSJavaScript
- 從原始碼角度深入理解Glide4(上)原始碼IDE
- 從原始碼角度深入理解Glide4(下)原始碼IDE
- 徹底理解JavaScript中的thisJavaScript