相比新的網路請求框架Volley真的很落後,一無是處嗎,要知道Volley是由google官方推出的,雖然推出的時間很久了,但是其中依然有值得學習的地方。 從命名我們就能看出一些端倪,volley中文意為群射,齊射,官方解釋說它適合通信頻繁但是數據量不大的網路請求操作( a burst or emi ...
相比新的網路請求框架Volley真的很落後,一無是處嗎,要知道Volley是由google官方推出的,雖然推出的時間很久了,但是其中依然有值得學習的地方。 從命名我們就能看出一些端倪,volley中文意為群射,齊射,官方解釋說它適合通信頻繁但是數據量不大的網路請求操作( a burst or emission of many things or a large amount at once ),至於為什麼我們解讀完源碼就知道了。
回想下使用Volley的過程:比如請求一個網頁的內容。
1. 創建RequestQueue對象
RequestQueue mQueue = Volley.newRequestQueue(MyApplication.getInstance());
2. 先創建一個StringRequest對象
private StringRequest stringRequest = new StringRequest( Request.Method.GET, "https://www.baidu.com", new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d(TAG, "current thread :" + Thread.currentThread().getName()); // main thread ((TextView)findViewById(R.id.content)).setText(response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { Log.d(TAG, "error :" + error.getMessage()); } } ) ;View Code
3. 將請求對象添加到mQueue中
mQueue.add(stringRequest);
如下流程描述請自行結合Volley中的源碼閱讀(需要說明的是本文分析的Volley代碼不是最新版本,還是1.0.x的版本):
請求執行流程:
首先我們要構造RequestQueue, 其內部封裝了緩存請求隊列:
首先我們要構造RequestQueue, 其內部封裝了緩存請求隊列PriorityBlockingQueue<Request<?>> mCacheQueue 和網路請求隊列 PriorityBlockingQueue<Request<?>> mNetworkQueue,同時也封裝了一條緩存調度線程mCacheDispatcher和若幹條網路請求調度線程 NetworkDispatcher[] mDispatchers,雖然RequestQueue的構造方法是public,但是我們還是調用Volley的newRequestQueue方法,因為在newRequestQueue方法有些重要的處理,比如設置DiskBasedCache的目錄, 添加請求的User-agent,判斷SDK的版本號,如果是2.3(API=9)以下則使用HttpClient, 如果是>=2.3的版本,則使用HttpUrlConnection,接著構建RequestQueue對象,並調用其start方法,創建並啟動緩存調度線程和網路請求調度線程,目前的版本是1條緩存線程和4條網路請求線程。
接著查看RequestQueue.add的相關邏輯:
將構造的Request添加到RequestQueue中,即調用RequestQueue.add方法,這裡會將請求先Add到一個Set集合中,即Set<Request<?>> mCurrentRequests中,然後判斷是否禁用了緩存,如果禁用緩存則直接添加到mNetworkQueue中, 又因為NetworkDispatcher調度線程run方法中是while死迴圈,會一直取隊列中的對象,故加入網路請求隊列後,就相當於直接發起了網路請求。 而如果允許緩存,即Request.shouldCache返回true,則判斷Map(Map<String,Queue<Request<?>> mWaitingRequests中是否有相同的請求,判斷的標準就是請求的url,即request.getCacheKey()),如果mWaitingRequests中存在,則做提示處理,如果不存在則將請求添加到map中做記錄,並執行mCacheQueue.add(request)
請求加入了CacheQueue隊列中,則緩存調度線程就可以從隊列中取出requeset做處理。查看緩存調度線程CacheDispatcher的run方法,while迴圈中的邏輯如下,先取出緩存queue中的請求對象request,根據請求的url得到cache, 判斷cache中entry是否為空,如果為空則說明沒有緩存,則將請求添加到mNetworkQueue中,mNetworkQueue.put(request), 交由網路請求線程處理。如果有緩存,判斷緩存是否過期,如果過期則同上,如果緩存可用,則取出緩存中數據做解析並返回,即調用request.parseNetworkResponse方法,解析之後調用mDelivery.postResponse方法做結果的投遞,這裡就將操作從子線程轉移到主線程了,具體是由mDelivery去處理切換的操作, mDelivery(具體實現類是ExecutorDelivery)內部封裝了Handler和Executor,將最終解析出的結果投遞到主線程handler.post(runnable), 此handler是主線程的handler,構造RequestQueue隊列時創建了主線程的Handler對象了,代碼如下:
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
5. 當請求添加到網路請求隊列queue之後,在NetworkDispatcher的run方法中執行真正的網路請求,首先會判斷線程是否退出了,或者request是否被取消了等邏輯,一切ok則執行mNetwork.performRequest(request),發起網路請求,然後解析結果,做緩存操作,派發解析結果到主線程等等
// 這裡註意BlockingQueue的add offer put//// remove poll take peek等方法的區別
1.add 將元素插入queue中,如果立即可行且不違反容量規則返回true,如果當前沒有可用空間,則拋出IllegalStateExecption
2.offer 與add方法類似,但是使用有限制容量的queue時,此方法通常優於add方法,後者可能可能無法插入元素,只是拋出一個異常
3. put 插入元素到queue尾部,如果空間不夠,則等待空間變得可用
-----------------------------------------------------------------------------------------------------------------------------------
4. remove 移除元素,返回true如果queue總包含此元素
5.poll 獲取並移除頭部元素, E poll(), 如果queue為空,則返回null
6.take 獲取並移除頭部元素,如果沒有則等待直到有頭部元素變得可用, E take() throws InterruptedException。
7.peek 只是獲取頭部元素,並不做移除操作,如果queue為空,則返回null。
緩存執行流程
上面簡要分析了請求執行的過程,那麼Volley是如何實現緩存和獲取緩存的呢,我們接著分析,試想我們第一次請求某個網路資源時,必然是沒有緩存的,那麼最終會走到網路調用線程NetworkDispatcher run方法中的邏輯,執行網路請求拿到NetworkResponse,然後解析networkResponse,即調用request的parseNetworkResponse得到Response對象,然後判斷request是否允許緩存,如果需要緩存且response中的Cache.Entry即緩存對象不為空,則做緩存的操作。Cache.Entry對象cacheEntry什麼時候被賦值的呢?就是在parseNetworkResponse返回Response對象的過程中,構造Response對象調用Response.success(result, HttpHeaderParser.parseCacheHeaders(response));, success函數的第二參數即為cacheEntry,查看parseCacheHeaders方法可以看到,entry中包含有data, etag,softTtl,lastModified,responseHeaders等數據。我們要緩存就是上邊的cacheEntry,對應代碼中的mCache.put(request.getCacheKey(), response.cacheEntry); 這裡的mCache又是什麼呢。查找mCache的源頭又回到了Volley.newRequestQueue方法中,這裡構建RequestQueue時傳入了DiskBasedCache,那麼看來mCache的具體實現類就是DiskBasedCache了。查看DiskBasedCache的源碼,可以看到其預設緩存路徑是/data/data/packagename/cache/volley/ , 預設的緩存大小為10M,其中最關鍵的就是put方法,put(String key, Entry entry) ,此方法首先會根據entry中data數組的長度判斷是否能夠緩存得下,也就是緩存後是否超過了設定的最大緩存容量值。具體在pruneINeed中做判斷,如果超過最大值,則會按順序依次從已緩存的文件中做刪除操作(PS:如何做到按順序刪除呢,因為在putEntry方法中將key和cacheHeader的信息存儲在了LinkedHashMap中了, 所以刪除的時候才能依次按照緩存的先後順序刪除,最先緩存的先被刪除掉),直到緩存本次data不再超過最大值為止,然後創建一個File對象存儲緩存數據,File的name是將Url字元串的前半部分的hashcode加上字元串後半部分的hashcode組合而成,具體請查看getFilenameForKey(String key)方法,然後構建FileOutputStream對象分別將CacheHeader信息和data數據部分信息寫入文件,如果寫入的過程中發生了異常,則會做刪除文件的處理。至於讀取的操作請查看get方法.
ClearCacheRequest請求執行流程
可以看到在toolbox包下有一個ClearCacheRequest的類,看名字大概能猜測出來它是做清除緩存操作的。因為我們已經知道在Volley中的緩存邏輯是在DiskBasedCache中,查看DiskBasedCache中的的代碼,可以找個一個clear方法, 我們可以在此方法的第一行打上斷點,然後構造一個ClearCacheRequest對象,並添加到請求隊列中(在構造ClearCacheRequest方法中需要傳遞兩個參數,一個是mCache,一個是Runnable,其實mCache就是我們內部實現緩存的引用,Runnable可以做Clear後主線程上的操作), 啟動調試模式,可以看到其執行流程 CacheDispatcher.run --- > ClearCacheReqeuest.isCanceled -->
DiskBasedCache.clear方法,其中ClearCacheRequest的isCanceled方法與其他xxxRequest的isCancled方法不同,其內部調用了mCache.clear() ,,並將Runnable對象投遞到主線程的消息隊列中,如果mCallback不為空的話。在DiskBasedCache的clear方法中則分別做了對文件緩存刪除 和對記憶體緩存mEntries clear的操作。
網路請求流程
發起網路請求的邏輯在BasicNetwork的performRequest方法中,我們可以看到方法內部使用的是while死迴圈也就是說要麼得到請求的結果,要麼拋出異常。 而使用while迴圈也是重試機制的關鍵。 先看下大致的流程, 添加請求的header (這裡會從CacheHeader中獲取,如果entry不為空,取出etag,headers.put("If-None-Match", etag, 取出lastModified,headers.put("If-Modified-Since", lastModified)) --> 發起網路請求 mHttpStack.performRequest --> 得到response ---> 解析response --> 返回NetworkResponse。 如果返回的狀態碼statusCode == 304 ,那麼說明伺服器在對比etag和lastModified後發現資源沒有修改過,客戶端直接使用緩存即可, 如果返回的狀態碼是301或302,則說明請求的資源移動了位置,需要重定向,我們取出響應頭中的location信息,調用request.setRedirectUrl(url), 而後由於邏輯的處理返回的狀態碼不是2XX則會拋出IOException異常, 在catch的處理中會再次判斷狀態碼並調用attemptRetryOnException,而此方法中的預設重試代理是DefaultRetryPolicy, 那麼這個RetryPolicy是在哪設置的呢,查看Request的構造方法不難發現, 其中有setRetryPolicy(new DefaultRetryPolicy()) 的身影, 其retry方法中會對重試次數做判斷,如果超過最大重試次數,則拋出異常,那麼performRequest方法也會終止執行,如果小於等於最大重試次數則while迴圈的邏輯會再次執行,直到有結果。 其中需要註意到一點, 因為預設的連接超時時間較短只有2500ms,(不管是HttpClientStack的PerformRequest方法還是HurlStack的openConnection方法都會拿到request中設置的超時時間 int time = request.getTimeOutMs();)在國內複雜的網路環境中可能從發起請求到響應時間會超過此值,一旦超過此值Volley預設則認為是超時了,從而觸發重試的機制,導致一個請求發送兩次的情況。解決的辦法是可以增大預設超時的時間值,比如設置5000ms,或者設置不使用重試機制。
request.setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); 關於這個問題Volley的github庫issue中也有提及:https://github.com/google/volley/issues/7
其實說了這麼多,還是下麵這張流程圖的內容:
現在來做下問題總結:
1. 為什麼說Volley不適合大文件的下載等操作,而是數據量小的通信網路場景?
因為從Volley的源碼中我們可以發現,其內部執行網路請求的線程是固定數量4條線程,如果下載大文件可能就會導致線程被長時間占用,後面排隊的Request可能長時間得不到執行,且在Volley內部有緩存機制,如果大文件也允許緩存,而設定的最大緩存容量值較小,則可能發生長時間的IO操作(因為可能超過最大容量而要做刪除文件操作),導致應用性能下降。
2. Volley中的緩存調度線程和網路調用線程的run方法中是while死迴圈,什麼時候退出,也就是緩存和網路調度線程什麼時候結束工作?
其實在run方法的內部有相關邏輯, 比如NetworkDispatcher的run方法中,會捕獲InterruptedException異常,在異常處理中判斷mQuit的值,如果為true則直接返回。而調用Interrupt方法和設置mQuit值的處理就在NetworkDispatcher對應的quit() 方法中。
3. 可否將處理網路請求的線程改成線程池ThreadPoolExecutor?
可以改,但是即使改為線程池實現,性能可能也不會有提升,一方面對於手機cpu來說其核心數是有限的,如果線程池內的線程數配置的較大,則網路請求時可能導致線程的頻繁的發生切換,而線程的切換是有開銷的。
4. Volley可否載入較大的圖片,比如十幾M,幾十M等?
因為Volley中解析完數據是要保存在byte[] data,中的,所以如果數據過大則有可能發生OOM異常。https://github.com/google/volley/issues/12
5. 使用Volley時應該在哪裡創建RequestQueue合適?
具體可以在自定義的Application中,主要是傳遞給newRequestQueue的Context應該使用ApplicationContext,這樣可以避免可能發生的記憶體泄漏的情況,試想如果持有Activity的context那麼Volley內部的工作沒有做完則一直持有Activity,導致Activity無法釋放,故在自定義的Apllication初始化一個全局的請求隊列即可。
6. onResponse是在主線程中執行,但是返回結果後還需要做耗時操作怎麼辦?
從Volley的源碼中我們能夠知道派發器mDelivery的是ExecutorDelivery,其預設實現是傳遞主線程的handler的構造方法,而ExecutorDelivery的內部還有一個傳遞executor的構造方法,只要構建一個的executor,在new RequestQueue時,讓 mDelivery = new ExecutorDelivery(executor), 那麼onResponse最終就在executor的線程中執行, 不再是主線程了。
7. 如何取消某個或者多個網路請求?
取消單個request可以調用request.cancel(), 如果是多個可以給某個類別的request設置一個tag,想要取消請求調用requestQueue.cancelAll(tag),調用cancel方法後Request內的屬性mCanneled即被覆製為true,在CacheDispatcher或者NetworkDispatcher的run方法中會對request.isCanceled做判斷。如果是取消多個請求,調用cancelAll 方法,則會在當前的請求集合中進行遍歷,找到tag一致的request。
7. Volley有什麼優缺點。
優點:
還是那句: 適合網路通信頻繁,但是通信數據量不大的請求,不適合大文件的下載。
可以緩存http請求,過濾重覆請求(一般網路請求框架也都支持)
支持請求的優先順序
支持取消請求的API,可以取消單個請求,也可以設置取消請求的範圍域
基於介面的設計,使擴展相對容易(比如寫一個XMLRequest類 繼承Request,實現onResponse方法和parseNetworkResponse方法)
缺點:
對於文件的上傳和下載支持的不好
與Apache的Httpclient 和 HttpUrlConnection耦合較緊密
Android 6.0系統移除對HttpClient的支持,所以要使用Volley,需要配置org.apache.http.legacy.jar的引用
https://github.com/google/volley/releases 最新的Volley是1.1.0的版本,修複瞭如下問題:
- Apache HTTP is now an optional dependency (#2). See Migrating from Apache HTTP for details on how to avoid using it.
- Fix OutOfMemoryErrors and NegativeArraySizeExceptions in DiskBasedCache (#12).
- Fix memory leak in Request#mErrorListener (#15).
- Support for multiple identical response headers (#21).
- Fix potential NullPointerException in ImageRequest/JsonRequest/StringRequest (#64).
- Fix soft TTL for duplicate in-flight requests (#73).
- Fix case-sensitive header reads from cache (#76).
待補充。。。