Volley是2013 Google I/O大會(huì)上發(fā)布的異步網(wǎng)絡(luò)請(qǐng)求框架和圖片加載框架,適合數(shù)據(jù)量小、通信頻繁的網(wǎng)絡(luò)操作。最近正好在做框架優(yōu)化,借機(jī)會(huì)再溫習(xí)一遍這個(gè)老框架。
Volley官方文檔:https://developer.android.com/training/volley
Volley項(xiàng)目:https://github.com/google/volley
一、Volley整體框架解析
1.1 架構(gòu)圖

從Volley整體架構(gòu)來看,主要分三層:
- 請(qǐng)求封裝:支持自定義各種數(shù)據(jù)類型的請(qǐng)求。
- 請(qǐng)求調(diào)度:分緩存和網(wǎng)絡(luò)兩類線程。
- 數(shù)據(jù)獲?。簝?nèi)存、磁盤、網(wǎng)絡(luò)。
1.2 類圖

核心類:
Volley:Volley框架入口,初始化RequestQueue。
RequestQueue:Volley網(wǎng)絡(luò)請(qǐng)求核心管理類。Network、Cache、ResponseDelivery、CacheDispatcher、NetworkDispatcher都聚合在RequestQueue中,它們都是Volley初始化RequestQueue時(shí)候一起初始化的,并作為參數(shù)傳入RequestQueue作為全局變量。
- CacheDispatcher 緩存線程*1
- NetworkDispatcher 網(wǎng)絡(luò)請(qǐng)求線程*4
- PriorityBlockingQueue:mCacheQueue 緩存線程任務(wù)隊(duì)列
- PriorityBlockingQueue:mNetworkQueue 網(wǎng)絡(luò)請(qǐng)求線程任務(wù)隊(duì)列
- ResponseDelivery:響應(yīng)分發(fā)調(diào)度
BasicNetwork:網(wǎng)絡(luò)請(qǐng)求處理
- HttpStack:具體網(wǎng)絡(luò)請(qǐng)求執(zhí)行,封裝了HttpURLConnection和HttpClient按sdk9版本來選擇使用。
- ByteArrayPool:網(wǎng)絡(luò)請(qǐng)求原始數(shù)據(jù)緩存池。
DiskBaseCache:磁盤緩存處理
Request:網(wǎng)絡(luò)請(qǐng)求封裝類
- 框架定義Request
- 自定義Request
NetworkResponse:網(wǎng)絡(luò)請(qǐng)求原始數(shù)據(jù)響應(yīng)封裝類。
Response:對(duì)網(wǎng)絡(luò)請(qǐng)求原始數(shù)據(jù)進(jìn)行解析后的響應(yīng)封裝類。
1.3 請(qǐng)求時(shí)序圖

一次網(wǎng)絡(luò)請(qǐng)求流程:
1)Volley通過newRequestQueue初始化RequestQueue: 初始化HttpStack、BasicNetwork、ExecutorDelivery,啟動(dòng)CacheDispatcher和NetworkDispatcher線程。
2)RequestQueue通過add添加Request觸發(fā)數(shù)據(jù)獲取流程:

3)CacheDispatcher和NetworkDispatcher均為while(true)循環(huán)執(zhí)行,通過對(duì)應(yīng)的queue來阻塞任務(wù),當(dāng)對(duì)應(yīng)的queue添加了request,會(huì)執(zhí)行如下流程:

