轉載請注明本文出自changel的博客(http://www.cnblogs.com/caichongyang/p/4399790.html ),請尊重他人的辛勤勞動成果,謝謝!
Volley框架特點:
- 適用於頻繁請求而每次請求數據量不會很大;
- 在請求的基礎上做了磁盤緩存;
- 防止多次相同請求浪費資源;
- 提供String、Json、圖片異步下載;
- 網絡請求的優先級處理;
- 圖片請求無需擔心生命周期問題。
Volley框架使用:
- 首先,通過Volley的靜態方法new一個請求隊列
1 RequestQueue mQueue = Volley.newRequestQueue(context); - 假如我們創建一個StringRequest實例(Volley提供,StringRequest、ImageRequest、JsonRequest。)
StringRequest stringRequest = new StringRequest("http://www.baidu.com", new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d("TAG", response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.e("TAG", error.getMessage(), error); } });
- 將XXXRequest對象添加進隊列中
mQueue.add(stringRequest);
- 調用RequestQueue的start方法就可以開始一條網絡請求了
mQueue.start();
- 當然我們可以設置請求的方式:
StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener); - 設置提交的參數:
StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener) { @Override protected Map<String, String> getParams() throws AuthFailureError { Map<String, String> map = new HashMap<String, String>(); map.put("params1", "value1"); map.put("params2", "value2"); return map; } }; - 至於其他的請求,讀者可以自己去嘗試。比如圖片和Json的請求。(注意Volley還提供了自己的控件,com.android.volley.NetworkImageView,使用時我們無需擔心相關網絡請求的生命周期問題。)代碼如下:
mImageLoader = new ImageLoader(mRequestQueue, new BitmapLruCache()); if(holder.imageRequest != null) { holder.imageRequest.cancel(); } holder.imageRequest = mImageLoader.get(BASE_UR + item.image_url, holder.imageView, R.drawable.loading, R.drawable.error);mImageView.setImageUrl(url, imageLoader) //這里使用的imageView的控件是Volley提供的
Volley框架時序圖:
自己畫了一個Volley框架的時序圖,幫助理解。
Volley框架實現原理:
下面給出自己畫的一張UML幫助理解,畫得可能不是很准確。但至少整體的框架還是明確的。

