天天看點

volley請求原理 Volley 實作原了解析

volley 是 google 推出的 android 異步網絡請求架構和圖檔加載架構。在 google i/o 2013 大會上釋出。

名字由來:a burst or emission of many things or a large amount at once 釋出演講時候的配圖
volley請求原理 Volley 實作原了解析

從名字由來和配圖中無數急促的火箭可以看出 volley 的特點:特别适合資料量小,通信頻繁的網絡操作。(個人認為 android 應用中絕大多數的網絡操作都屬于這種類型)。

(1). 擴充性強。volley 中大多是基于接口的設計,可配置性強。

(2). 一定程度符合 http 規範,包括傳回 responsecode(2xx、3xx、4xx、5xx)的處理,請求頭的處理,緩存機制的支援等。并支援重試及優先級定義。

(3). 預設 android2.3 及以上基于 httpurlconnection,2.3 以下基于 httpclient 實作,這兩者的差別及優劣在<code>4.2.1 volley</code>中具體介紹。

(4). 提供簡便的圖檔加載工具。

volley請求原理 Volley 實作原了解析

上面是 volley 的總體設計圖,主要是通過兩種<code>diapatch thread</code>不斷從<code>requestqueue</code>中取出請求,根據是否已緩存調用<code>cache</code>或<code>network</code>這兩類資料擷取接口之一,從記憶體緩存或是伺服器取得請求的資料,然後交由<code>responsedelivery</code>去做結果分發及回調處理。

簡單介紹一些概念,在<code>詳細設計</code>中會仔細介紹。

volley 的調用比較簡單,通過 newrequestqueue(…) 函數建立并啟動一個請求隊列<code>requestqueue</code>後,隻需要往這個<code>requestqueue</code>不斷

add request 即可。

volley:volley 對外暴露的 api,通過 newrequestqueue(…) 函數建立并啟動一個請求隊列<code>requestqueue</code>。

request:表示一個請求的抽象類。<code>stringrequest</code>、<code>jsonrequest</code>、<code>imagerequest</code> 都是它的子類,表示某種類型的請求。

requestqueue:表示請求隊列,裡面包含一個<code>cachedispatcher</code>(用于處理走緩存請求的排程線程)、<code>networkdispatcher</code>數組(用于處理走網絡請求的排程線程),一個<code>responsedelivery</code>(傳回結果分發接口),通過

start() 函數啟動時會啟動<code>cachedispatcher</code>和<code>networkdispatchers</code>。

cachedispatcher:一個線程,用于排程處理走緩存的請求。啟動後會不斷從緩存請求隊列中取請求處理,隊列為空則等待,請求處理結束則将結果傳遞給<code>responsedelivery</code>去執行後續處理。當結果未緩存過、緩存失效或緩存需要重新整理的情況下,該請求都需要重新進入<code>networkdispatcher</code>去排程處理。

networkdispatcher:一個線程,用于排程處理走網絡的請求。啟動後會不斷從網絡請求隊列中取請求處理,隊列為空則等待,請求處理結束則将結果傳遞給<code>responsedelivery</code>去執行後續處理,并判斷結果是否要進行緩存。

responsedelivery:傳回結果分發接口,目前隻有基于<code>executordelivery</code>的在入參 handler 對應線程内進行分發。

httpstack:處理 http 請求,傳回請求結果。目前 volley 中有基于 httpurlconnection 的<code>hurlstack</code>和 基于 apache httpclient 的<code>httpclientstack</code>。

network:調用<code>httpstack</code>處理請求,并将結果轉換為可被<code>responsedelivery</code>處理的<code>networkresponse</code>。

cache:緩存請求結果,volley 預設使用的是基于 sdcard 的<code>diskbasedcache</code>。<code>networkdispatcher</code>得到請求結果後判斷是否需要存儲在

cache,<code>cachedispatcher</code>會從 cache 中取緩存結果。

volley 請求流程圖

volley請求原理 Volley 實作原了解析
上圖是 volley 請求時的流程圖,在 volley 的釋出演講中給出,我在這裡将其用中文重新畫出。
volley請求原理 Volley 實作原了解析

