溫故而知新 Volley源碼解讀與思考,溫故而知新volley
相比新的網路請求架構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中的源碼閱讀:
請求執行流程:
首先我們要構造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不再超過最大值為止,然後建立一個FileObject Storage Service快取資料,File的name是將Url字串的前半部分的hashcode加上字串後半部分的hashcode組合而成,具體請查看getFilenameForKey(String key)方法,然後構建FileOutputStream對象分別將CacheHeader資訊和data資料部分資訊寫入檔案,如果寫入的過程中發生了異常,則會做刪除檔案的處理。至於讀取的操作請查看get方法.
網路請求流程
發起網路請求的邏輯在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).
待補充。。。