Volley
源碼分析三部曲
Volley 源碼解析之網絡請求
Volley 源碼解析之圖片請求
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>() {@Overridepublic void onResponse(String response) {// Display the first 500 characters of the response string.mTextView.setText("Response is: "+ response.substring(0,500));}
}, new Response.ErrorListener() {@Overridepublic 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.EntryCache.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() {@Overridepublic void run() {try {mNetworkQueue.put(request);} catch (InterruptedException e) {// Restore the interrupted statusThread.currentThread().interrupt();}}});} else {//這里第三個參數傳遞null,不用再去分發,因為已經有相同的請求已經在執行,//直接添加到了等待請求的列表中,然后返回的時候從已經執行的請求收到響應mDelivery.postResponse(request, response);}}
}
復制代碼
這部分主要是對請求的緩存判斷,是否過期以及需要刷新緩存。我們調用取消所有請求或者取消某個請求實質上就是對mCanceled
這個變量賦值,然后在緩存線程或者網絡線程里面都回去判斷這個值,就完成了取消。上面的isExpired
和refreshNeeded
,兩個區別就是,前者如果過期就直接請求最新的內容,后者就是還在新鮮的時間內,但是把內容返回給用戶還是會發起請求,兩者一個與ttl值相比,另一個與softTtl相比。
其中有一個WaitingRequestManager,如果有相同的請求那么就需要一個暫存的地方,這個類就是做的這個操作
private static class WaitingRequestManager implements Request.NetworkRequestCompleteListener {//所有等待請求的集合,鍵是緩存的keyprivate final Map<String, List<Request<?>>> mWaitingRequests = new HashMap<>();private final CacheDispatcher mCacheDispatcher;WaitingRequestManager(CacheDispatcher cacheDispatcher) {mCacheDispatcher = cacheDispatcher;}//請求接受到一個有效的響應,后面等待的相同請求就可以使用這個響應@Overridepublic 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);}}}//沒有收到相應,則需要釋放請求@Overridepublic 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值作對比,一致則返回304Map<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() {@Overridepublic void execute(Runnable command) {handler.post(command);}};}public ExecutorDelivery(Executor executor) {mResponsePoster = executor;}//這個方法就是我們NetworkDispatcher里面調用的方法,調用下面這個三個參數的構造方法@Overridepublic void postResponse(Request<?> request, Response<?> response) {postResponse(request, response, null);}@Overridepublic void postResponse(Request<?> request, Response<?> response, Runnable runnable) {request.markDelivered();request.addMarker("post-response");//構造了一個ResponseDeliveryRunnable類,傳入execute,現在這個runnable就是在主線程里執行mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));}@Overridepublic 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")@Overridepublic void run() {//請求取消,那么就不分發給用戶if (mRequest.isCanceled()) {mRequest.finish("canceled-at-delivery");return;}// 根據isSuccess這個值來提供相應的回調給用戶,調用Response會通過error的值是否為null來確定這個值,//我們調用VolleyError這個構造函數的時候就為這個值就為falseif (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
里,首先是添加到緩存隊列,然后查看是否已經緩存,如果有并且在有效期內的緩存直接回調給用戶,如果沒有查找到,那么則需要添加到網絡請求隊列重新請求并且解析響應、寫入緩存在發送到主線程給用戶回調。
參考以及相關鏈接
- 【第1250期】徹底理解瀏覽器的緩存機制
- Android Volley完全解析(四),帶你從源碼的角度理解Volley
- Volley 源碼解析
- Volley 源碼解析