這是 volley 架構的主要類關系圖

圖中紅色圈内的部分,組成了 volley 架構的核心,圍繞 requestqueue 類,将各個功能點以組合的方式結合在了一起。各個功能點也都是以接口或者抽象類的形式提供。 紅色圈外面的部分,在 volley 源碼中放在了toolbox包中,作為 volley 為各個功能點提供的預設的具體實作。 通過類圖我們看出, volley 有着非常好的拓展性。通過各個功能點的接口,我們可以給出自定義的,更符合我們需求的具體實作。 多用組合,少用繼承;針對接口程式設計,不針對具體實作程式設計。 優秀架構的設計,令人叫絕,受益良多。

這個和 volley 架構同名的類,其實是個工具類,作用是建構一個可用于添加網絡請求的<code>requestqueue</code>對象。

(1). 主要函數

volley.java 有兩個重載的靜态方法。

第一個方法的實作調用了第二個方法,傳 httpstack 參數為 null。

第二個方法中,如果 httpstatck 參數為 null,則如果系統在 gingerbread 及之後(即 api level &gt;= 9),采用基于 httpurlconnection 的 hurlstack,如果小于 9,采用基于 httpclient 的 httpclientstack。

得到了 httpstack,然後通過它構造一個代表網絡(network)的具體實作<code>basicnetwork</code>。

接着構造一個代表緩存(cache)的基于 disk 的具體實作<code>diskbasedcache</code>。

最後将網絡(network)對象和緩存(cache)對象傳入建構一個 requestqueue,啟動這個 requestqueue,并傳回。

我們平時大多采用<code>volly.newrequestqueue(context)</code>的預設實作,建構requestqueue。 通過源碼可以看出,我們可以抛開 volley 工具類建構自定義的requestqueue,采用自定義的<code>httpstatck</code>,采用自定義的<code>network</code>實作,采用自定義的cache實作等來建構<code>requestqueue</code>。 優秀架構的高可拓展性的魅力來源于此啊

(2). 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。

(3). 關于 user agent

通過代碼我們發現如果是使用 androidhttpclient,volley 還會将請求頭中的 user-agent 字段設定為 app 的 ${packagename}/${versioncode},如果異常則使用 "volley/0",不過這個擷取 user-agent 的操作應該放到 if else 内部更合适。而對于 httpurlconnection 卻沒有任何操作,為什麼呢?

經常用 webview 的同學會也許會發現似曾相識,是的,webview 預設的 user-agent 也是這個。實際在請求發出之前,會檢測 user-agent 是否為空,如果不為空,則加上系統預設 user-agent。在 android 2.1 之後,我們可以通過

得到系統預設的 user-agent,volley 如果希望自定義 user-agent,可在自定義 request 中重寫 getheaders() 函數

代表一個網絡請求的抽象類。我們通過建構一個<code>request</code>類的非抽象子類(stringrequest、jsonrequest、imagerequest或自定義)對象,并将其加入到·requestqueue·中來完成一次網絡請求操作。

volley 支援 8 種 http 請求方式 get, post, put, delete, head, options, trace, patch

request 類中包含了請求 url,請求請求方式,請求 header,請求 body,請求的優先級等資訊。

因為是抽象類,子類必須重寫的兩個方法。

子類重寫此方法,将網絡傳回的原生位元組内容,轉換成合适的類型。此方法會在工作線程中被調用。

子類重寫此方法,将解析成合适類型的内容傳遞給它們的監聽回調。

以下兩個方法也經常會被重寫

重寫此方法,可以建構用于 post、put、patch 請求方式的 body 内容。

在上面<code>getbody</code>函數沒有被重寫情況下,此方法的傳回值會被 key、value 分别編碼後拼裝起來轉換為位元組碼作為 body 内容。

volley 架構的核心類,将請求request加入到一個運作的<code>requestqueue</code>中,來完成請求操作。

requestqueue 中維護了兩個基于優先級的 request 隊列,緩存請求隊列和網絡請求隊列。

放在緩存請求隊列中的 request,将通過緩存擷取資料;放在網絡請求隊列中的 request,将通過網絡擷取資料。

