Volley 原始碼解析之網路請求
Volley 是 Google 推出的一款網路通訊框架,非常適合資料量小、通訊頻繁的網路請求,支援併發、快取和容易擴充套件、除錯等;不過不太適合下載大檔案、大量資料的網路請求,因為volley在解析期間將響應放到記憶體中,我們可以使用okhttp或者系統提供的
DownloadManager
來下載檔案。
一、簡單使用
首先在工程引入volley的library:
dependencies {
implementation 'com.android.volley:volley:1.1.1'
}
複製程式碼
然後需要我們開啟網路許可權,我這裡直接貼出官網簡單請求的示例程式碼:
final TextView mTextView = (TextView) findViewById(R.id.text);
// ...
// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// Display the first 500 characters of the response string.
mTextView.setText("Response is: "+ response.substring(0,500));
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse (VolleyError error) {
mTextView.setText("That didn't work!");
}
});
// Add the request to the RequestQueue.
queue.add(stringRequest);
複製程式碼
使用相對簡單,回撥直接在主執行緒,我們取消某個請求直接這樣操作:
-
定義一個標記新增到requests中
public static final String TAG = "MyTag"; StringRequest stringRequest; // Assume this exists. RequestQueue mRequestQueue; // Assume this exists. // Set the tag on the request. stringRequest.setTag(TAG); // Add the request to the RequestQueue. mRequestQueue.add(stringRequest); 複製程式碼
-
然後我們可以在 onStop() 中取消所有標記的請求
@Override protected void onStop () { super.onStop(); if (mRequestQueue != null) { mRequestQueue.cancelAll(TAG); } } 複製程式碼
二、原始碼分析
我們先從Volley這個類入手:
public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
BasicNetwork network;
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
network = new BasicNetwork(new HurlStack());
} else {
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info =
context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}
network =
new BasicNetwork(
new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
}
} else {
network = new BasicNetwork(stack);
}
return newRequestQueue(context, network);
}
private static RequestQueue newRequestQueue(Context context, Network network) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, (BaseHttpStack) null);
}
複製程式碼
當我們傳遞一個Context
的時候,首先為BaseHttpStack
為null,會執行到建立BaseHttpStack
,BaseHttpStack
是一個網路具體的處理請求,Volley
預設提供了基於HttpURLCollection
的HurlStack
和基於HttpClient
的HttpClientStack
。Android6.0移除了HttpClient
,Google官方推薦使用HttpURLCollection
類作為替換。所以這裡在API大於9的版本是用的是HurlStack
,為什麼這樣選擇,詳情可見這篇部落格Android訪問網路,使用HttpURLConnection還是HttpClient?。我們使用的是預設的構造,BaseHttpStack
傳入為null,如果我們想使用自定義的okhttp替換底層,我們直接繼承HttpStack
重寫即可,也可以自定義Network
和RequestQueue
,Volley
的高擴充套件性充分體現。接下來則建立一個Network
物件,然後例項化RequestQueue
,首先建立了一個用於快取的資料夾,然後建立了一個磁碟快取,將檔案快取到指定目錄的硬碟上,預設大小是5M,但是大小可以配置。接下來呼叫RequestQueue
的start()
方法進行啟動,我們進入這個方法檢視一下:
public void start() {
stop();
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher =
new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
複製程式碼
開始啟動的時候先停止所有的請求執行緒和網路快取執行緒,然後例項化一個快取執行緒並執行,然後一個迴圈開啟DEFAULT_NETWORK_THREAD_POOL_SIZE
(4)個網路請求執行緒並執行,一共就是5個執行緒在後臺執行,不斷的等待網路請求的到來。
構造了RequestQueue
之後,我們呼叫add()
方法將相應的Request
傳入就開始執行網路請求了,我們看看這個方法:
public <T> Request<T> add(Request<T> request) {
//將請求佇列和請求關聯起來
request.setRequestQueue(this);
//新增到正在請求中但是還未完成的集合中
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
//設定請求的一個序列號,通過原子變數的incrementAndGet方法,
//以原子方式給當前值加1並獲取新值實現請求的優先順序
request.setSequence(getSequenceNumber());
//新增一個除錯資訊
request.addMarker("add-to-queue");
//如果不需要快取則直接加到網路的請求佇列,預設每一個請求都是快取的,
//如果不需要快取需要呼叫Request的setShouldCache方法來修改
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
//加到快取的請求佇列
mCacheQueue.add(request);
return request;
}
複製程式碼
關鍵地方都寫了註釋,主要作用就是將請求加到請求佇列,執行網路請求或者從快取中獲取結果。網路和快取的請求都是一個優先順序阻塞佇列,按照優先順序出隊。上面幾個關鍵步驟,新增到請求集合裡面還有設定優先順序以及新增到快取和請求佇列都是執行緒安全的,要麼加鎖,要麼使用執行緒安全的佇列或者原子操作。
接下來我們看看新增到CacheDispatcher
快取請求佇列的run
方法:
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//初始化DiskBasedCache的快取類
mCache.initialize();
while (true) {
try {
processRequest();
} catch (InterruptedException e) {
if (mQuit) {
Thread.currentThread().interrupt();
return;
}
VolleyLog.e(
"Ignoring spurious interrupt of CacheDispatcher thread; "
+ "use quit() to terminate it");
}
}
}
複製程式碼
接下來的重點是看看processRequest()
這個方法:
private void processRequest() throws InterruptedException {
//從快取佇列取出請求
final Request<?> request = mCacheQueue.take();
processRequest(request);
}
@VisibleForTesting
void processRequest(final Request<?> request) throws InterruptedException {
request.addMarker("cache-queue-take");
// 如果請求被取消,我們可以通過RequestQueue的回撥介面來監聽
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
return;
}
// 從快取中獲取Cache.Entry
Cache.Entry entry = mCache.get(request.getCacheKey());
//沒有取到快取
if (entry == null) {
request.addMarker("cache-miss");
// 快取未命中,對於可快取的請求先去檢查是否有相同的請求是否已經在執行中,
//如果有的話先加入請求等待佇列,等待請求完成,返回true;如果返回false則表示第一次請求
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
//加入到網路請求的阻塞佇列
mNetworkQueue.put(request);
}
return;
}
// 如果快取完全過期,處理過程跟上面類似
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
//設定請求快取的entry到這個request中
request.setCacheEntry(entry);
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
mNetworkQueue.put(request);
}
return;
}
//快取命中,將資料解析並返回到request的抽象方法中
request.addMarker("cache-hit");
Response<?> response =
request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
//判斷請求結果是否需要重新整理
if (!entry.refreshNeeded()) {
// 未過期的快取命中,通過ExecutorDelivery回撥給我們的request子類的介面中,
// 我們在使用的時候就可以通過StringRequest、JsonRequest等拿到結果,
// 切換到主執行緒也是在這個類裡執行的
mDelivery.postResponse(request, response);
} else {
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// 將這個響應標記為中間值,即這個響應是軟過期的,那麼第二個響應正在請求隨時到來
response.intermediate = true;
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
//發起網路請求,這裡為什麼直接呼叫上面的mNetworkQueue.put(request);呢,
//主要是為了新增一個已經分發的標記,在響應分發的時候不再回調給使用者,
//不然就回調了兩次
mDelivery.postResponse(
request,
response,
new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
});
} else {
//這裡第三個引數傳遞null,不用再去分發,因為已經有相同的請求已經在執行,
//直接新增到了等待請求的列表中,然後返回的時候從已經執行的請求收到響應
mDelivery.postResponse(request, response);
}
}
}
複製程式碼
這部分主要是對請求的快取判斷,是否過期以及需要重新整理快取。我們呼叫取消所有請求或者取消某個請求實質上就是對mCanceled
這個變數賦值,然後在快取執行緒或者網路執行緒裡面都回去判斷這個值,就完成了取消。上面的isExpired
和refreshNeeded
,兩個區別就是,前者如果過期就直接請求最新的內容,後者就是還在軟過期的時間內,但是把內容返回給使用者還是會發起請求,兩者一個與ttl值相比,另一個與softTtl相比。
其中有一個WaitingRequestManager,如果有相同的請求那麼就需要一個暫存的地方,這個類就是做的這個操作
private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {
//所有等待請求的集合,鍵是快取的key
private final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();
private final CacheDispatcher mCacheDispatcher;
WaitingRequestManager(CacheDispatcher cacheDispatcher) {
mCacheDispatcher = cacheDispatcher;
}
//請求接受到一個有效的響應,後面等待的相同請求就可以使用這個響應
@Override
public void onResponseReceived(Request<?> request, Response<?> response) {
//如果快取為空或者已經過期,那麼就釋放等待的請求
if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
onNoUsableResponseReceived(request);
return;
}
String cacheKey = request.getCacheKey();
//等待的請求的集合
List<Request<?>> waitingRequests;
synchronized (this) {
//從map裡面移除這個請求的集合
waitingRequests = mWaitingRequests.remove(cacheKey);
}
if (waitingRequests != null) {
if (VolleyLog.DEBUG) {
VolleyLog.v(
"Releasing %d waiting requests for cacheKey=%s.",
waitingRequests.size(), cacheKey);
}
// 裡面所有的請求都分發到相應的回撥執行,下面會講解
for (Request<?> waiting : waitingRequests) {
mCacheDispatcher.mDelivery.postResponse(waiting, response);
}
}
}
//沒有收到相應,則需要釋放請求
@Override
public synchronized void onNoUsableResponseReceived(Request<?> request) {
String cacheKey = request.getCacheKey();
List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null && !waitingRequests.isEmpty()) {
if (VolleyLog.DEBUG) {
VolleyLog.v(
"%d waiting requests for cacheKey=%s; resend to network",
waitingRequests.size(), cacheKey);
}
//下面這個請求執會重新執行,將這個移除新增到
Request<?> nextInLine = waitingRequests.remove(0);
//將剩下的請求放到等待請求的map中
mWaitingRequests.put(cacheKey, waitingRequests);
//在request裡面註冊一個回撥介面,因為重新開始請求,需要重新註冊一個監聽,
//後面請求成功失敗以及取消都可以收到回撥
nextInLine.setNetworkRequestCompleteListener(this);
try {
//從上面if判斷方法可以得出:waitingRequests != null && !waitingRequests.isEmpty()
//排除了第一次請求失敗、取消的情況,後面的那個條件則表示這個等待請求佇列必須要有一個請求,
//同時滿足才會執行這裡面的程式碼,一般只要這裡面的請求執行成功一次後續所有的請求都會被移除,
//所以這裡對多個請求的情況,失敗一次,那麼後續的請求會繼續執行
mCacheDispatcher.mNetworkQueue.put(nextInLine);
} catch (InterruptedException iex) {
VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
// Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
Thread.currentThread().interrupt();
// Quit the current CacheDispatcher thread.
mCacheDispatcher.quit();
}
}
}
//對於可以快取的請求,相同快取的請求已經在執行中就新增到一個傳送佇列,
//等待執行中的佇列請求完成,返回true表示已經有請求在執行,false則是第一次執行
private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
String cacheKey = request.getCacheKey();
// 存在相同的請求則把請求加入到相同快取鍵的集合中
if (mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
//如果包含相同的請求但是有可能是第二次請求,前面第一次請求插入null了
if (stagedRequests == null) {
stagedRequests = new ArrayList<>();
}
request.addMarker("waiting-for-response");
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
return true;
} else {
//第一次請求那麼則插入一個null,表示當前有一個請求正在執行
mWaitingRequests.put(cacheKey, null);
//註冊一個介面監聽
request.setNetworkRequestCompleteListener(this);
if (VolleyLog.DEBUG) {
VolleyLog.d("new request, sending to network %s", cacheKey);
}
return false;
}
}
}
複製程式碼
這個類主要是避免相同的請求多次請求,而且在NetworkDispatcher
裡面也會通過這個介面回撥相應的值在這裡執行,最終比如在網路請求返回304、請求取消或者異常那麼都會在這裡來處理,如果收到響應則會把值回撥給使用者,後面的請求也不會再去請求,如果無效的響應則會做一些釋放等待的請求操作,請求完成也會將後面相同的請求回撥給使用者,三個方法都在不同的地方發揮作用。
我們接下來看看NetworkDispatcher
網路請求佇列的run
方法中的processRequest
方法:
@VisibleForTesting
void processRequest(Request<?> request) {
long startTimeMs = SystemClock.elapsedRealtime();
try {
request.addMarker("network-queue-take");
// 請求被取消了,就不執行網路請求,
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
request.notifyListenerResponseNotUsable();
return;
}
addTrafficStatsTag(request);
// 這裡就是執行網路請求的地方
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// 如果伺服器返回304響應,即沒有修改過,
//快取依然是有效的並且是在需要重新整理的有效期內,那麼則不需要解析響應
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
//沒有收到來自網路的有效響應,釋放請求
request.notifyListenerResponseNotUsable();
return;
}
// 在工作執行緒中解析這些響應
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// 將快取寫入到應用
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// 標記此請求已將分發
request.markDelivered();
//將請求的響應回撥給使用者
mDelivery.postResponse(request, response);
//請求接受到了一個響應,其他相同的請求可以使用這個響應
request.notifyListenerResponseReceived(response);
} catch (VolleyError volleyError) {
...
}
}
複製程式碼
這裡才是網路請求的真正執行以及解析分發的地方,重點看兩個地方的程式碼,執行和解析,我們先看看執行網路請求這個程式碼,執行的地方是BasicNetwork.performRequest
,下面看看這個方法:
@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
List<Header> responseHeaders = Collections.emptyList();
try {
// 構造快取的頭部,新增If-None-Match和If-Modified-Since,都是http/1.1中控制協商快取的兩個欄位,
// If-None-Match:客服端再次發起請求時,攜帶上次請求返回的唯一標識Etag值,
//服務端用攜帶的值和最後修改的值作對比,最後修改時間大於攜帶的欄位值則返回200,否則304;
// If-Modified-Since:客服端再次發起請求時,攜帶上次請求返回的Last-Modified值,
//服務端用攜帶的值和伺服器的Etag值作對比,一致則返回304
Map<String, String> additionalRequestHeaders =
getCacheHeaders(request.getCacheEntry());
//因為現在一般的sdk都是大於9的,那麼這裡執行的就是HurlStack的executeRequest方法,
//執行網路請求,和我們平時使用HttpURLConnection請求網路大致相同
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
int statusCode = httpResponse.getStatusCode();
responseHeaders = httpResponse.getHeaders();
// 服務端返回304時,那麼就表示資源無更新,可以繼續使用快取的值
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
Entry entry = request.getCacheEntry();
if (entry == null) {
return new NetworkResponse(
HttpURLConnection.HTTP_NOT_MODIFIED,
/* data= */ null,
/* notModified= */ true,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
}
// 將快取頭和響應頭組合在一起,一次響應就完成了
List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
return new NetworkResponse(
HttpURLConnection.HTTP_NOT_MODIFIED,
entry.data,
/* notModified= */ true,
SystemClock.elapsedRealtime() - requestStart,
combinedHeaders);
}
// 如果返回204,執行成功,沒有資料,這裡需要檢查
InputStream inputStream = httpResponse.getContent();
if (inputStream != null) {
responseContents =
inputStreamToBytes(inputStream, httpResponse.getContentLength());
} else {
//返回204,就返回一個空的byte陣列
responseContents = new byte[0];
}
// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusCode);
if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
return new NetworkResponse(
statusCode,
responseContents,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
} catch (SocketTimeoutException e) {
//異常進行重新請求等...
}
}
}
複製程式碼
這裡主要執行了新增快取頭併發起網路請求,然後將返回值組裝成一個NetworkResponse
值返回,接下來我們看看是如何解析這個值的,解析是由Request
的子類去實現的,我們就看系統提供的StringRequest
:
@Override
@SuppressWarnings("DefaultCharset")
protected Response<String> parseNetworkResponse(NetworkResponse response) {
String parsed;
try {
parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
// Since minSdkVersion = 8, we can't call
// new String(response.data, Charset.defaultCharset())
// So suppress the warning instead.
parsed = new String(response.data);
}
return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}
複製程式碼
我們可以看到將值組裝成一個String,然後組裝成一個Response
返回,接下來看看這裡如何將這個值回撥給使用者的這個方法mDelivery.postResponse(request, response)
,這裡我們先重點看看這個類ExecutorDelivery
:
public class ExecutorDelivery implements ResponseDelivery {
//構造執行已提交的Runnable任務物件
private final Executor mResponsePoster;
//這裡在RequestQueue構造引數中初始化,new ExecutorDelivery(new Handler(Looper.getMainLooper())),
//那麼這裡runnable就通過繫結主執行緒的Looper的Handler物件投遞到主執行緒中執行
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster =
new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
public ExecutorDelivery(Executor executor) {
mResponsePoster = executor;
}
//這個方法就是我們NetworkDispatcher裡面呼叫的方法,呼叫下面這個三個引數的構造方法
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
//構造了一個ResponseDeliveryRunnable類,傳入execute,現在這個runnable就是在主執行緒裡執行
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
@Override
public void postError(Request<?> request, VolleyError error) {
request.addMarker("post-error");
Response<?> response = Response.error(error);
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
}
/** A Runnable used for delivering network responses to a listener on the main thread. */
@SuppressWarnings("rawtypes")
private static class ResponseDeliveryRunnable implements Runnable {
private final Request mRequest;
private final Response mResponse;
private final Runnable mRunnable;
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
//請求取消,那麼就不分發給使用者
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// 根據isSuccess這個值來提供相應的回撥給使用者,呼叫Response會通過error的值是否為null來確定這個值,
//我們呼叫VolleyError這個建構函式的時候就為這個值就為false
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
// 如果這是一個在軟過期時間的請求的響應,就新增一個標記,否則就結束
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
// 在CacheDispatcher裡面軟過期那個地方直接呼叫三個引數的構造方法,通過這個runnable就執行run方法
if (mRunnable != null) {
mRunnable.run();
}
}
}
}
複製程式碼
上面方法主要是將值回撥給使用者,那麼整個網路請求大致就完成了,其中還涉及很多細節的東西,但是大致流程是走通了,不得不說這個庫有很多值得我們學習的地方。
三、總結
現在我們看官網的一張圖,總結一下整個流程:
- 藍色是主執行緒
- 綠色是快取執行緒
- 黃色是網路執行緒
我們可以看到首先是請求新增到RequestQueue
裡,首先是新增到快取佇列,然後檢視是否已經快取,如果有並且在有效期內的快取直接回調給使用者,如果沒有查詢到,那麼則需要新增到網路請求佇列重新請求並且解析響應、寫入快取在傳送到主執行緒給使用者回撥。