1. 程式人生 > >Volley原始碼分析一

Volley原始碼分析一

我有一篇文章很不負責的,沒有頭緒的分析了一些Volley的原始碼。我自己回頭去看了一下,於是就把他刪掉了,於是就有了今天的這篇文章。

Volley的使用步驟

  1. 建立一個RequestQueue物件。
  2. 建立一個Request物件。
  3. 將Request物件新增到RequestQueue裡面。

我們可以看到使用非常的簡單,順著這個邏輯我們在來看一遍原始碼

1.建立RequestQueue物件

RequestQueue mQueue = Volley.newRequestQueue(context);

在newRequestQueue方法中建立了一個Cache一個newWork,然後用這兩個建立了一個RequestQueue,隨後啟動這個請求佇列,這個佇列啟動的是5個執行緒(預設是1個處理走快取的請求的執行緒,4個處理網路請求的執行緒,其實這裡可以自己改,走快取的請求個人覺得1個就足夠,處理網路請求的執行緒數可以根據cpu核數等因素自己來確定一個合理值),在5個執行緒中主要的工作就是在自己的while迴圈中不斷的讀取佇列中的內容(1個走快取的請求佇列,之後都稱為A,1個走網路的請求佇列,之後都稱為B),也就是1個執行緒不斷從A中讀資料,4個執行緒不斷從B中讀資料。A、B中沒有資料的時候,也就是take拿到的資料為空時,take就會阻塞所在的執行緒,take不為空的話,就進行各自的處理,一個走快取,一個走網路。
走快取:
這裡寫圖片描述


走網路:
這裡寫圖片描述

其中一些細節可能大家不太明白,比如:走網路中的304響應、為什麼使用佇列不用執行緒池(其實是可以用執行緒池),像上面這些比較細枝末節的在之後會講解。

2.建立一個Request物件

這一部分是我當時讀原始碼的時候比較難受的一部分,裡面有很多我當時不知道什麼用的引數,有http請求報文相關的引數,而且和network緊密相連,而這個network對於我這種一開始http協議不怎麼了解的人來說簡直就是痛苦,什麼請求首部、請求主體,完全就是想要頭大,難怪很多人都是建議不要自己來寫網路請求框架,是有道理的不容易寫,更不容易寫好,我也就不談自己對於其中http請求實現的粗淺的理解,不誤人子弟了,有興趣大家可以和我一樣去啃http權威指南。對於我們想要去實現自己的請求,主要關注的就4個方法:

abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

子類重寫此方法,將網路返回的原生位元組內容,轉換成合適的型別。此方法會在工作執行緒中被呼叫。

abstract protected void deliverResponse(T response);

子類重寫此方法,將解析成合適型別的內容傳遞給它們的監聽回撥。

public byte[] getBody()
protected Map<String, String> getParams()

上述兩個方法就是寫請求主體的內容。像一般我們知道的post是需要有請求主體的

3.將Request物件add到RequestQueue裡面

這個將請求add到RequestQueue其實就沒什麼好講的,就是將請求新增到佇列中。但這裡還是展開具體的講一講比較能幫助理解,首先,執行新增的add函式之後,就將這個請求和這個請求佇列相關聯(方便之後結束請求),隨後將此請求新增到mCurrentRequests 這個集合中,我們看一下他的宣告

private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();

它的作用是維護了一個正在進行中,尚未完成的請求集合。因為是set所以其中的請求是不會重複的。
隨後給這個請求設定一個請求的標號,判斷請求是否要快取,要就加入到快取請求佇列,否則加入網路請求佇列,對於之後重複的請求(前提:要快取的)都加入到mWaitingRequests 集合中

private final Map<String, Queue<Request<?>>> mWaitingRequests = new HashMap<String, Queue<Request<?>>>();