維護了一個正在進行中,尚未完成的請求集合。

維護了一個等待請求的集合,如果一個請求正在被處理并且可以被緩存,後續的相同 url 的請求,将進入此等待隊列。

建立出 requestqueue 以後,調用 start 方法,啟動隊列。

start 方法中,開啟一個緩存排程線程<code>cachedispatcher</code>和 n 個網絡排程線程<code>networkdispatcher</code>,這裡

n 預設為4,存在優化的餘地,比如可以根據 cpu 核數以及網絡類型計算更合适的并發數。

緩存排程線程不斷的從緩存請求隊列中取出 request 去處理,網絡排程線程不斷的從網絡請求隊列中取出 request 去處理。

流程圖如下:

volley請求原理 Volley 實作原了解析

request 請求結束

(1). 首先從正在進行中請求集合<code>mcurrentrequests</code>中移除該請求。 (2). 然後查找請求等待集合<code>mwaitingrequests</code>中是否存在等待的請求,如果存在,則将等待隊列移除,并将等待隊列所有的請求添加到緩存請求隊列中,讓緩存請求處理線程<code>cachedispatcher</code>自動處理。

取消目前請求集合中所有符合條件的請求。

filter 參數表示可以按照自定義的過濾器過濾需要取消的請求。

tag 表示按照<code>request.settag</code>設定好的 tag 取消請求,比如同屬于某個 activity 的。

一個線程,用于排程處理走緩存的請求。啟動後會不斷從緩存請求隊列中取請求處理,隊列為空則等待,請求處理結束則将結果傳遞給<code>responsedelivery</code> 去執行後續處理。當結果未緩存過、緩存失效或緩存需要重新整理的情況下,該請求都需要重新進入<code>networkdispatcher</code>去排程處理。

<code>blockingqueue&lt;request&lt;?&gt;&gt; mcachequeue</code> 緩存請求隊列

<code>blockingqueue&lt;request&lt;?&gt;&gt; mnetworkqueue</code> 網絡請求隊列

<code>cache mcache</code> 緩存類,代表了一個可以擷取請求結果,存儲請求結果的緩存

<code>responsedelivery mdelivery</code> 請求結果傳遞類

volley請求原理 Volley 實作原了解析

一個線程,用于排程處理走網絡的請求。啟動後會不斷從網絡請求隊列中取請求處理,隊列為空則等待,請求處理結束則将結果傳遞給 responsedelivery 去執行後續處理,并判斷結果是否要進行緩存。

<code>blockingqueue&lt;request&lt;?&gt;&gt; mqueue</code> 網絡請求隊列

<code>network mnetwork</code> 網絡類,代表了一個可以執行請求的網絡

<code>responsedelivery mdelivery</code> 請求結果傳遞類,可以傳遞請求的結果或者錯誤到調用者

volley請求原理 Volley 實作原了解析

緩存接口,代表了一個可以擷取請求結果,存儲請求結果的緩存。

<code>public entry get(string key);</code> 通過 key 擷取請求的緩存實體

<code>public void put(string key, entry entry);</code> 存入一個請求的緩存實體

<code>public void remove(string key);</code> 移除指定的緩存實體

<code>public void clear();</code> 清空緩存

成員變量和方法

<code>byte[] data</code> 請求傳回的資料(body 實體)

<code>string etag</code> http 響應首部中用于緩存新鮮度驗證的 etag

<code>long serverdate</code> http 響應首部中的響應産生時間

<code>long ttl</code> 緩存的過期時間

<code>long softttl</code> 緩存的新鮮時間

<code>map&lt;string, string&gt; responseheaders</code> 響應的 headers

<code>boolean isexpired()</code> 判斷緩存是否過期,過期緩存不能繼續使用

<code>boolean refreshneeded()</code> 判斷緩存是否新鮮,不新鮮的緩存需要發到服務端做新鮮度的檢測

繼承 cache 類,基于 disk 的緩存實作類。

<code>public synchronized void initialize()</code> 初始化,掃描緩存目錄得到所有緩存資料摘要資訊放入記憶體。

<code>public synchronized entry get(string key)</code> 從緩存中得到資料。先從摘要資訊中得到摘要資訊,然後讀取緩存資料檔案得到内容。