這個框架我覺得最核心的部分就是RequestQueue這個請求隊列,首先通過Volley.newRequestQueue(context)的方法就可以拿到一個RequestQueue對象。
這個請求隊列主要是由兩部分構成,一個是cache緩存,一個network網絡請求。其實這兩個只是一個接口而已。
我們一開始要往請求隊列里面添加請求的任務,比如請求一些文字信息,那么就創建一個StringRequest對象,通過構造函數,將兩個接口對象傳遞進去,進行綁定。
1 /** 2 * Adds a Request to the dispatch queue. 3 * @param request The request to service 4 * @return The passed-in request 5 */ 6 public <T> Request<T> add(Request<T> request) { 7 // Tag the request as belonging to this queue and add it to the set of current requests. 8 request.setRequestQueue(this); 9 synchronized (mCurrentRequests) { 10 mCurrentRequests.add(request); 11 } 12 13 // Process requests in the order they are added. 14 request.setSequence(getSequenceNumber()); 15 request.addMarker("add-to-queue"); 16 17 // If the request is uncacheable, skip the cache queue and go straight to the network. 18 if (!request.shouldCache()) { 19 mNetworkQueue.add(request); 20 return request; 21 } 22 23 // Insert request into stage if there's already a request with the same cache key in flight. 24 synchronized (mWaitingRequests) { 25 String cacheKey = request.getCacheKey(); 26 if (mWaitingRequests.containsKey(cacheKey)) { 27 // There is already a request in flight. Queue up. 28 Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey); 29 if (stagedRequests == null) { 30 stagedRequests = new LinkedList<Request<?>>(); 31 } 32 stagedRequests.add(request); 33 mWaitingRequests.put(cacheKey, stagedRequests); 34 if (VolleyLog.DEBUG) { 35 VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); 36 } 37 } else { 38 // Insert 'null' queue for this cacheKey, indicating there is now a request in 39 // flight. 40 mWaitingRequests.put(cacheKey, null); 41 mCacheQueue.add(request); 42 } 43 return request; 44 } 45 }
(其實內部並不是簡單的將Request的子類直接添加到隊列中,而是通過mWaitingRequests,這個是一個MAP的集合,通過containsKey(cacheKey)來判斷任務隊列中是否已經存在了該請求任務,主要是避免重復請求。)
其實所有的請求對象都是繼承於Request的抽象類,該抽象類還實現了Comparable 的接口,該接口是將該對象添加進PriorityBlockingQueue中必須實現的,然后Request的子類實現Request類中parseNetworkResponse()的抽象方法,這個方法是對請求返回的數據進行解析。我們可以將數據封裝為一個對象,進行使用。
1 @Override 2 protected Response<String> parseNetworkResponse(NetworkResponse response) { 3 String parsed; 4 try { 5 parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); 6 } catch (UnsupportedEncodingException e) { 7 parsed = new String(response.data); 8 } 9 return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); 10 }
當調用RequestQueue對象內的start()方法的時候,具體是通過CacheDispatcher緩存調度和NetworkDispatcher網絡調度去實現的。NetworkDispatcher這個就是網絡調度的核心,它其實是一個Thread的子類。而CacheDispatcher也是一個Thread的子類。簡單點說,NetworkDispatcher 線程的run()方法就是把請求隊列取出一個Request,讓Network去執行一次請求。這里說的並不是很准確,因為其實Cache和Network只是兩個接口,NetWork接口定義了一個請求的方法,真正的實現類還是BasicNetwork,所以具體的網絡請求是在BasicNetwork里面的performRequest方法去實現的。下面我貼出這個方法的源碼:
1 public NetworkResponse performRequest(Request<?> request) throws VolleyError { 2 long requestStart = SystemClock.elapsedRealtime(); 3 while (true) { 4 HttpResponse httpResponse = null; 5 byte[] responseContents = null; 6 Map<String, String> responseHeaders = Collections.emptyMap(); 7 try { 8 // Gather headers. 9 Map<String, String> headers = new HashMap<String, String>(); 10 addCacheHeaders(headers, request.getCacheEntry()); 11 httpResponse = mHttpStack.performRequest(request, headers); 12 StatusLine statusLine = httpResponse.getStatusLine(); 13 int statusCode = statusLine.getStatusCode(); 14 15 responseHeaders = convertHeaders(httpResponse.getAllHeaders()); 16 // Handle cache validation. 17 if (statusCode == HttpStatus.SC_NOT_MODIFIED) { 18 19 Entry entry = request.getCacheEntry(); 20 if (entry == null) { 21 return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, 22 responseHeaders, true, 23 SystemClock.elapsedRealtime() - requestStart); 24 } 25 26 // A HTTP 304 response does not have all header fields. We 27 // have to use the header fields from the cache entry plus 28 // the new ones from the response. 29 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 30 entry.responseHeaders.putAll(responseHeaders); 31 return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, 32 entry.responseHeaders, true, 33 SystemClock.elapsedRealtime() - requestStart); 34 } 35 36 // Handle moved resources 37 if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { 38 String newUrl = responseHeaders.get("Location"); 39 request.setRedirectUrl(newUrl); 40 } 41 42 // Some responses such as 204s do not have content. We must check. 43 if (httpResponse.getEntity() != null) { 44 responseContents = entityToBytes(httpResponse.getEntity()); 45 } else { 46 // Add 0 byte response as a way of honestly representing a 47 // no-content request. 48 responseContents = new byte[0]; 49 } 50 51 // if the request is slow, log it. 52 long requestLifetime = SystemClock.elapsedRealtime() - requestStart; 53 logSlowRequests(requestLifetime, request, responseContents, statusLine); 54 55 if (statusCode < 200 || statusCode > 299) { 56 throw new IOException(); 57 } 58 return new NetworkResponse(statusCode, responseContents, responseHeaders, false, 59 SystemClock.elapsedRealtime() - requestStart); 60 } catch (SocketTimeoutException e) { 61 attemptRetryOnException("socket", request, new TimeoutError()); 62 } catch (ConnectTimeoutException e) { 63 attemptRetryOnException("connection", request, new TimeoutError()); 64 } catch (MalformedURLException e) { 65 throw new RuntimeException("Bad URL " + request.getUrl(), e); 66 } catch (IOException e) { 67 int statusCode = 0; 68 NetworkResponse networkResponse = null; 69 if (httpResponse != null) { 70 statusCode = httpResponse.getStatusLine().getStatusCode(); 71 } else { 72 throw new NoConnectionError(e); 73 } 74 if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 75 statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { 76 VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl()); 77 } else { 78 VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); 79 } 80 if (responseContents != null) { 81 networkResponse = new NetworkResponse(statusCode, responseContents, 82 responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); 83 if (statusCode == HttpStatus.SC_UNAUTHORIZED || 84 statusCode == HttpStatus.SC_FORBIDDEN) { 85 attemptRetryOnException("auth", 86 request, new AuthFailureError(networkResponse)); 87 } else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 88 statusCode == HttpStatus.SC_MOVED_TEMPORARILY) { 89 attemptRetryOnException("redirect", 90 request, new AuthFailureError(networkResponse)); 91 } else { 92 // TODO: Only throw ServerError for 5xx status codes. 93 throw new ServerError(networkResponse); 94 } 95 } else { 96 throw new NetworkError(networkResponse); 97 } 98 } 99 } 100 }
而且這個方法有兩種訪問服務器的方法,一種是基於HttpClient去實現的,一種是HttpURLConnection去實現的。這么做是為了適配不同Sdk版本的對網絡請求的要求。在創建BasicNetwork的時候,會判斷當前的版本,去選擇哪種方式。在Volley類的靜態方法里面進行判斷。源碼如下:
1 public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
2 File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); 3 4 String userAgent = "volley/0"; 5 try { 6 String packageName = context.getPackageName(); 7 PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); 8 userAgent = packageName + "/" + info.versionCode; 9 } catch (NameNotFoundException e) { 10 } 11 12 if (stack == null) { 13 if (Build.VERSION.SDK_INT >= 9) { 14 stack = new HurlStack(); 15 } else { 16 // Prior to Gingerbread, HttpUrlConnection was unreliable. 17 // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html 18 stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); 19 } 20 } 21 22 Network network = new BasicNetwork(stack); 23 24 RequestQueue queue; 25 if (maxDiskCacheBytes <= -1) 26 { 27 // No maximum size specified 28 queue = new RequestQueue(new DiskBasedCache(cacheDir), network); 29 } 30 else 31 { 32 // Disk cache size specified 33 queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network); 34 } 35 36 queue.start(); 37 38 return queue; 39 }
1 @Override 2 public synchronized void put(String key, Entry entry) { 3 pruneIfNeeded(entry.data.length); 4 File file = getFileForKey(key); 5 try { 6 FileOutputStream fos = new FileOutputStream(file); 7 CacheHeader e = new CacheHeader(key, entry); 8 boolean success = e.writeHeader(fos); 9 if (!success) { 10 fos.close(); 11 VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); 12 throw new IOException(); 13 } 14 fos.write(entry.data); 15 fos.close(); 16 putEntry(key, e); 17 return; 18 } catch (IOException e) { 19 } 20 boolean deleted = file.delete(); 21 if (!deleted) { 22 VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); 23 } 24 }
1 @Override 2 public synchronized Entry get(String key) { 3 CacheHeader entry = mEntries.get(key); 4 // if the entry does not exist, return. 5 if (entry == null) { 6 return null; 7 } 8 9 File file = getFileForKey(key); 10 CountingInputStream cis = null; 11 try { 12 cis = new CountingInputStream(new FileInputStream(file)); 13 CacheHeader.readHeader(cis); // eat header 14 byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); 15 return entry.toCacheEntry(data); 16 } catch (IOException e) { 17 VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); 18 remove(key); 19 return null; 20 } finally { 21 if (cis != null) { 22 try { 23 cis.close(); 24 } catch (IOException ioe) { 25 return null; 26 } 27 } 28 } 29 }
@Override public void run() { if (DEBUG) VolleyLog.v("start new dispatcher"); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // Make a blocking call to initialize the cache. mCache.initialize(); while (true) { try { // Get a request from the cache triage queue, blocking until // at least one is available. final Request<?> request = mCacheQueue.take(); request.addMarker("cache-queue-take"); // If the request has been canceled, don't bother dispatching it. if (request.isCanceled()) { request.finish("cache-discard-canceled"); continue; } // Attempt to retrieve this item from cache. Cache.Entry entry = mCache.get(request.getCacheKey()); if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the network dispatcher. mNetworkQueue.put(request); continue; } // If it is completely expired, just send it to the network. if (entry.isExpired()) { request.addMarker("cache-hit-expired"); request.setCacheEntry(entry); mNetworkQueue.put(request); continue; } // We have a cache hit; parse its data for delivery back to the request. request.addMarker("cache-hit"); Response<?> response = request.parseNetworkResponse( new NetworkResponse(entry.data, entry.responseHeaders)); request.addMarker("cache-hit-parsed"); if (!entry.refreshNeeded()) { // Completely unexpired cache hit. Just deliver the response. mDelivery.postResponse(request, response); } else { // Soft-expired cache hit. We can deliver the cached response, // but we need to also send the request to the network for // refreshing. request.addMarker("cache-hit-refresh-needed"); request.setCacheEntry(entry); // Mark the response as intermediate. response.intermediate = true; // Post the intermediate response back to the user and have // the delivery then forward the request along to the network. mDelivery.postResponse(request, response, new Runnable() { @Override public void run() { try { mNetworkQueue.put(request); } catch (InterruptedException e) { // Not much we can do about this. } } }); } } catch (InterruptedException e) { // We may have been interrupted because it was time to quit. if (mQuit) { return; } continue; } } }
在閱讀的時候,注意下request.addMarker("*********");這些方法的調用,其實是對request設置一些標簽,這個在后面會依據這些標簽,對request的狀態進行判斷,主要的思路就是先對請求的任務在磁盤中查找,查找不到就將這個請求任務put進網絡請求的隊列中(在這里我們可以對這個框架進行優化,在堆磁盤查找的前,我們可以用一個數據結構來將一些數據保存在內存中,每次查找時先查詢內存緩存,沒有再在磁盤中查找。)request.addMarker("cache-hit-expired");這個就是標志着在磁盤中沒有該緩存,所以接下來的代碼會將請求添加到網絡請求隊列中。 mNetworkQueue.put(request);
1 /** 2 * A Runnable used for delivering network responses to a listener on the 3 * main thread. 4 */ 5 @SuppressWarnings("rawtypes") 6 private class ResponseDeliveryRunnable implements Runnable { 7 private final Request mRequest; 8 private final Response mResponse; 9 private final Runnable mRunnable; 10 11 public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { 12 mRequest = request; 13 mResponse = response; 14 mRunnable = runnable; 15 } 16 17 @SuppressWarnings("unchecked") 18 @Override 19 public void run() { 20 // If this request has canceled, finish it and don't deliver. 21 if (mRequest.isCanceled()) { 22 mRequest.finish("canceled-at-delivery"); 23 return; 24 } 25 26 // Deliver a normal response or error, depending. 27 if (mResponse.isSuccess()) { 28 mRequest.deliverResponse(mResponse.result); 29 } else { 30 mRequest.deliverError(mResponse.error); 31 } 32 33 // If this is an intermediate response, add a marker, otherwise we're done 34 // and the request can be finished. 35 if (mResponse.intermediate) { 36 mRequest.addMarker("intermediate-response"); 37 } else { 38 mRequest.finish("done"); 39 } 40 41 // If we have been provided a post-delivery runnable, run it. 42 if (mRunnable != null) { 43 mRunnable.run(); 44 } 45 } 46 }
值得注意的是,ExecutorDelivery對象里面還有一個handler,它是獲取了主線程的looper,主要是為了及時實現與UI界面進行交互。比如在我們請求獲取圖片類型數據的時候。
Volley框架的不足:
- 它只適用於頻繁而數據量小的請求。(當請求的是比較大的數據時,這個Volley框架發揮的作用就不是很大了。)
- 它只實現了一級緩存(磁盤緩存)。(這樣做雖然節省內存資源的消耗,但是讀寫數據的速度會比較慢。我們可以在這個基礎上進行優化,多加一級內存緩存,實現多級緩存。讀者可以自行對這個框架進行優化。對於緩存機制,個人覺得可以學習下Universal image loader這個框架,里面有幾種緩存機制寫得很好。)。
好了,以上就是我對Volley框架的學習和理解,希望這篇文章能幫到那些對Volley這個框架還不是很熟悉的朋友們,當然也希望可以拋磚引玉的作用。大家對這篇文章有什么看法或者評價,歡迎評論交流交流。