我們再繼續講,將請求新增到請求佇列中之後,這個請求就會有很多種後續的可能:請求自然結束(就是請求成功了或者請求失敗,我們沒有手動的去結束這個請求)、主動結束(比如我們開啟一個activity,在其中建立了一個請求新增到佇列中,但是我們很快就離開了這個頁面,並且我們不想要這個請求還在佇列中佔用資源,因為它並不重要,於是我們就主動的結束這個請求),結束請求最終呼叫的是void finish(Request<?> request) 方法,首先從正在進行中請求集合mCurrentRequests中移除該請求。 然後查詢請求等待集合mWaitingRequests中是否存在等待的請求,如果存在,則將等待佇列移除,並將等待佇列所有的請求新增到快取請求佇列中,讓快取請求處理執行緒CacheDispatcher自動處理。
一開始我讀到這裡的時候會覺得(如果存在,則將等待佇列移除,並將等待佇列所有的請求新增到快取請求佇列中,讓快取請求處理執行緒CacheDispatcher自動處理。)這一步有問題,但仔細理解了之後其實是自己想的太少了,只要最後都執行了請求的finish也就是請求佇列的finish方法就不會有問題,所有的請求都會執行。

響應與分發

上面的內容我講完了建立請求將請求新增到請求佇列,並不斷從佇列中讀取請求進行處理,到這裡我們自然會問的就是之後呢?對啊!之後呢,請求之後得到響應,並將響應分發給請求者。

響應

響應可以使從網路中拿到資料然後響應,也可以是從Cache中拿到資料然後響應。
首先從簡單的開始說起,從Cache中拿到資料然後響應,拿資料的過程不太清楚的看之前的圖,拿到了資料之後就需要對其進行解析,使用的是request的parseNetworkResponse方法,這個是可以我們實現的(這也是Volley之所以好的體現之一吧,高擴充套件性!),如何解析呢,我們先來看看拿到的資料是什麼樣的:

byte[] data 請求返回的資料(Body 實體)

String etag Http響應首部中用於快取新鮮度驗證的 ETag

long serverDate Http 響應首部中的響應產生時間

long lastModified 所請求的物件上一次修改的時間

long ttl 快取的過期時間

long softTtl 快取的新鮮時間

Map<String, String> responseHeaders 響應的 Headers

boolean isExpired() 判斷快取是否過期,過期快取不能繼續使用

boolean refreshNeeded() 判斷快取是否新鮮,不新鮮的快取需要發到服務端做新鮮度的檢測

要解析的就是data 資料,如何解析就根據responseHeaders 和我們自己定義的parseNetworkResponse方法,最終將其轉換為我們指定的型別,之後要做的就是講這個解析得到的響應分發,這是下一部分的內容,下一部分再講。

注意注意!接下來就是要重點攻克的難關了!網路請求包括它的重試機制
這裡寫圖片描述

從網路中獲取請求的資料,取出一個請求之後執行Basicwork(或者自己實現,只要繼承Network介面就行)的performRequest方法,在其中首先取得那些快取了但是已經過期或者不新鮮了的請求的etag 和 lastModified 兩個資訊,連同使用者自己定義的請求頭部(預設的是 User-Agent 欄位設定為 App 的 packageName/{versionCode},就是在Volley.newRequestQueue方法中拿段話)一起新增到請求報文中,這部分是在httpstack物件的performRequest方法中,這個方法我就不具體展開了,就是一套流程下來(建立連線,設定超時時間等等),總之最後返回響應報文給Basicwork的performRequest方法,在其中解析這個響應報文,獲取起始行中的狀態碼和所有首部的資訊(以鍵值對的形式),根據不同的狀態碼執行不同的操作。下面以狀態碼分類做進一步的講解

304

如果是304,表示我們現在擁有的資料就是最新資料了,於是就直接拿取請求中儲存的快取資料建立NetworkResponse返回到NetworkDispatcher就行。

301或者302

301和302的意思就是現有的地址訪問不到資料需要重定向(應該是這麼說吧,關於http協議我也是菜鳥,有錯的話請大家積極的指正啊),至於重定向的地址就從響應報文的首部中獲取,首部的資訊一開始都解析出來放在了一個map中,拿到這個地址之後給請求設定重定向地址,隨後設定代表響應主體的引數的時候也就設定為空