<code>public synchronized void put(string key, entry entry)</code> 将資料存入緩存内。先檢查緩存是否會滿,會則先删除緩存中部分資料,然後再建立緩存檔案。

<code>private void pruneifneeded(int neededspace)</code> 檢查是否能再配置設定 neededspace 位元組的空間,如果不能則删除緩存中部分資料。

<code>public synchronized void clear()</code> 清空緩存。 <code>public synchronized void remove(string key)</code> 删除緩存中某個元素。

cacheheader 是緩存檔案摘要資訊,存儲在緩存檔案的頭部,與上面的<code>cache.entry</code>相似。

繼承 cache 類,不做任何操作的緩存實作類,可将它作為建構<code>requestqueue</code>的參數以實作一個不帶緩存的請求隊列。

代表網絡的接口,處理網絡請求。

唯一的方法,用于執行特定請求。

<code>network</code>中方法 performrequest 的傳回值,<code>request</code>的

parsenetworkresponse(…) 方法入參,是 volley 中用于内部 response 轉換的一級。

封裝了網絡請求響應的 statuscode,headers 和 body 等。

<code>int statuscode</code> http 響應狀态碼

<code>byte[] data</code> body 資料

<code>map&lt;string, string&gt; headers</code> 響應 headers

<code>boolean notmodified</code> 表示是否為 304 響應

<code>long networktimems</code> 請求耗時

volley請求原理 Volley 實作原了解析

從上到下表示從得到資料後一步步的處理,箭頭旁的注釋表示該步處理後的實體類。

實作 network,volley 中預設的網絡接口實作類。調用<code>httpstack</code>處理請求,并将結果轉換為可被<code>responsedelivery</code>處理的<code>networkresponse</code>。

主要實作了以下功能:

(1). 利用 httpstack 執行網絡請求。

(2). 如果 request 中帶有實體資訊,如 etag,last-modify 等,則進行緩存新鮮度的驗證,并處理 304(not modify)響應。

(3). 如果發生逾時,認證失敗等錯誤,進行重試操作,直到成功、抛出異常(不滿足重試政策等)結束。

用于處理 http 請求,傳回請求結果的接口。目前 volley 中的實作有基于 httpurlconnection 的 hurlstack 和 基于 apache httpclient 的 httpclientstack。

唯一方法,執行請求

執行 request 代表的請求,第二個參數表示發起請求之前,添加額外的請求 headers。

實作 httpstack 接口,利用 apache 的 httpclient 進行各種請求方式的請求。

基本就是 org.apache.http 包下面相關類的常見用法,不做詳解,不過與下面 httpurlconnection 做下對比就能發現 httpurlconnection 的 api 相對簡單的多。

實作 httpstack 接口,利用 java 的 httpurlconnection 進行各種請求方式的請求。

封裝了經過解析後的資料,用于傳輸。并且有兩個内部接口 listener 和 errorlistener 分别可表示請求失敗和成功後的回調。

response 的構造函數被私有化,而通過兩個函數名更易懂的靜态方法建構對象。

byte[] 的回收池,用于 byte[] 的回收再利用,減少了記憶體的配置設定和回收。 主要通過一個元素長度從小到大排序的<code>arraylist</code>作為 byte[] 的緩存,另有一個按使用時間先後排序的<code>arraylist</code>屬性用于緩存滿時清理元素。

将用過的 byte[] 回收,根據 byte[] 長度按照從小到大的排序将 byte[] 插入到緩存中合适位置。

擷取長度不小于 len 的 byte[],周遊緩存,找出第一個長度大于傳入參數<code>len</code>的 byte[],并傳回;如果最終沒有合适的byte[],new 一個傳回。

當緩存的 byte 超過預先設定的大小時,按照先進先出的順序删除最早的 byte[]。

繼承bytearrayoutputstream,原始 bytearrayoutputstream 中用于接受寫入 bytes 的 buf,每次空間不足時便會 new 更大容量的 byte[],而 poolingbytearrayoutputstream 使用了 bytearraypool 作為 byte[] 緩存來減少這種操作,進而提高性能。