二、Volley核心源碼分析
對(duì)整個(gè)框架結(jié)構(gòu)和執(zhí)行流程有了大致了解之后,來細(xì)化分析幾點(diǎn)核心功能:
- Volley的線程管理
- Volley的緩存邏輯
- Volley的網(wǎng)絡(luò)請(qǐng)求原始數(shù)據(jù)緩存池優(yōu)化:ByteArrayPool
- 請(qǐng)求cancel邏輯
2.1 Volley的線程管理
Volley默認(rèn)線程:CacheDispatcher *1,NetworkDispatcher *4,多余的請(qǐng)求入隊(duì)PriorityBlockingQueue。
源碼位置:com/android/volley/RequestQueue.java
/**
* Starts the dispatchers in this queue.
*/
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
com/android/volley/NetworkDispatcher.java
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
...
try {
// 從queue中獲取任務(wù)
request = mQueue.take();
...
// 執(zhí)行網(wǎng)絡(luò)請(qǐng)求
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// 解析網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// 寫緩存數(shù)據(jù)
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response.cacheEntry.isCache) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// 分發(fā)response
request.markDelivered();
mDelivery.postResponse(request, response);
...
}
}
源碼分析:
RequestQueue組合了CacheDispatcher1和NetworkDispatcher4,默認(rèn)5個(gè)常駐線程,除非主動(dòng)stop。
線程是while(true)循環(huán),通過PriorityBlockingQueue阻塞,同一線程循環(huán)獲取PriorityBlockingQueue任務(wù),正常情況下,下一個(gè)任務(wù)的調(diào)度需要等上一次請(qǐng)求執(zhí)行完畢分發(fā)響應(yīng)后才會(huì)調(diào)度到。
PriorityBlockingQueue是一個(gè)支持優(yōu)先級(jí)的無界阻塞隊(duì)列,默認(rèn)容量11,擴(kuò)容規(guī)則:舊容量小于64則翻倍,舊容量大于64則增加一半。雖然比LinkedBlockingQueue初始化默認(rèn)容量為Integer.MAX_VALUE要強(qiáng)點(diǎn), 但是理論上還是可以一直添加request,直到系統(tǒng)資源耗盡。
2.2 Volley的緩存邏輯
源碼分析:
Volley通過Request.setShouldCache設(shè)置請(qǐng)求是否緩存。
如果設(shè)置了緩存,整體邏輯是先從磁盤緩存獲取,如果沒有再進(jìn)行網(wǎng)絡(luò)請(qǐng)求,網(wǎng)絡(luò)請(qǐng)求成功后數(shù)據(jù)再做磁盤緩存。
這里主要看下磁盤緩存是怎么做的:
- 路徑:data/data/packageName/cache/volley
- 文件形式:
-rw------- 1 u0_a255 u0_a255_cache 6250 2020-11-17 14:57 -11505126341240133916
-rw------- 1 u0_a255 u0_a255_cache 1532 2020-11-17 14:57 -12302758661826148972
-rw------- 1 u0_a255 u0_a255_cache 1787 2020-11-17 14:57 -1391051118-579309601
-rw------- 1 u0_a255 u0_a255_cache 4877 2020-11-17 14:57 -13947998811265044989
- 文件內(nèi)容即原始response數(shù)據(jù)。
- 磁盤緩存大?。?M
- 文件刪除規(guī)則:LRU
2.3 Volley字節(jié)流內(nèi)存優(yōu)化ByteArrayPool
com/android/volley/toolbox/BasicNetwork.java
public NetworkResponse performRequest(Request<?> request)
throws VolleyError {
while (true) {
… //具體執(zhí)行網(wǎng)絡(luò)請(qǐng)求
httpResponse = mHttpStack.performRequest(request, headers);
...
if (httpResponse.getEntity() != null) {
//HttpEntity內(nèi)容轉(zhuǎn)到內(nèi)存bytes[]中
responseContents = entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}
… //返回原始response數(shù)據(jù)
return new NetworkResponse(statusCode, responseContents,
responseHeaders, false,
SystemClock.elapsedRealtime() - requestStart);
...
}
}
接下來看看entityToBytes方法:
/** Reads the contents of HttpEntity into a byte[]. */
private byte[] entityToBytes(HttpEntity entity)
throws IOException, ServerError {
PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(
mPool, (int) entity.getContentLength());
byte[] buffer = null;
try {
InputStream in = entity.getContent();
if (in == null) {
throw new ServerError();
}
buffer = mPool.getBuf(1024);
int count;
while ((count = in.read(buffer)) != -1) {
bytes.write(buffer, 0, count);
}
return bytes.toByteArray();
} finally {
try {
// Close the InputStream and release the resources by "consuming
// the content".
entity.consumeContent();
} catch (IOException e) {
// This can happen if there was an exception above that left the
// entity in
// an invalid state.
VolleyLog.v("Error occurred when calling consumingContent");
}
mPool.returnBuf(buffer);
bytes.close();
}
}
源碼分析:
BasicNetwork的performRequest執(zhí)行具體網(wǎng)絡(luò)請(qǐng)求,總共三步,http核心庫(kù)執(zhí)行網(wǎng)絡(luò)請(qǐng)求獲取HttpEntry、Entry轉(zhuǎn)byte[]、byte[]內(nèi)容封裝入NetworkResponse中返回。由于volley是輕量級(jí)頻次高的網(wǎng)絡(luò)請(qǐng)求框架,因此在這里會(huì)頻繁創(chuàng)建和銷毀byte[],為了提高性能,volley定義了一個(gè)byte[]緩沖池,即ByteArrayPool 。
PoolingByteArrayOutputStream其實(shí)就是一個(gè)輸出流,只不過系統(tǒng)的輸出流在使用byte[]時(shí),如果大小不夠,會(huì)自動(dòng)擴(kuò)大byte[]的大小。而PoolingByteArrayOutputStream則是使用了上面的字節(jié)數(shù)組緩沖池,從池中獲取byte[] 使用完畢后再歸還。 在BasicNetwork中對(duì)響應(yīng)進(jìn)行解析的時(shí)候使用到了該輸出流。
2.4 請(qǐng)求cancel邏輯
請(qǐng)求cancel邏輯主要分兩個(gè)部分:RequestQueue對(duì)Request進(jìn)行統(tǒng)一cancel,Request自己標(biāo)記cancel。
com/android/volley/Request.java
/**
* Mark this request as canceled. No callback will be delivered.
*/
public void cancel() {
mCanceled = true;
}
/**
* Returns true if this request has been canceled.
*/
public boolean isCanceled() {
return mCanceled;
}
com/android/volley/NetworkDispatcher.java
public void run() {
...
// Take a request from the queue.
request = mQueue.take();
...
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
...
}
Request被線程調(diào)度到了,會(huì)先判斷cancel標(biāo)記來決定是否執(zhí)行任務(wù)。
com/android/volley/RequestQueue.java
//保存正在被調(diào)度的request以及queue中等待的request的集合
private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
/**
* Cancels all requests in this queue for which the given filter applies.
*
* @param filter The filtering function to use
*/
public void cancelAll(RequestFilter filter) {
synchronized (mCurrentRequests) {
for (Request<?> request : mCurrentRequests) {
if (filter.apply(request)) {
request.cancel();
}
}
}
}
/**
* Cancels all requests in this queue with the given tag. Tag must be non-null
* and equality is by identity.
*/
public void cancelAll(final Object tag) {
if (tag == null) {
throw new IllegalArgumentException("Cannot cancelAll with a null tag");
}
cancelAll(new RequestFilter() {
@Override
public boolean apply(Request<?> request) {
return request.getTag() == tag;
}
});
}
RequestQueue支持按過濾條件和標(biāo)簽進(jìn)行request的批量刪除。
很明顯,Volley對(duì)Request的cancel只能作用于還沒走網(wǎng)絡(luò)請(qǐng)求的任務(wù),沒法對(duì)正在進(jìn)行網(wǎng)絡(luò)請(qǐng)求的Request進(jìn)行cancel。
Volley核心源碼分析完之后,來解析下為什么Volley適合做數(shù)據(jù)量小、通信頻繁的網(wǎng)絡(luò)操作,不適合高并發(fā)、不適合大文件上傳下載。
個(gè)人理解如下:
- Volley默認(rèn)是4個(gè)常駐loop線程來并行執(zhí)行任務(wù),整體并發(fā)性不高。因此數(shù)據(jù)量大而頻繁的任務(wù)會(huì)阻塞隊(duì)列中排隊(duì)的任務(wù)。
- Volley常駐loop線程帶來的好處是在通信頻繁的網(wǎng)絡(luò)操作場(chǎng)景下能減少線程創(chuàng)建銷毀的開銷。
- Volley在執(zhí)行網(wǎng)絡(luò)請(qǐng)求過程中會(huì)一個(gè)網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)的內(nèi)存byte[]轉(zhuǎn)換,對(duì)于頻繁的網(wǎng)絡(luò)請(qǐng)求,會(huì)頻繁創(chuàng)建和銷毀byte[],為了提高性能,volley定義了一個(gè)byte[]緩沖池,即ByteArrayPool 。它的問題在于大文件上傳下載會(huì)擠大緩沖池的byte[]內(nèi)存占用,從而造成內(nèi)存壓力。
三、Volley線程池方案分析

Volley線程池整體架構(gòu)有幾個(gè)關(guān)鍵點(diǎn):
PriorityBlockingQueue是一個(gè)支持優(yōu)先級(jí)的無界阻塞隊(duì)列,默認(rèn)容量11,擴(kuò)容規(guī)則:舊容量小于64則翻倍,舊容量大于64則增加一半。雖然比LinkedBlockingQueue初始化默認(rèn)容量為Integer.MAX_VALUE要強(qiáng)點(diǎn), 但是理論上還是可以一直添加request,直到系統(tǒng)資源耗盡,因此隊(duì)列阻塞任務(wù)過多有oom風(fēng)險(xiǎn)。
線程本身是一個(gè)while(true)的死循環(huán),通過queue.take()來做阻塞,queue中添加了任務(wù),線程會(huì)馬上take任務(wù)來處理,處理流程包括:1)performRequest:通過HttpUrlConnection/HttpClient執(zhí)行網(wǎng)絡(luò)請(qǐng)求。2)parseNetworkResponse:對(duì)網(wǎng)絡(luò)請(qǐng)求返回的response進(jìn)行解析封裝。3)postResponse:分發(fā)response出去。也就是說一個(gè)線程執(zhí)行任務(wù)的生命周期是從獲取任務(wù)到分發(fā)response出去,一個(gè)任務(wù)執(zhí)行完成才重新去queue中take新任務(wù),所以上一個(gè)任務(wù)中網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)解析耗時(shí)都有可能造成后面的任務(wù)delay。
默認(rèn)1個(gè)cache線程和4個(gè)network線程,常駐線程5個(gè)。常駐線程目的是在通信頻繁場(chǎng)景下能減少線程頻繁創(chuàng)建消耗的開銷。
四、Volley線程池優(yōu)化思考
首先Volley本身設(shè)計(jì)方案是否有優(yōu)化空間?
有些項(xiàng)目會(huì)按請(qǐng)求類型來構(gòu)造多個(gè)RequestQueue來處理任務(wù),增加并發(fā)性。
例如:
常見的網(wǎng)絡(luò)請(qǐng)求類型包括:普通數(shù)據(jù)請(qǐng)求、埋點(diǎn)及時(shí)上報(bào)、大文件上傳下載。那么就做三個(gè)RequestQueue,然后根據(jù)業(yè)務(wù)的并發(fā)性來配置合理的常駐線程數(shù),比如:4:4:1。
優(yōu)點(diǎn)是:增大了并發(fā)性,也降低了不同業(yè)務(wù)請(qǐng)求之間相互影響的概率。比如扎堆的數(shù)據(jù)上報(bào)和大文件上傳下載阻塞頁(yè)面UI刷新數(shù)據(jù)的網(wǎng)絡(luò)請(qǐng)求。
缺點(diǎn):Volley的線程池方案增加并發(fā)性需要以犧牲內(nèi)存為代價(jià),畢竟是固定的常駐線程。JDK1.5+ -Xss配置是1M,因此一個(gè)線程的開銷差不多是1M內(nèi)存。對(duì)于低內(nèi)存設(shè)備來說還是不太友好的。之前就遇到過如下問題:
java.lang.OutOfMemoryError: thread creation failed at java.lang.VMThread.create(Native Method)
at java.lang.Thread.start(Thread.java:1050)
at com.mgtv.tv.ad.library.network.android.volley.RequestQueue.start(RequestQueue.java:7)
at com.mgtv.tv.ad.library.network.android.volley.toolbox.Volley.newRequestQueue(Volley.java:10)
at com.mgtv.tv.ad.library.network.android.volley.toolbox.Volley.newRequestQueue(Volley.java:11)
所以這里考慮用線程池替換Volley固定數(shù)量常駐loop線程方案。主要有兩種線程池方案可供參考:
4.1 Okhttp線程池方案
private int maxRequests = 64;//最大并發(fā)數(shù)。
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();//等待任務(wù)
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();//執(zhí)行任務(wù)
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
okhttp使用的是類CacheThreadPool方案,無核心線程,純非核心線程。線程池配置上非核心線程數(shù)量為:Integer.MAX_VALUE,但實(shí)際是通過maxRequests來從代碼層面做了限制。在不滿足最大請(qǐng)求數(shù)時(shí),任務(wù)直接入running隊(duì)列,被執(zhí)行。超過最大請(qǐng)求數(shù),任務(wù)暫時(shí)先入ready隊(duì)列等待,當(dāng)有任務(wù)結(jié)束時(shí),會(huì)嘗試同步ready隊(duì)列任務(wù)到running隊(duì)列中,并執(zhí)行。
該方案并發(fā)可控,線程完全可回收,線程池本身使用的是SynchronousQueue,該隊(duì)列本身不存儲(chǔ)元素,因此任務(wù)排隊(duì)需求需要單獨(dú)實(shí)現(xiàn)。
那么這里有個(gè)問題,為什么要單獨(dú)實(shí)現(xiàn)隊(duì)列?

類CacheThreadPool方案核心在于要及時(shí)滿足任務(wù)的調(diào)度,它是并發(fā)性最好的線程池方案,但是同時(shí)它自身也存在明顯問題,因?yàn)榫€程創(chuàng)建是無限的,很容易造成oom, 所以在移動(dòng)端是需要限制并發(fā)數(shù)量的,怎么限制呢?
首先SynchronousQueue是不能替換的,它是高并發(fā)的保證,它自身不存儲(chǔ)元素,只是做一個(gè)阻塞,那如果改成ArrayBlockingQueue呢?按線程池執(zhí)行流程,在沒有核心線程情況下,會(huì)先入隊(duì),隊(duì)滿再創(chuàng)建非核心線程來執(zhí)行任務(wù),這樣肯定不行。所以比較好的辦法是從外部單獨(dú)實(shí)現(xiàn)隊(duì)列
看看Okhttp如何單獨(dú)實(shí)現(xiàn)隊(duì)列:
okhttp3/Dispatcher.java
synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求任務(wù),沒到最大請(qǐng)求數(shù)直接執(zhí)行,并添加到runningAsyncCalls中,否則進(jìn)readyAsyncCalls中排隊(duì)。
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}
if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}
一個(gè)任務(wù)結(jié)束會(huì)觸發(fā)finish,這里runningAsyncCalls先移除當(dāng)前任務(wù),然后通過promoteCalls將readyAsyncCalls的任務(wù)同步到runningAsyncCalls中來,如下所示:
private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();
if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}
if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
借用兩個(gè)Deque來從外部限制最大并發(fā)線程數(shù),非常簡(jiǎn)單,值得借鑒學(xué)習(xí)。
4.2 自定義線程池方案
mPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, //CORE_POOL_SIZE *2
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(QUEUE_CAPACITY),//限制queue容量
new ThreadPoolExecutor.CallerRunsPolicy()//queue滿之后的飽和策略
該方案是核心線程+非核心線程的線程池,整體方案還是屬于IO密集型線程池配置,常駐的核心線程避免線程頻繁創(chuàng)建和銷毀的內(nèi)存開銷,非核心線程提供一定的并發(fā)性拓展,總線程數(shù)有限,且非核心線程可回收,因此在核心線程數(shù)量不大的情況下內(nèi)存開銷也同樣可控,過多的任務(wù)只能入LinkedBlockingQueue隊(duì)列等待執(zhí)行,入隊(duì)任務(wù)過多會(huì)導(dǎo)致OOM,因此需要限制隊(duì)列容量以及配合拒絕策略,這里同樣也可以用ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY)。
注:ArrayBlockingQueue 與LinkedBlockingQueue區(qū)別:
- 有界性:ArrayBlockingQueue有界,LinkedBlockingQueue可有界可無界;
- 數(shù)據(jù)結(jié)構(gòu):ArrayBlockingQueue采用的是數(shù)組作為數(shù)據(jù)存儲(chǔ)容器,而LinkedBlockingQueue采用的則是以Node節(jié)點(diǎn)作為連接對(duì)象的鏈表。由于ArrayBlockingQueue采用的是數(shù)組的存儲(chǔ)容器,因此在插入或刪除元素時(shí)不會(huì)產(chǎn)生或銷毀任何額外的對(duì)象實(shí)例,而LinkedBlockingQueue則會(huì)生成一個(gè)額外的Node對(duì)象。這可能在長(zhǎng)時(shí)間內(nèi)需要高效并發(fā)地處理大批量數(shù)據(jù)的時(shí),對(duì)于GC可能存在較大影響。
- 并發(fā)性:ArrayBlockingQueue添加刪除是用的一把鎖,而LinkedBlockingQueue這兩個(gè)操作是分別加鎖的。在高并發(fā)情況下,LinkedBlockingQueue的生產(chǎn)者和消費(fèi)者可以并行地操作隊(duì)列中的數(shù)據(jù),并發(fā)性更好。
4.3 框架外部傳入項(xiàng)目已有線程池復(fù)用
這個(gè)方案也考慮過,好處是減少線程池?cái)?shù)量,本身也是一種線程開銷的優(yōu)化,但是這樣會(huì)讓網(wǎng)絡(luò)請(qǐng)求任務(wù)和項(xiàng)目中各中各樣的任務(wù)糅雜在一起,還是有互相影響到的問題。另外也不好為一類工作線程統(tǒng)一命名。
總體來說,如果Volley要做線程池改造的話,可以考慮引入Okhttp線程池方案:

換了這種方案之后,Volley的并發(fā)痛點(diǎn)能得到緩解,尤其是極端情況下:
例如:
項(xiàng)目中數(shù)據(jù)上報(bào)很頻繁且大部分都是及時(shí)上報(bào),某些頁(yè)面數(shù)據(jù)請(qǐng)求接口又比較多,因此進(jìn)入該類頁(yè)面會(huì)觸發(fā)比較多的網(wǎng)絡(luò)請(qǐng)求,因此頻繁切換此類頁(yè)面并發(fā)性要求相對(duì)來說會(huì)比較高,一旦遇到比如:任務(wù)數(shù)據(jù)量大、網(wǎng)絡(luò)超時(shí)、弱網(wǎng)環(huán)境,Volley的表現(xiàn)就很糟糕。
在網(wǎng)絡(luò)超時(shí)場(chǎng)景下:
request:
1970-01-01 08:22:29.582 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:185031429
1970-01-01 08:22:30.085 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:164326795
1970-01-01 08:22:30.203 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:58808167
1970-01-01 08:22:30.713 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:262858173
1970-01-01 08:22:30.986 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:123129723
1970-01-01 08:22:30.992 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:114437590
1970-01-01 08:22:30.999 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:219127620
1970-01-01 08:22:31.001 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:102453602
1970-01-01 08:22:31.033 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:86987358
response:
1970-01-01 08:22:34.739 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:185031429,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:31.716 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:164326795,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:45.334 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:58808167,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:32.459 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:262858173,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:39.696 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:123129723,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:46.832 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:114437590,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:51.034 4295-4295/com.xxx.xxx D/NetWorkVolleyImpl: requestID:219127620,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:47.484 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:102453602,error:java.net.SocketTimeoutException: connect timed out
1970-01-01 08:22:50.351 4295-4295/com.xxx.xxx I/NetWorkVolleyImpl: requestID:86987358,error:java.net.SocketTimeoutException: timeout
超時(shí)時(shí)間設(shè)置是5s,很明顯看到,在過了并發(fā)之后,后續(xù)的超時(shí)反饋越來越慢。超時(shí)時(shí)間是從任務(wù)獲取到線程執(zhí)行網(wǎng)絡(luò)請(qǐng)求開始計(jì)算的,因此如果是隊(duì)列阻塞狀態(tài),上一個(gè)任務(wù)耗時(shí)會(huì)delay到當(dāng)前任務(wù)的執(zhí)行,因此在超時(shí)回調(diào)上會(huì)是一個(gè)累加狀態(tài),讓后續(xù)的網(wǎng)絡(luò)請(qǐng)求遲遲沒有回調(diào),有些頁(yè)面無法刷新UI,一直處于黑屏或者loading狀態(tài)。該問題在okhttp的線程池中得到很大緩解。
那有人就問了,為什么不直接換okhttp呢?因?yàn)檫@篇文章寫的是Volley,它畢竟也是一個(gè)非常經(jīng)典的網(wǎng)絡(luò)框架,有很多設(shè)計(jì)思想是值得學(xué)習(xí)的,同時(shí)嘗試解決老框架的痛點(diǎn)問題,也是一種自我提升。
好了,就寫到這,文章有不對(duì)之處還望批評(píng)指正,如果有更好的想法也歡迎溝通交流。