200

如果是200,就直接設定代表響應主體的引數,在前面301或者302的時候是設定響應主體為空,因為就沒有得到資料,200的時候就不一樣了,他是成功返回資料,所以直接設定,只是要將HttpEntity物件轉換為二進位制陣列,這個轉換的過程後續也會再講,因為會牽扯到ByteArrayPool這個類,可以說是記憶體複用吧,所以值得單獨再講一講原理。好了,轉換成二進位制陣列之後建立NetworkResponse返回到NetworkDispatcher

小於200大於299

這些都會拋異常,但是要注意,這一步判斷是在上述判斷之後,所以304是不會執行到這一步判斷的,304在這之前就返回了NetworkResponse,而301和302則會丟擲異常,在他們的判斷之中做的只是得到重定向的地址,然後給請求設定這個重定向地址。

接下來要關注的就是之後的異常捕獲處理了,Basicwork的performRequest方法中有若干個異常,有連線超時,io異常等,這些異常都會被捕獲處理,大部分會在其中執行重試機制,說道這個重試機制,我一開始看的時候完全就懵逼了,
這裡寫圖片描述
都沒發現它怎麼重試的,上網查又自己看了好幾遍才明白,那他是怎麼實現重試的呢?

重試機制

讓我們再看一遍Basicwork的performRequest方法,阿西吧!!外面有個while死迴圈,這下就瞭然了,讓我來告訴你吧!首先請求由於連線超時或者重定向之類的導致這次請求失敗,於是呼叫attemptRetryOnException得到請求中的重試策略執行其中的retry方法,預設的實現是沒有重試的,失敗就失敗了,但是看他的預設實現我們就能寫出自己的重試策略,首先重試次數加1,超時時間非線性增長的,這個也可以自己來定,第一次2秒第二次就是4秒第三次就8秒,類似這樣實現是比較好的方式。當重試的此時大於最大的限制時就丟擲異常,也就是跳出迴圈結束這次請求。大致就是這樣的。

講到這裡之後就跳回到了NetworkDispatcher中繼續執行了,網路部分返回就兩種情況:一是成功返回響應,二是丟擲異常。對於異常的話就直接捕獲然後分發就行,而對於成功響應,首先成功返回的資料包括響應報文的首部和主體部分、是否有被更改、狀態碼、這次請求到成功返回所有時間(ms)。將響應成功的資料返回之後,就可以再回去看看一開始走網路的那幅圖了。

到此響應的部分就基本差不多了

分發

分發就是拿到資料之後通知使用者拿到了資料或者沒有拿到,也就是在拿到資料之後執行,那麼他也就在兩個地方,一個是在cache裡拿到資料之後分發,一個是從網路中拿到資料之後。但這個分發不用分開講,預設實現做的工作就是從工作執行緒分發資料到ui執行緒,你可以自己再去實現。總的來說這個沒啥好講的,一看就能懂,所以就這樣吧!

下面一幅圖可以幫助理解上面的響應和分發
這裡寫圖片描述

一些細節的講解

在下面講解一些比較細的,但是很可能會在閱讀原始碼的時候有問題的點。

為什麼在RequestQueue中快取請求佇列和網路請求佇列新增資料方法使用的是add而不是put

兩個佇列宣告如下:

private final PriorityBlockingQueue<Request<?>> mCacheQueue = new PriorityBlockingQueue<Request<?>>();

private final PriorityBlockingQueue<Request<?>> mNetworkQueue = new PriorityBlockingQueue<Request<?>>();

對於BlockingQueue它的put和add方法是有區別的,add方法在在佇列滿的時候是直接丟擲異常,而put方法是會阻塞,相比之下,RequestQueue基本都是會在ui執行緒中使用的,如果阻塞就會有anr錯誤,直接就是程式奔潰,那還是異常比較好一些。(再深入有異常怎麼辦,好像也沒看到異常捕獲,我自己也去查了一下,沒有查到就沒有繼續深究,畢竟這種情況基本不會出現,有興趣可以自己去查檢視)

為什麼使用工作執行緒任務佇列而不用執行緒池

不斷的建立新的執行緒銷燬執行緒是很好資源的,所以對於頻繁的網路請求就需要複用已有工作執行緒,這樣做同時也可以避免出現同一時間出現大量的執行緒的情況。就這兩點來看,其實是也可以使用執行緒池的,都能達到要求。但是我們可以先看看Volley的設計目標就是非常適合去進行資料量不大,但通訊頻繁的網路操作,而對於大資料量的網路操作,比如說下載檔案等,Volley的表現就會非常糟糕。從這個目標出發工作執行緒任務佇列適合處理大量耗時較短的任務,並且我個人覺得如果使用執行緒池的話編寫的難度上比使用執行緒任務佇列大,而且就最終的效能上我不覺得會有什麼差別,可能就一點:靈活性上來說執行緒池更加優越,總的來說就是兩個能用同樣的效能達到同樣的效果,當然是選擇更容易使用的。

處理走網路的請求中的304響應

304是http響應報文返回時都會攜帶的一個狀態碼,304的意思是和伺服器的資料比對之後發現沒有改變,我們常常會遇到的還有200:表示正確返回,404:沒有找到,302:重定向。著一部分其實是http協議的內容,有興趣的可以去讀一下http權威指南這本書,我也正在讀,如果自己想要搞一個網站什麼的,我覺得是必定要學好http協議的,對於編寫http相關程式也是幫助很大。

為什麼讀取請求佇列中請求的5個執行緒啟動之後都指定了優先順序

我們可以發現在CacheDispatcher和NetworkDispatcher中都有這麼一句話

Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

什麼意思呢,也就是指定了執行緒的優先順序,不要覺得執行緒幹嘛還要指定優先順序啊,不注意使用的話你的ui執行緒會出現卡頓的情況,詳情請看android執行緒的正確使用姿勢看完你就會明白!

NetworkDispatcher中的addTrafficStatsTag方法的作用

跟蹤進去你會發現其中使用的是TsafficStats的setThreadStatsTag方法,TsafficStats就是流量監控類,TrafficStats.setThreadStatsTag()方法用來標記執行緒內部發生的資料傳輸情況,而且這裡是不同的請求都會設定不同的Tag,我們就能比較清楚的通過ddms識別傳輸峰值所產生的原因,從而去優化我們的網路任務的寫法。我自己並沒有進行過嘗試,也是上網查的,參考:Android—優化下載讓網路訪問更高效(四),就這個地方要不要去深究我覺得仁者見仁智者見智,優化到一定程度這個點就會有必要去深究,但是一般我覺得這裡都可以跳過不看,對於整個Volley的理解基本沒什麼影響。

為什麼要將那些快取了但是已經過期或者不新鮮了的請求的etag 和 lastModified 兩個資訊,連同使用者自己定義的請求頭部一起新增到請求報文中

服務端根據請求時通過If-Modified-Since首部傳過來的時間,也就是lastModified ,判斷資原始檔是否在lastModified 時間 以後 有改動,如果有改動,返回新的請求結果。如果沒有改動,返回 304 not modified。這是為了更加準確的獲取資料,不要去重複獲取相同的資料。etag我不是很瞭解,但應該也是進行請求再驗證,和lastModified 的作用類似,只是lastModified 是判斷是否過期,而etag是新鮮度的認證,等我閱讀完http權威指南之後我會做確切的解答。在這裡可以先將這兩個資訊理解為幫助伺服器做出正確的響應,不要響應給我已經有的資料,如果我已經有這個資料就告訴我304(你已經有這個資料了)。

上面的細節問題都是我自己當時看的時候,會有障礙的地方,所以就列出來給大家提個醒,沒有問題的也看看幫我看看我的理解是否有誤,如果有朋友還有什麼不懂的問題,可以在文章下留言,我會將覺得有必要解釋的問題新增到文章中。
今天先到這裡,後續更新請看:Volley原始碼分析二