http header 的解析工具類,在 volley 中主要作用是用于解析 header 進而判斷傳回結果是否需要緩存,如果需要傳回 header 中相關資訊。

有三個方法

解析時間,将 rfc1123 的時間格式,解析成 epoch 時間

解析編碼集,在 content-type 首部中擷取編碼集,如果沒有找到,預設傳回 iso-8859-1

比較重要的方法,通過網絡響應中的緩存控制 header 和 body 内容,建構緩存實體。如果 header 的 cache-control 字段含有<code>no-cache</code>或<code>no-store</code>表示不緩存,傳回

null。

(1). 根據 date 首部,擷取響應生成時間

(2). 根據 etag 首部,擷取響應實體标簽

(3). 根據 cache-control 和 expires 首部,計算出緩存的過期時間,和緩存的新鮮度時間

兩點需要說明下: 1.沒有處理<code>last-modify</code>首部,而是處理存儲了<code>date</code>首部,并在後續的新鮮度驗證時,使用<code>date</code>來建構<code>if-modified-since</code>。 這與 http 1.1 的語義有些違背。 2.計算過期時間,cache-control 首部優先于 expires 首部。

重試政策接口

有三個方法:

擷取目前請求用時(用于log)

擷取已經重試的次數(用于log)

确定是否重試,參數為這次異常的具體資訊。在請求異常時此接口會被調用,可在此函數實作中抛出傳入的異常表示停止重試。

實作 retrypolicy,volley 預設的重試政策實作類。主要通過在 retry(…) 函數中判斷重試次數是否達到上限确定是否繼續重試。

其中<code>mcurrenttimeoutms</code>變量表示已經重試次數。

<code>mbackoffmultiplier</code>表示每次重試之前的 timeout 該乘以的因子。

<code>mcurrenttimeoutms</code>變量表示目前重試的 timeout 時間,會以<code>mbackoffmultiplier</code>作為因子累計前幾次重試的

timeout。

請求結果的傳輸接口,用于傳遞請求結果或者請求錯誤。

此方法用于傳遞請求結果,<code>request</code> 和 <code>response</code> 參數分别表示請求資訊和傳回結果資訊。

此方法用于傳遞請求結果,并在完成傳遞後執行 runnable。

此方法用于傳輸請求錯誤。

請求結果傳輸接口具體實作類。

在 handler 對應線程中傳輸緩存排程線程或者網絡排程線程中産生的請求結果或請求錯誤,會在請求成功的情況下調用 request.deliverresponse(…) 函數,失敗時調用 request.delivererror(…) 函數。

繼承 request 類,代表了一個傳回值為 string 的請求。将網絡傳回的結果資料解析為 string 類型。通過構造函數的 listener 傳參,支援請求成功後的 onresponse(…) 回調。

抽象類,繼承自 request,代表了 body 為 json 的請求。提供了建構 json 請求參數的方法。

繼承自 jsonrequest,将網絡傳回的結果資料解析為 jsonobject 類型。

繼承自 jsonrequest,将網絡傳回的結果資料解析為 jsonarray 類型。

繼承 request 類,代表了一個傳回值為 image 的請求。将網絡傳回的結果資料解析為 bitmap 類型。

可以設定圖檔的最大寬度和最大高度,并計算出合适尺寸傳回。每次最多解析一張圖檔防止 oom。

封裝了 imagerequst 的友善使用的圖檔加載工具類。

1.可以設定自定義的<code>imagecache</code>,可以是記憶體緩存,也可以是 disk 緩存,将擷取的圖檔緩存起來,重複利用,減少請求。 2.可以定義圖檔請求過程中顯示的圖檔和請求失敗後顯示的圖檔。 3.相同請求(相同位址,相同大小)隻發送一個,可以避免重複請求。 // todo

利用 imageloader,可以加載網絡圖檔的 imageview

有三個公開的方法:

設定預設圖檔,加載圖檔過程中顯示。

設定錯誤圖檔,加載圖檔失敗後顯示。

設定網絡圖檔的 url 和 imageloader,将利用這個 imageloader 去擷取網絡圖檔。

如果有新的圖檔加載請求,會把這個imageview上舊的加載請求取消。

用于人為清空 http 緩存的請求。

添加到 requestqueue 後能很快執行,因為優先級很高,為<code>priority.immediate</code>。并且清空緩存的方法<code>mcache.clear()</code>寫在了<code>iscanceled()</code>方法體中,能最早的得到執行。

clearcacherequest 的寫法不敢苟同,目前看來唯一的好處就是可以将清空緩存操作也當做一個請求。而在<code>iscanceled()</code>中做清空操作本身就造成了歧義,不看源碼沒人知道在<code>networkdispatcher</code> run

方法循環的過程中,<code>iscanceled()</code>這個讀操作竟然做了可能造成緩存被清空。隻能跟源碼的解釋一樣當做一個 hack 操作。

身份認證接口,用于基本認證或者摘要認證。這個類是 volley 用于和身份驗證打通的接口,比如 oauth,不過目前的使用不是特别廣泛和 volley 的内部結合也不是特别緊密。

繼承 authenticator,基于 android accountmanager 的認證互動實作類。

volley 的 log 工具類。

volley 中所有錯誤異常的父類,繼承自 exception,可通過此類設定和擷取 networkresponse 或者請求的耗時。

繼承自 volleyerror,代表請求認證失敗錯誤,如 respondecode 的 401 和 403。

繼承自 volleyerror,代表網絡錯誤。

繼承自 volleyerror,代表内容解析錯誤。

繼承自 volleyerror,代表服務端錯誤。

繼承自 volleyerror,代表請求逾時錯誤。

繼承自networkerror,代表無法建立連接配接錯誤。

volley 建構了一套相對完整的符合 http 語義的緩存機制。

優點和特點

(1). 根據<code>cache-control</code>和<code>expires</code>首部來計算緩存的過期時間。如果兩個首部都存在情況下,以<code>cache-control</code>為準。

(2). 利用<code>if-none-match</code>和<code>if-modified-since</code>對過期緩存或者不新鮮緩存,進行請求再驗證,并處理

304 響應,更新緩存。

(3). 預設的緩存實作,将緩存以檔案的形式存儲在 disk,程式退出後不會丢失。

我個人認為的不足之處

緩存的再驗證方面,在建構<code>if-modified-since</code>請求首部時,volley 使用了服務端響應的<code>date</code>首部,沒有使用<code>last-modified</code>首部。整個架構沒有使用<code>last-modified</code>首部。這與

http 語義不符。

服務端根據請求時通過<code>if-modified-since</code>首部傳過來的時間,判斷資源檔案是否在<code>if-modified-since</code>時間 以後 有改動,如果有改動,傳回新的請求結果。如果沒有改動,傳回

304 not modified。

<code>last-modified</code>代表了資源檔案的最後修改時間。通常使用這個首部建構<code>if-modified-since</code>的時間。

<code>date</code>代表了響應産生的時間,正常情況下<code>date</code>時間在<code>last-modified</code>時間之後。也就是<code>date</code>&gt;=<code>last-modified</code>。

通過以上原理,既然<code>date</code>&gt;=<code>last-modified</code>。那麼我利用<code>date</code>建構,也是完全正确的。

可能的問題出在服務端的 http 實作上,如果服務端完全遵守 http 語義,采用時間比較的方式來驗證<code>if-modified-since</code>,判斷伺服器資源檔案修改時間是不是在<code>if-modified-since</code>之後。那麼使用<code>date</code>完全正确。

可是有的服務端實作不是比較時間,而是直接的判斷伺服器資源檔案修改時間,是否和<code>if-modified-since</code>所傳時間相等。這樣使用<code>date</code>就不能實作正确的再驗證,因為<code>date</code>的時間總不會和伺服器資源檔案修改時間相等。

盡管使用<code>date</code>可能出現的不正确情況,歸結于服務端沒有正确的實作 http 語義。

但我還是希望volley也能完全正确的實作http語義,至少同時處理<code>last-modified</code>和<code>date</code>,并且優先使用<code>last-modified</code>。

如下代碼:

basicnetwork.performrequest(…) 最後的

應該是

更合理。