Android——Volley框架學習總結


轉載請注明本文出自changel的博客(http://www.cnblogs.com/caichongyang/p/4399790.html ),請尊重他人的辛勤勞動成果,謝謝!

Volley框架特點:

  • 適用於頻繁請求而每次請求數據量不會很大;
  • 在請求的基礎上做了磁盤緩存;
  • 防止多次相同請求浪費資源;
  • 提供String、Json、圖片異步下載;
  • 網絡請求的優先級處理;
  • 圖片請求無需擔心生命周期問題。

Volley框架使用:

  1. 首先,通過Volley的靜態方法new一個請求隊列
    1 RequestQueue mQueue = Volley.newRequestQueue(context); 
  2. 假如我們創建一個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);  
                                }  
                            });  
  3. 將XXXRequest對象添加進隊列中
    mQueue.add(stringRequest); 
  4. 調用RequestQueue的start方法就可以開始一條網絡請求了
    mQueue.start();
  5. 當然我們可以設置請求的方式:
    StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener); 
  6. 設置提交的參數:
    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; } }; 
  7. 至於其他的請求,讀者可以自己去嘗試。比如圖片和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對象,通過構造函數,將兩個接口對象傳遞進去,進行綁定。

一個是當請求成功返回的數據,通過回調這個接口對象的方法傳遞出來。一個是回調顯示錯誤信息的接口。
 
接下來將該請求對象添加進RequestQueue中,其實內部是保存在來一個按優先級排序的PriorityBlockingQueue的對象中。以下是源碼
 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()的抽象方法,這個方法是對請求返回的數據進行解析。我們可以將數據封裝為一個對象,進行使用。

比如StringRequest實現的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 }

 

而Cache接口定義了一些讀取和寫入文件的方法,具體的實現類是DiskBasedCache,主要是用來查詢磁盤中有沒有對應的請求數據還有對第一次請求回來的數據進行存儲。主要有兩個方法,一個是put(),還有一個是get();
 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     }
 
其實在每次請求網絡的時候,都是從mNetworkQueue里面取出任務然后進行請求的,而mNetworkQueue里面的對象是怎么來的? 他其實是在CacheDispatcher執行的時候,添加進來的。因為mNetworkQueue是PriorityBlockingQueue類型的,該類型對象的操作是線程安全的(通過ReentrantLock加鎖),而且是阻塞型的,只要queue里面沒有對象,那么再take()的時候就會阻塞在那里。這也解釋了為什么用一個循環,不斷的向隊列里面取出請求對象的問題。大家仔細看下CacheDispatcher的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 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);

NetworkDispatcher的run()方法和CacheDispatcher很相似,這里就不貼出來了。讀者可以自己查看理解。
而當通過請求網絡獲取數據,或者從緩存中讀取數據,最后都是將數據包裝成Response類型的,然后調用request實現的parseNetworkResponse進行解析,最后將解析后返回的Response類型的對象,傳遞給ExecutorDelivery的對象,這個類實現了ResponseDelivery接口。
這個類的主要的功能就是去判斷獲取數據是否成功並且通過異步的方式去通知請求者,成功的話就把獲取得到信息通過調用request的deliverResponse方法傳遞出去,在deliverResponse這個方法里面再通過之前綁定的接口將信息傳遞在最外面。我們在外面實現了該接口的方法里就能拿到這些信息。如果獲取失敗的話,則調用request的deliverError,將錯誤信息傳遞出去,最后也是通過調用接口對象的方法,將這些信息顯示出來。
下面試的的一個實現了Runnable接口的對象,里面的成員方法就是回調了request對象中的mRequest.deliverResponse(mResponse.result);或者 mRequest.deliverError(mResponse.error);的方法將結果返回到外面。
 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這個框架還不是很熟悉的朋友們,當然也希望可以拋磚引玉的作用。大家對這篇文章有什么看法或者評價,歡迎評論交流交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM