Volley是2013 Google I/O大會上發(fā)布的異步網(wǎng)絡(luò)請求框架和圖片加載框架,適合數(shù)據(jù)量小菇篡、通信頻繁的網(wǎng)絡(luò)操作漩符。最近正好在做框架優(yōu)化,借機會再溫習(xí)一遍這個老框架驱还。
Volley官方文檔:https://developer.android.com/training/volley
Volley項目:https://github.com/google/volley
一嗜暴、Volley整體框架解析
1.1 架構(gòu)圖
從Volley整體架構(gòu)來看,主要分三層:
- 請求封裝:支持自定義各種數(shù)據(jù)類型的請求议蟆。
- 請求調(diào)度:分緩存和網(wǎng)絡(luò)兩類線程闷沥。
- 數(shù)據(jù)獲取:內(nèi)存咐容、磁盤舆逃、網(wǎng)絡(luò)。
1.2 類圖
核心類:
Volley:Volley框架入口疟丙,初始化RequestQueue颖侄。
RequestQueue:Volley網(wǎng)絡(luò)請求核心管理類。Network享郊、Cache览祖、ResponseDelivery、CacheDispatcher炊琉、NetworkDispatcher都聚合在RequestQueue中展蒂,它們都是Volley初始化RequestQueue時候一起初始化的,并作為參數(shù)傳入RequestQueue作為全局變量苔咪。
- CacheDispatcher 緩存線程*1
- NetworkDispatcher 網(wǎng)絡(luò)請求線程*4
- PriorityBlockingQueue:mCacheQueue 緩存線程任務(wù)隊列
- PriorityBlockingQueue:mNetworkQueue 網(wǎng)絡(luò)請求線程任務(wù)隊列
- ResponseDelivery:響應(yīng)分發(fā)調(diào)度
BasicNetwork:網(wǎng)絡(luò)請求處理
- HttpStack:具體網(wǎng)絡(luò)請求執(zhí)行锰悼,封裝了HttpURLConnection和HttpClient按sdk9版本來選擇使用。
- ByteArrayPool:網(wǎng)絡(luò)請求原始數(shù)據(jù)緩存池团赏。
DiskBaseCache:磁盤緩存處理
Request:網(wǎng)絡(luò)請求封裝類
- 框架定義Request
- 自定義Request
NetworkResponse:網(wǎng)絡(luò)請求原始數(shù)據(jù)響應(yīng)封裝類箕般。
Response:對網(wǎng)絡(luò)請求原始數(shù)據(jù)進行解析后的響應(yīng)封裝類。
1.3 請求時序圖
一次網(wǎng)絡(luò)請求流程:
1)Volley通過newRequestQueue初始化RequestQueue: 初始化HttpStack舔清、BasicNetwork丝里、ExecutorDelivery曲初,啟動CacheDispatcher和NetworkDispatcher線程。
2)RequestQueue通過add添加Request觸發(fā)數(shù)據(jù)獲取流程:
3)CacheDispatcher和NetworkDispatcher均為while(true)循環(huán)執(zhí)行杯聚,通過對應(yīng)的queue來阻塞任務(wù)臼婆,當(dāng)對應(yīng)的queue添加了request,會執(zhí)行如下流程:
二幌绍、Volley核心源碼分析
對整個框架結(jié)構(gòu)和執(zhí)行流程有了大致了解之后颁褂,來細(xì)化分析幾點核心功能:
- Volley的線程管理
- Volley的緩存邏輯
- Volley的網(wǎng)絡(luò)請求原始數(shù)據(jù)緩存池優(yōu)化:ByteArrayPool
- 請求cancel邏輯
2.1 Volley的線程管理
Volley默認(rèn)線程:CacheDispatcher *1,NetworkDispatcher *4傀广,多余的請求入隊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ò)請求
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ò)請求數(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個常駐線程主儡,除非主動stop奖唯。
線程是while(true)循環(huán),通過PriorityBlockingQueue阻塞糜值,同一線程循環(huán)獲取PriorityBlockingQueue任務(wù)丰捷,正常情況下,下一個任務(wù)的調(diào)度需要等上一次請求執(zhí)行完畢分發(fā)響應(yīng)后才會調(diào)度到寂汇。
PriorityBlockingQueue是一個支持優(yōu)先級的無界阻塞隊列病往,默認(rèn)容量11,擴容規(guī)則:舊容量小于64則翻倍骄瓣,舊容量大于64則增加一半停巷。雖然比LinkedBlockingQueue初始化默認(rèn)容量為Integer.MAX_VALUE要強點, 但是理論上還是可以一直添加request榕栏,直到系統(tǒng)資源耗盡畔勤。
2.2 Volley的緩存邏輯
源碼分析:
Volley通過Request.setShouldCache設(shè)置請求是否緩存。
如果設(shè)置了緩存扒磁,整體邏輯是先從磁盤緩存獲取庆揪,如果沒有再進行網(wǎng)絡(luò)請求,網(wǎng)絡(luò)請求成功后數(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ù)缸榛。
- 磁盤緩存大小:5M
- 文件刪除規(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ò)請求
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ò)請求兰伤,總共三步内颗,http核心庫執(zhí)行網(wǎng)絡(luò)請求獲取HttpEntry、Entry轉(zhuǎn)byte[]敦腔、byte[]內(nèi)容封裝入NetworkResponse中返回均澳。由于volley是輕量級頻次高的網(wǎng)絡(luò)請求框架,因此在這里會頻繁創(chuàng)建和銷毀byte[],為了提高性能负懦,volley定義了一個byte[]緩沖池筒捺,即ByteArrayPool 。
PoolingByteArrayOutputStream其實就是一個輸出流纸厉,只不過系統(tǒng)的輸出流在使用byte[]時,如果大小不夠五嫂,會自動擴大byte[]的大小颗品。而PoolingByteArrayOutputStream則是使用了上面的字節(jié)數(shù)組緩沖池,從池中獲取byte[] 使用完畢后再歸還沃缘。 在BasicNetwork中對響應(yīng)進行解析的時候使用到了該輸出流躯枢。
2.4 請求cancel邏輯
請求cancel邏輯主要分兩個部分:RequestQueue對Request進行統(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)度到了锄蹂,會先判斷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)簽進行request的批量刪除水慨。
很明顯得糜,Volley對Request的cancel只能作用于還沒走網(wǎng)絡(luò)請求的任務(wù),沒法對正在進行網(wǎng)絡(luò)請求的Request進行cancel晰洒。
Volley核心源碼分析完之后朝抖,來解析下為什么Volley適合做數(shù)據(jù)量小、通信頻繁的網(wǎng)絡(luò)操作谍珊,不適合高并發(fā)治宣、不適合大文件上傳下載。
個人理解如下:
- Volley默認(rèn)是4個常駐loop線程來并行執(zhí)行任務(wù)砌滞,整體并發(fā)性不高侮邀。因此數(shù)據(jù)量大而頻繁的任務(wù)會阻塞隊列中排隊的任務(wù)。
- Volley常駐loop線程帶來的好處是在通信頻繁的網(wǎng)絡(luò)操作場景下能減少線程創(chuàng)建銷毀的開銷贝润。
- Volley在執(zhí)行網(wǎng)絡(luò)請求過程中會一個網(wǎng)絡(luò)請求數(shù)據(jù)的內(nèi)存byte[]轉(zhuǎn)換绊茧,對于頻繁的網(wǎng)絡(luò)請求,會頻繁創(chuàng)建和銷毀byte[]题暖,為了提高性能按傅,volley定義了一個byte[]緩沖池,即ByteArrayPool 胧卤。它的問題在于大文件上傳下載會擠大緩沖池的byte[]內(nèi)存占用唯绍,從而造成內(nèi)存壓力。
三枝誊、Volley線程池方案分析
Volley線程池整體架構(gòu)有幾個關(guān)鍵點:
PriorityBlockingQueue是一個支持優(yōu)先級的無界阻塞隊列况芒,默認(rèn)容量11,擴容規(guī)則:舊容量小于64則翻倍,舊容量大于64則增加一半绝骚。雖然比LinkedBlockingQueue初始化默認(rèn)容量為Integer.MAX_VALUE要強點耐版, 但是理論上還是可以一直添加request,直到系統(tǒng)資源耗盡压汪,因此隊列阻塞任務(wù)過多有oom風(fēng)險粪牲。
線程本身是一個while(true)的死循環(huán),通過queue.take()來做阻塞止剖,queue中添加了任務(wù)腺阳,線程會馬上take任務(wù)來處理,處理流程包括:1)performRequest:通過HttpUrlConnection/HttpClient執(zhí)行網(wǎng)絡(luò)請求穿香。2)parseNetworkResponse:對網(wǎng)絡(luò)請求返回的response進行解析封裝亭引。3)postResponse:分發(fā)response出去。也就是說一個線程執(zhí)行任務(wù)的生命周期是從獲取任務(wù)到分發(fā)response出去皮获,一個任務(wù)執(zhí)行完成才重新去queue中take新任務(wù)焙蚓,所以上一個任務(wù)中網(wǎng)絡(luò)請求、數(shù)據(jù)解析耗時都有可能造成后面的任務(wù)delay洒宝。
默認(rèn)1個cache線程和4個network線程购公,常駐線程5個。常駐線程目的是在通信頻繁場景下能減少線程頻繁創(chuàng)建消耗的開銷待德。
四君丁、Volley線程池優(yōu)化思考
首先Volley本身設(shè)計方案是否有優(yōu)化空間?
有些項目會按請求類型來構(gòu)造多個RequestQueue來處理任務(wù)将宪,增加并發(fā)性绘闷。
例如:
常見的網(wǎng)絡(luò)請求類型包括:普通數(shù)據(jù)請求、埋點及時上報较坛、大文件上傳下載印蔗。那么就做三個RequestQueue,然后根據(jù)業(yè)務(wù)的并發(fā)性來配置合理的常駐線程數(shù)丑勤,比如:4:4:1华嘹。
優(yōu)點是:增大了并發(fā)性,也降低了不同業(yè)務(wù)請求之間相互影響的概率法竞。比如扎堆的數(shù)據(jù)上報和大文件上傳下載阻塞頁面UI刷新數(shù)據(jù)的網(wǎng)絡(luò)請求耙厚。
缺點:Volley的線程池方案增加并發(fā)性需要以犧牲內(nèi)存為代價,畢竟是固定的常駐線程岔霸。JDK1.5+ -Xss配置是1M薛躬,因此一個線程的開銷差不多是1M內(nèi)存。對于低內(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,但實際是通過maxRequests來從代碼層面做了限制岖寞。在不滿足最大請求數(shù)時抡四,任務(wù)直接入running隊列,被執(zhí)行慎璧。超過最大請求數(shù)床嫌,任務(wù)暫時先入ready隊列等待,當(dāng)有任務(wù)結(jié)束時胸私,會嘗試同步ready隊列任務(wù)到running隊列中,并執(zhí)行鳖谈。
該方案并發(fā)可控岁疼,線程完全可回收,線程池本身使用的是SynchronousQueue缆娃,該隊列本身不存儲元素捷绒,因此任務(wù)排隊需求需要單獨實現(xiàn)。
那么這里有個問題贯要,為什么要單獨實現(xiàn)隊列暖侨?
類CacheThreadPool方案核心在于要及時滿足任務(wù)的調(diào)度,它是并發(fā)性最好的線程池方案崇渗,但是同時它自身也存在明顯問題字逗,因為線程創(chuàng)建是無限的,很容易造成oom, 所以在移動端是需要限制并發(fā)數(shù)量的宅广,怎么限制呢葫掉?
首先SynchronousQueue是不能替換的,它是高并發(fā)的保證跟狱,它自身不存儲元素俭厚,只是做一個阻塞,那如果改成ArrayBlockingQueue呢驶臊?按線程池執(zhí)行流程挪挤,在沒有核心線程情況下,會先入隊关翎,隊滿再創(chuàng)建非核心線程來執(zhí)行任務(wù)扛门,這樣肯定不行。所以比較好的辦法是從外部單獨實現(xiàn)隊列
看看Okhttp如何單獨實現(xiàn)隊列:
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ā)起一個網(wǎng)絡(luò)請求任務(wù)笤休,沒到最大請求數(shù)直接執(zhí)行尖飞,并添加到runningAsyncCalls中,否則進readyAsyncCalls中排隊。
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();
}
}
一個任務(wù)結(jié)束會觸發(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.
}
}
借用兩個Deque來從外部限制最大并發(fā)線程數(shù)嘶朱,非常簡單珍促,值得借鑒學(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隊列等待執(zhí)行,入隊任務(wù)過多會導(dǎo)致OOM址否,因此需要限制隊列容量以及配合拒絕策略餐蔬,這里同樣也可以用ArrayBlockingQueue<Runnable>(QUEUE_CAPACITY)。
注:ArrayBlockingQueue 與LinkedBlockingQueue區(qū)別:
- 有界性:ArrayBlockingQueue有界佑附,LinkedBlockingQueue可有界可無界樊诺;
- 數(shù)據(jù)結(jié)構(gòu):ArrayBlockingQueue采用的是數(shù)組作為數(shù)據(jù)存儲容器,而LinkedBlockingQueue采用的則是以Node節(jié)點作為連接對象的鏈表音同。由于ArrayBlockingQueue采用的是數(shù)組的存儲容器词爬,因此在插入或刪除元素時不會產(chǎn)生或銷毀任何額外的對象實例,而LinkedBlockingQueue則會生成一個額外的Node對象权均。這可能在長時間內(nèi)需要高效并發(fā)地處理大批量數(shù)據(jù)的時顿膨,對于GC可能存在較大影響。
- 并發(fā)性:ArrayBlockingQueue添加刪除是用的一把鎖螺句,而LinkedBlockingQueue這兩個操作是分別加鎖的虽惭。在高并發(fā)情況下,LinkedBlockingQueue的生產(chǎn)者和消費者可以并行地操作隊列中的數(shù)據(jù)蛇尚,并發(fā)性更好芽唇。
4.3 框架外部傳入項目已有線程池復(fù)用
這個方案也考慮過,好處是減少線程池數(shù)量取劫,本身也是一種線程開銷的優(yōu)化匆笤,但是這樣會讓網(wǎng)絡(luò)請求任務(wù)和項目中各中各樣的任務(wù)糅雜在一起,還是有互相影響到的問題谱邪。另外也不好為一類工作線程統(tǒng)一命名炮捧。
總體來說,如果Volley要做線程池改造的話惦银,可以考慮引入Okhttp線程池方案:
換了這種方案之后咆课,Volley的并發(fā)痛點能得到緩解末誓,尤其是極端情況下:
例如:
項目中數(shù)據(jù)上報很頻繁且大部分都是及時上報,某些頁面數(shù)據(jù)請求接口又比較多书蚪,因此進入該類頁面會觸發(fā)比較多的網(wǎng)絡(luò)請求喇澡,因此頻繁切換此類頁面并發(fā)性要求相對來說會比較高,一旦遇到比如:任務(wù)數(shù)據(jù)量大殊校、網(wǎng)絡(luò)超時晴玖、弱網(wǎng)環(huán)境,Volley的表現(xiàn)就很糟糕为流。
在網(wǎng)絡(luò)超時場景下:
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è)置是5s,很明顯看到侠碧,在過了并發(fā)之后抹估,后續(xù)的超時反饋越來越慢。超時時間是從任務(wù)獲取到線程執(zhí)行網(wǎng)絡(luò)請求開始計算的弄兜,因此如果是隊列阻塞狀態(tài)药蜻,上一個任務(wù)耗時會delay到當(dāng)前任務(wù)的執(zhí)行,因此在超時回調(diào)上會是一個累加狀態(tài)替饿,讓后續(xù)的網(wǎng)絡(luò)請求遲遲沒有回調(diào)语泽,有些頁面無法刷新UI,一直處于黑屏或者loading狀態(tài)视卢。該問題在okhttp的線程池中得到很大緩解踱卵。
那有人就問了,為什么不直接換okhttp呢据过?因為這篇文章寫的是Volley惋砂,它畢竟也是一個非常經(jīng)典的網(wǎng)絡(luò)框架,有很多設(shè)計思想是值得學(xué)習(xí)的绳锅,同時嘗試解決老框架的痛點問題西饵,也是一種自我提升。
好了鳞芙,就寫到這眷柔,文章有不對之處還望批評指正期虾,如果有更好的想法也歡迎溝通交流。