Volley 源碼分析

我的博客: Volley 源碼分析

Volley 的使用流程分析

官網(wǎng)示例

  1. 創(chuàng)建一個請求隊列 RequestQueue昔穴,并啟動隊列
  2. 創(chuàng)建一個請求 Request 添加到請求隊列中

創(chuàng)建 RequestQueue 對象

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// 實例化一個請求隊列
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";

// 創(chuàng)建一個期待類型為字符串類型的請求
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!");
    }
});
// 將請求添加到請求隊列中
queue.add(stringRequest);

上面代碼片段的第5行湿痢,我們調用 Volley.newRequestQueue(this) 來創(chuàng)建一個請求隊列。Volley 中提供了兩種創(chuàng)建請求隊列的方法认烁,newRequestQueue(Context context,HttpStack stack)newRequestQueue(Context context)

public static RequestQueue newRequestQueue(Context context) {
//  在此方法內部會調用另一個 newRequestQueue 方法,第二個參數(shù)為 null 代表使用默認的 HttpStack 實現(xiàn)
    return newRequestQueue(context, null);
}
 public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    // 緩存文件目錄 data/data/packagename/cache/volley
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    String userAgent = "volley/0";
    try {
        String packageName = context.getPackageName();
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
        userAgent = packageName + "/" + info.versionCode;
    } catch (NameNotFoundException e) {
    }

    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {
            stack = new HurlStack();//基于HttpClient
        } else {
            //基于HttpUrlConnection
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }
    //利用HttpStack創(chuàng)建一個Network對象
    Network network = new BasicNetwork(stack);

    //創(chuàng)建一個RequestQueue對象纲堵,在構造函數(shù)中傳入緩存對象,網(wǎng)絡對象
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    //啟動隊列
    queue.start();

    return queue;
}

在上面代碼片段中的第2行可以看見闰渔,Volley 調用 getCacheDir() 方法來獲取緩存目錄席函,Volley 中的緩存文件會存儲在 /data/data/packagename/cache 目錄下面,并不是存儲在 SD 卡中的冈涧。

從12~18的代碼可以看見茂附,Volley 當中有對 HttpStack 的默認實現(xiàn)正蛙,HttpStack 是真正用來執(zhí)行請求的接口 ,根據(jù)版本號的不同营曼,實例化不同的對象乒验,在 Android2.3 版本之前采用基于 HttpClient 實現(xiàn)的 HttpClientStack 對象,不然則采用基于 HttpUrlConnection 實現(xiàn)的 HUrlStack溶推。

之后我們通過 HttpStack 構建了一個 Network 對象徊件,它會調用 HttpStack#performRequest() 方法來執(zhí)行請求奸攻,并且將請求的結果轉化成 NetworkResponse 對象蒜危,NetworkResponse 類封裝了響應的響應碼響應體睹耐,響應頭等數(shù)據(jù)辐赞。

接著我們會將之前構建的緩存目錄以及網(wǎng)絡對象傳入 RequestQueue(Cache cache, Network network) 的構造函數(shù)中,構造一個 RequestQueue 對象硝训,然后調用隊列的 start()方法來啟動隊列响委,其實就是啟動隊列中的兩種線程:


//啟動隊列中所有的調度線程.
public void start() {
    stop();  // 確保停止所有當前正在運行的調度線程
    // 創(chuàng)建緩存調度線程,并啟動它窖梁,用來處理緩存隊列中的請求
    mCacheDispatcher = new CacheDispatcher(mCacheQueue,mNetworkQueue,mCache,mDelivery);
    mCacheDispatcher.start();
    
    // 創(chuàng)建一組網(wǎng)絡調度線程赘风,并啟動它們,用來處理網(wǎng)絡隊列中的請求纵刘,默認線程數(shù)量為4邀窃,也可以通過RequestQueue的構造函數(shù)指定線程數(shù)量。
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue,mNetwork,mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

start() 方法中假哎,主要是啟動了兩種線程分別是 CacheDispatcherNetworkDispatcher瞬捕,它們都是線程類,顧名思義 CacheDispatcher 線程會處理緩存隊列中請求舵抹,NetworkDispatcher 處理網(wǎng)絡隊列中的請求肪虎,由此可見在我們調用 Volley 的公開方法創(chuàng)建請求隊列的時候,其實就是開啟了兩種線程在等待著處理我們添加的請求惧蛹。

添加請求 add(Request)

之前我們已經創(chuàng)建了 RequestQueue 對象扇救,現(xiàn)在我們只需要構建一個 Request 對象,并將它加入到請求隊列中即可香嗓。下面我們來看看 add(Request<T> request) 方法:

public <T> Request<T> add(Request<T> request) {
    // 將請求加入到當前請求隊列當中迅腔,毋庸置疑的我們需要將所有的請求集合在一個隊列中,方便我們做統(tǒng)一操作陶缺,例如:取消單個請求或者取消具有相同標記的請求...
    request.setRequestQueue(this);
    synchronized (mCurrentRequests) {
        mCurrentRequests.add(request);
    }

    // 給請求設置順序.
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");

    // 如果請求是不能夠被緩存的钾挟,直接將該請求加入網(wǎng)絡隊列中.
    if (!request.shouldCache()) {
        mNetworkQueue.add(request);
        return request;
    }

    // 如果有相同的請求正在被處理,就將請求加入對應請求的等待隊列中去.等到相同的正在執(zhí)行的請求處理完畢的時候會調用 finish()方法饱岸,然后將這些等待隊列中的請求全部加入緩存隊列中去掺出,讓緩存線程來處理
    synchronized (mWaitingRequests) {
        String cacheKey = request.getCacheKey();
        if (mWaitingRequests.containsKey(cacheKey)) {
            // 有相同請求在處理徽千,加入等待隊列.
            Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
            if (stagedRequests == null) {
                stagedRequests = new LinkedList<>();
            }
            stagedRequests.add(request);
            mWaitingRequests.put(cacheKey, stagedRequests);
            if (VolleyLog.DEBUG) {
                VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);}
        } else {
            // 向mWaitingRequests中插入一個當前請求的空隊列,表明當前請求正在被處理
            mWaitingRequests.put(cacheKey, null);
            mCacheQueue.add(request);
        }
        return request;
    }
}

add.png-53.6kB
add.png-53.6kB

RequestQueue#add(Request) 方法的調用流程

處理請求 Cache/NetworkDispatcher

請求被加入緩存請求隊列或者是網(wǎng)絡請求隊列汤锨,在后臺我們的緩存處理線程双抽,網(wǎng)絡處理線程,一直在運行著等待著請求的到來闲礼。我們先來看看 CacheDispatcher 線程是如何處理的:

CacheDispatcher

public CacheDispatcher(
    BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
    Cache cache, ResponseDelivery delivery) {
    mCacheQueue = cacheQueue;
    mNetworkQueue = networkQueue;
    mCache = cache;
    mDelivery = delivery;
}

這是 CacheDispatcher 的構造函數(shù)牍汹,可以看見該對象內部持有緩存隊列網(wǎng)絡隊列柬泽,緩存對象慎菲,響應投遞對象的引用。

@Override
public void run() {
    if (DEBUG) VolleyLog.v("start new dispatcher");
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

    // 初始化緩存锨并,將緩存目錄下的所有緩存文件的摘要信息加載到內存中.
    mCache.initialize();

    //無線循環(huán)露该,意味著線程啟動之后會一直運行
    while (true) {
        try {
            // 從緩存隊列中取出一個請求,如果沒有請求則一直等待
            final Request<?> request = mCacheQueue.take();
            request.addMarker("cache-queue-take");

            // 如果當前請求已經取消第煮,那就停止處理它
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                continue;
            }
            // 嘗試取出緩存實體對象
            Cache.Entry entry = mCache.get(request.getCacheKey());
            if (entry == null) {
                request.addMarker("cache-miss");
                // 沒有緩存解幼,將當前請求加入網(wǎng)絡請求隊列,讓NetworkDispatcher進行處理.
                mNetworkQueue.put(request);
                continue;
            }

            // 如果緩存實體過期包警,任然將當前請求加入網(wǎng)絡請求隊列撵摆,讓NetworkDispatcher進行處理.
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }

            // 將緩存實體解析成NetworkResponse對象.
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
            new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");
            
            if (!entry.refreshNeeded()) {
                // 緩存依舊新鮮,投遞響應.
                mDelivery.postResponse(request, response);
            } else {
                //緩存已經不新鮮了害晦,我們可以進行響應投遞特铝,然后將請求加入網(wǎng)絡隊列中去,進行新鮮度驗證篱瞎,如果響應碼為 304苟呐,代表緩存新鮮可以繼續(xù)使用,不用刷新響應結果
                request.addMarker("cache-hit-refresh-needed");
                request.setCacheEntry(entry);

                // 標記當前響應為中間響應俐筋,如果經過服務器驗證緩存不新鮮了牵素,那么隨后將有第二條響應到來.這意味著當前請求并沒有完成,只是暫時顯示緩存的數(shù)據(jù)澄者,等到服務器驗證緩存的新鮮度之后才會將請求標記為完成
                response.intermediate = true;

                // 將響應投遞給用戶笆呆,然后加入網(wǎng)絡請求隊列中去.
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mNetworkQueue.put(request);
                        } catch (InterruptedException e) {
                            // Not much we can do about this.
                        }
                    }
                });
            }

        } catch (InterruptedException e) {
            // We may have been interrupted because it was time to quit.
            if (mQuit) {
                return;
            }
        }
    }
}

在代碼的注釋中基本上可以理清 CacheDispatcher 的工作流程,下面附上流程圖

緩存run方法 .png-95.6kB
緩存run方法 .png-95.6kB

CacheDispatcher#run() 方法內部流程

NetworkDispatcher
在 CacheDispatcher 當中我們會把一些不符合條件的請求加入網(wǎng)絡請求隊列中粱挡,下面我們來看看在 NetworkDispatcherrun() 方法中是怎么來處理這些請求的:

public NetworkDispatcher(BlockingQueue<Request<?>> queue,
    Network network, Cache cache,ResponseDelivery delivery) {
    mQueue = queue;
    mNetwork = network;
    mCache = cache;
    mDelivery = delivery;
}

這是 NetworkDispatcher 的構造函數(shù)赠幕,可以看見該對象內部持有網(wǎng)絡隊列緩存對象询筏,響應投遞對象,Network對象的引用榕堰。

@Override
    public void run() {
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    while (true) {
        long startTimeMs = SystemClock.elapsedRealtime();
        Request<?> request;
        try {
            // 從網(wǎng)絡隊列中取出一個請求,沒有請求則一直等待.
            request = mQueue.take();
        } catch (InterruptedException e) {
        // We may have been interrupted because it was time to quit.
            if (mQuit) {
            return;
            }
            continue;
        }

        try {
            request.addMarker("network-queue-take");

            // 如果請求被取消的話,就結束當前請求逆屡,不再執(zhí)行
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                continue;
            }
            addTrafficStatsTag(request);

            // 執(zhí)行網(wǎng)絡請求.
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            request.addMarker("network-http-complete");
            // 如果響應碼為304圾旨,并且我們已經傳遞了一次響應,不需要再傳遞一次驗證的響應,意味著本次請求處理完成魏蔗。也就是說該請求的緩存是新鮮的砍的,我們直接使用就可以了。
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                continue;
            }

            // 在工作線程中想響應數(shù)據(jù)解析成我們需要的Response對象莺治,之所以在工作線程進行數(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 != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);
                request.addMarker("network-cache-written");
            }

            // 傳遞響應數(shù)據(jù).
            request.markDelivered();
            mDelivery.postResponse(request, response);
        } catch (VolleyError volleyError) {
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            parseAndDeliverNetworkError(request, volleyError);
        } catch (Exception e) {
            VolleyLog.e(e, "Unhandled exception %s", e.toString());
            VolleyError volleyError = new VolleyError(e);
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            mDelivery.postError(request, volleyError);
        }
    }
}

在代碼當中的第29行,我們調用 Network 對象的 performRequest(Request<?> request) 方法來執(zhí)行網(wǎng)絡請求蔓挖,并且返回我們需要的 NetworkResponse 對象夕土。如果是304響應馆衔,并且我們已經傳遞過一次響應瘟判,就不需要在傳遞新的解析數(shù)據(jù),不然我們將數(shù)據(jù)解析成 Reponse 對象角溃,并傳遞給主線程進行回到處理拷获,如果該請求允許被緩存,就將該請求的結果寫入緩存中去减细,這就是 Networkdispatcher 的工作流程匆瓜。以下是 NetworkDispatcher 的流程圖:

網(wǎng)絡處理線程run方法.png-73.9kB
網(wǎng)絡處理線程run方法.png-73.9kB

NetworkDispatcher的run() 方法

執(zhí)行請求 performRequest

在上面 NetworkDispatcher 的代碼中第29行,會通過 NetworkperformRequest 方法來進行網(wǎng)絡請求:

public interface Network {
    /**
    * 執(zhí)行指定的請求.
    * @param request 被處理的請求
    * @return 一個 NetworkResponse 對象未蝌,包含響應的數(shù)據(jù)驮吱,頭部以及響應碼等數(shù)據(jù)
    * @throws VolleyError on errors
    */
    NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

Network 是一個接口,它的內部只有這一個方法萧吠,在 Volley 中我們最終調用的是它的實現(xiàn)類左冬,BasicNetwork 的 performRequest() 方法,方法如下所示:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
    long requestStart = SystemClock.elapsedRealtime();//請求開始的時間
    while (true) {
        HttpResponse httpResponse = null;
        byte[] responseContents = null;
        Map<String, String> responseHeaders = Collections.emptyMap();
        try {
            // 收集頭部
            Map<String, String> headers = new HashMap<String, String>();
            //對于addCacheHeaders這個方法纸型,我們也會在緩存的部分進行介紹
            addCacheHeaders(headers, request.getCacheEntry());//附加請求頭部,用來驗證緩存數(shù)據(jù)拇砰?
            httpResponse = mHttpStack.performRequest(request, headers);
            StatusLine statusLine = httpResponse.getStatusLine();
            int statusCode = statusLine.getStatusCode();

            responseHeaders = convertHeaders(httpResponse.getAllHeaders());
            // 驗證緩存的新鮮度.
            if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
                //請求的資源沒有修改,意思可以使用緩存中的數(shù)據(jù)
                Cache.Entry entry = request.getCacheEntry();
                if (entry == null) {
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
                    responseHeaders, true,SystemClock.elapsedRealtime() -requestStart);
                }

                // A HTTP 304 response does not have all header fields. We
                // have to use the header fields from the cache entry plus
                // the new ones from the response.
                // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
                entry.responseHeaders.putAll(responseHeaders);
                return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
                            entry.responseHeaders, true,
                            SystemClock.elapsedRealtime() - requestStart);
            }

            // Some responses such as 204s do not have content.  We must check.
            if (httpResponse.getEntity() != null) {
                responseContents = entityToBytes(httpResponse.getEntity());
            } else {
                // Add 0 byte response as a way of honestly representing a
                // no-content request.
                responseContents = new byte[0];
            }

            // if the request is slow, log it. 請求持續(xù)時間
            long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
            logSlowRequests(requestLifetime, request, responseContents, statusLine);

            if (statusCode < 200 || statusCode > 299) {
                throw new IOException();
            }
            //一條真正的網(wǎng)絡響應
            return new NetworkResponse(statusCode, responseContents, responseHeaders, false,SystemClock.elapsedRealtime() - requestStart);
        } catch (SocketTimeoutException e) {
            //......這部分代碼是用來進行請求重試的狰腌,我們隨后在做解析
        } 
    }
}

在上面代碼的第12行除破,又會調用 HttpStack 對象的 performRequest() 方法去執(zhí)行網(wǎng)絡請求,在 HttpStack 中才真正進行網(wǎng)絡請求琼腔,HttpStack 對象在我們一開始調用 Volley.newRequestQueue() 的方法時候初始化的瑰枫,默認情況下,如果系統(tǒng)版本在 Android2.3 之前就會創(chuàng)建 HttpClientStack丹莲,之后就會創(chuàng)建 HUrlStack 對象光坝,同樣我們也可以實現(xiàn)自己的 HttpStack對象剖毯,通過 Volley 的重載方法 newRequestQueue(Context,HttpStack) 將我們自定義的 HttpStack 傳入即可。

在代碼的18行教馆,我們會進行新鮮度驗證逊谋,如果是304響應那么我們會直接利用緩存實體的數(shù)據(jù)。之后會將響應的數(shù)據(jù)組裝成一個 NetworkResponse 對象返回土铺。
在回到之前 NetworkDispatcher 的代碼中胶滋,當我們獲得這個 NetworkResponse 對象之后,如果是304響應那我們的請求處理結束悲敷,不然的話就會調用 Request#parseNetworkResponse(NetworkResponse) 方法將 NetworkResponse 對象解析成我們需要的 Response 對象究恤,這是一個抽象方法,由子類具體實現(xiàn)來解析成期望的響應類型后德,此方法在工作線程調用部宿。如果該請求可以被緩存,就會將響應實體寫入緩存瓢湃,標記請求被投遞理张,然后調用 ResponseDelivery 對象的 postResponse() 方法來將解析的結果投遞到主線程中,然后進行回調處理绵患。

響應傳遞雾叭、回調 postResponse

響應結果投遞接口,主要負責將響應的結果/錯誤落蝙,投遞到主線程中织狐,供回調函數(shù)處理:

public interface ResponseDelivery {
    /**
     * 傳遞從網(wǎng)絡或者緩存中解析的Response對象.
     */
    void postResponse(Request<?> request, Response<?> response);

    /**
     * 傳遞從網(wǎng)絡或者緩存中解析的Response對象.提供一個Runnable對象,會在傳遞之后執(zhí)行
     */
    void postResponse(Request<?> request, Response<?> response, Runnable runnable);

    /**
     * 傳遞給定請求的Error
     */
    void postError(Request<?> request, VolleyError error);
}

它的內部實現(xiàn)類為 ExecutorDelivery筏勒,讓我們來看看 ExecutorDelivery 中的具體實現(xiàn):

 private final Executor mResponsePoster;
 
 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);
            }
        };
    }
    
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
    request.markDelivered();
    request.addMarker("post-response");
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}

我們會在 postResponse 方法中調用 mResponsePoster 對象的 execute 方法移迫,緊接著通過 handler 對象發(fā)送一個消息,這個消息是 ResponseDeliveryRunnable 對象管行,它是 Runnable 的實現(xiàn)類厨埋,并且這個 Runnable 對象會在主線程被執(zhí)行,為什么呢病瞳?這是因為 handler 是在 ExecutorDelivery 初始化的時候作為參數(shù)傳遞出來的揽咕,我們可以看一下該構造函數(shù)調用的時機:

 public RequestQueue(Cache cache, Network network, int threadPoolSize) {
    this(cache, network, threadPoolSize,
            new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}

可以看見我們使用主線的 Looper 在構建一個 Handler對象,所以由該 Handler 對象發(fā)送的消息套菜,都會在主線程被執(zhí)行亲善,不熟悉 Handler 機制的可以看下這篇文章 Android消息機制

接著我們看看這個 ResponseDeliveryRunnable 類:

private class ResponseDeliveryRunnable implements Runnable {
    //......省略
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        // If this request has canceled, finish it and don't deliver.
        /** 如果請求已經取消的話逗柴,就不用在投遞了*/
        if (mRequest.isCanceled()) {
            mRequest.finish("canceled-at-delivery");
            return;
        }

        // Deliver a normal response or error, depending.
        if (mResponse.isSuccess()) {
            mRequest.deliverResponse(mResponse.result);
        } else {
            mRequest.deliverError(mResponse.error);
        }

        // If this is an intermediate response, add a marker, otherwise we're done
        // and the request can be finished.
        if (mResponse.intermediate) {
            mRequest.addMarker("intermediate-response");
        } else {
            mRequest.finish("done");
        }

        // If we have been provided a post-delivery runnable, run it.
        if (mRunnable != null) {
                mRunnable.run();
        }
      }
}

這個 Runnable 會在主線程執(zhí)行蛹头,然后將響應結果傳遞給請求的回調函數(shù)。在代碼中的15,17行就是我們的回調函數(shù)渣蜗,每一個請求的響應結果屠尊,不論是成功或者是失敗,都會傳遞到這兩個方法中耕拷,第15行的 deliverResponse 方法也是一個抽象方法讼昆,由子類實現(xiàn),參照 Volley 提供的 StringRequest 可以看見骚烧,在這個方法中浸赫,我們最終將期望的對象傳遞給了 Response 中的 onResponse() 方法中,可以看 創(chuàng)建 RequestQueue 對象 這一段落的第一段代碼中的12行

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!");
    }
});

請求的結果最終就會被傳遞這 onResponse,onErrorResponse 中赃绊。在這里貼一張 Volley 內部的 Response 轉換流程圖:

Response 處理流程圖
Response 處理流程圖

圖片取自codeKK Volley 源碼分析

請求完成 Request#finish(String)

在執(zhí)行完我們的回調函數(shù)之后既峡,會調用 Request 中的 finish() 方法標記請求完成,然后會將我們的請求從請求隊列中移除,以下代碼展示了兩處調用 finish() 的地方:

if (mResponse.intermediate) {
    mRequest.addMarker("intermediate-response");
} else {
    mRequest.finish("done");
}
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
    request.finish("not-modified");
    continue;
}

他們的區(qū)別是碧查,第一段代碼的 Response 是從網(wǎng)絡返回的數(shù)據(jù)运敢,而第二段代碼是代表我們之前傳遞了需要驗證緩存新鮮度的緩存實體,經驗證后緩存新鮮忠售,標記請求完成传惠,大家可以查看一下 Response 的 intermediate 屬性被賦值的時機就明白了。

// Request中的finish()會調用 RequestQueue中的finish()方法
void finish(final String tag) {
    if (mRequestQueue != null) {
         mRequestQueue.finish(this);
    }
}
// RequestQueue#finish(Request)
<T> void finish(Request<T> request) {
    //從當前的請求隊列中移除該請求
    synchronized (mCurrentRequests) {
        mCurrentRequests.remove(request);
    }
    //調用該請求設置的回調函數(shù)
    synchronized (mFinishedListeners) {
        for (RequestFinishedListener<T> listener : mFinishedListeners) {
        listener.onRequestFinished(request);
        }
    }
    if (request.shouldCache()) {
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
            if (waitingRequests != null) {
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
                            waitingRequests.size(), cacheKey);
                }
                // Process all queued up requests. They won't be considered as in flight, but
                // that's not a problem as the cache has been primed by 'request'.
                mCacheQueue.addAll(waitingRequests);
            }
        }
    }
}

在移除已經完成的請求之后档痪,如果該請求是可以緩存的涉枫,并且存在著等待該請求的等待隊列,就將隊列中的所有請求加入緩存隊列(mCacheQueue) 中去腐螟,讓緩存線程接著處理。

補充一點---取消請求

  • 可以調用 Request 的 cancel() 方法來標記請求取消困后,這樣我們的回調函數(shù)永遠不會被調用
  • 可以調用 RequstQueue 的 cancelAll(Object) 的方法來批量取消被打上 Object 標記的請求
  • 可以調用 RequstQueue 的 cancelAll(RequestFilter) 的方法乐纸,按照自定義的過濾方法來取消符合過濾條件的請求

抽象的處理流程圖

抽象的流程圖.png-43.3kB
抽象的流程圖.png-43.3kB

通過 CacheDispatcher 和 NetworkDispatcher 兩種線程不斷的從 RequestQueue 中取出請求來處理,然后將獲取的數(shù)據(jù)在子線程解析成我們需要的結果摇予,通過 ResponseDelivery 的 postResponse 方法將結果投遞到主線程中去汽绢,觸發(fā)回調。

請求緩存和重試機制

再此之前我們先看一下 Request 類中的一些重要屬性和方法:
Request<T>
所有請求的抽象類侧戴,T 類型代表請求期望的類型宁昭,也是響應最終被解析成的類型。支持 Get,Post,Put,Delete,Options,Trace,Head,Patch 共8種請求酗宋,提供 Low,Normal,Hight,Imediate 4種優(yōu)先級积仗。下面會挑出一些比較重要的字段和方法進行講解:

  • mShouldCache,用于標識請求是否允許緩存蜕猫,緩存需要客戶端和服務器的支持寂曹,這個字段僅僅代表客戶端是否支持緩存
  • mShouldRetryServerErrors,默認值為 false,代表在服務器返回響應碼在 500~599的范圍內的話(服務器錯誤)隆圆,不進行請求重試
  • mCacheEntry漱挚,該請求的緩存實體,在 CacheDispatcher 處理 CacheQueue 中請求的時候渺氧,會判斷該請求之前是否有緩存存在旨涝,如果存在的話將緩存實體賦值給該字段。用于在服務器返回 304 響應的時候構建 NetworkResponse 對象
  • mResponseDelivered侣背,代表該請求的響應結果已經被投遞到主線程颊糜,只有在響應被傳遞給主線程的時候標記為 true,它的作用同樣是用來驗證緩存一致性
  • abstract protected Response<T> parseNetworkResponse(NetworkResponse response);抽象方法秃踩,將請求返回的結果解析成請求期望的類型衬鱼,具體的解析方式需要子類實現(xiàn)
  • abstract protected void deliverResponse(T response);抽象方法,也同樣需要子類自行實現(xiàn)憔杨,將解析后的結果傳遞給請求的回調函數(shù)
    之前我們有提到過要實現(xiàn)請求緩存需要客戶端和服務器端共同的支持才行鸟赫。

請求緩存

之前我們說過,緩存機制是需要客戶端和服務器端共同支持的消别。從客戶端的角度來說:需要實現(xiàn) Http 緩存相關的語義抛蚤;從服務器的角度來說:需要允許請求的資源被緩存;我們先來看一些有關于 Http 請求頭和響應頭的概念:

HTTP 響應頭

Cache-Control:指明當前資源的有效期寻狂,用來控制從緩存中取數(shù)據(jù)岁经,還是需要將請求發(fā)送到服務器進行驗證,重新取回數(shù)據(jù),該頭部有以下幾個值:

  • no-cache:使用緩存前必須先向服務器確認其有效性
  • no-store:不緩存響應的任何內容蛇券,相同的請求都會發(fā)送給服務器
  • max-age:緩存有效性的最長時間
  • must-revalidate:可緩存缀壤,但是使用的時候必需向源服務器驗證
  • proxy-revalidate:要求中間緩存服務器對緩存的有效性進行確認
  • stale-while-revalidate:在這段時間內,允許先使用緩存纠亚,但需要向服務器驗證緩存的有效性

Expires:資源失效的日期塘慕,如果和 max-age 同時存在,以 max-age 時間為準
ETag:可將資源以字符串形式做唯一性標識的方式蒂胞,服務器會為每份資源分配對應的 Etag 值
Last-Modified:資源最終被修改的時間

HTTP 請求頭

If-None-Match:如果上一次響應的的響應頭部中帶有 ETag 響應頭图呢,再次請求的時候會將 ETag 的值作為 If-None-Match 請求頭的值,當 If-None-Match 的值與請求資源的 Etag 不一致時骗随,服務器會處理該請求蛤织,該字段用來獲取最新的資源
If-Modified-Since:如果上一次響應的響應頭部中帶有 Last-Modified 響應頭,那么再次請求的時候會將 Last-Modified 的值作為 If-Modified-Since 請求頭的值鸿染,在If-Modified-Since 字段指定的之后指蚜,資源發(fā)生了更新,服務器會接受該請求牡昆,否則返回 304 響應

Entry

Cache接口中的內部類姚炕,代表著緩存實體

  • data摊欠,這是一個字節(jié)數(shù)組,其實也就是我們響應的 Content 部分
  • etag柱宦,用來驗證緩存一致性的標記
  • serverDate些椒,數(shù)據(jù)從服務器返回的時間
  • lastModified,訪問的服務器資源上次被修改的時間
  • ttl掸刊,數(shù)據(jù)的過期時間免糕,在(softTtl-ttl)這段時間內我們可以使用緩存,但是必須向服務器驗證緩存的有效性
  • softTtl忧侧,數(shù)據(jù)的新鮮時間石窑,緩存再次之前一直有效
  • responseHeaders,響應頭部
  • boolean isExpired()蚓炬,判斷是否過期松逊,ttl代表的時間小于當前時間就意味著過期了
  • boolean refreshNeeded(),顧名思義肯夏,當softTtl代表的時間小于當前時間经宏,就代表數(shù)據(jù)不新鮮了,需要刷新數(shù)據(jù)

下面我們來看看 Volley 中是怎么利用這些頭部信息來對響應結果進行緩存處理的:
在 NetworkDispatcher 的 run() 方法中有這樣一樣代碼

if (request.shouldCache() && response.cacheEntry != null) {
    mCache.put(request.getCacheKey(), response.cacheEntry);
    request.addMarker("network-cache-written");
}

想要緩存響應結果需要滿足兩個條件驯击,第一條該請求允許緩存烁兰,我們創(chuàng)建的每一條請求都是默認支持緩存的;第二條就是響應對象中的緩存實體不為空徊都。那么我們需要看一下緩存實體是在什么時候被創(chuàng)建的沪斟,在執(zhí)行上述的 if 語句判斷之前會執(zhí)行這么一句代碼 Response<?> response = request.parseNetworkResponse(networkResponse); 通過parseNetworkResponse 方法將 NetworkResponse 對象轉化為 Response 對象,parseNetworkResponse 方法是一個抽象方法暇矫,我們看一下 StringRequest 中是如何重寫該方法的:

@Override
protected Response<String> parseNetworkResponse(NetworkResponse response) {
    String parsed;
    try {
        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
    } catch (UnsupportedEncodingException e) {
        parsed = new String(response.data);
    }
    return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

在代碼的最后一行主之,我們會發(fā)現(xiàn) Response 中的 cacheEntry 字段的值來自于 HttpHeaderParser.parseCacheHeaders(response)方法的返回值,下面我們來看看該方法的內部實現(xiàn):

public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
    long now = System.currentTimeMillis();

    //提取響應頭部信息
    Map<String, String> headers = response.headers;

    long serverDate = 0;//資源返回時間
    long lastModified = 0;//資源上一次被修改的時間
    long serverExpires = 0;//資源的有效時間袱耽,以maxAge為主
    long softExpire = 0;//資源的新鮮時間杀餐,如果在該時間內,發(fā)起相同的請求朱巨,那么可以允許使用緩存的信息,不需要將請求發(fā)送給服務器枉长。
    long finalExpire = 0;//緩存過期時間冀续,在該時間之后的請求,都將發(fā)送給服務器必峰,無法使用緩存洪唐。
    long maxAge = 0;//資源的有效時間,
    //在 staleWhileRevalidate 時間內吼蚁,我們可以先用緩存數(shù)據(jù)展示給用戶凭需,在向服務器驗證緩存的有效性
    long staleWhileRevalidate = 0;
    boolean hasCacheControl = false;//代表是否有 Cache-Control 頭部
    boolean mustRevalidate = false;

    String serverEtag = null;//資源在服務器中的標識
    String headerValue;

    headerValue = headers.get("Date");
    if (headerValue != null) {
        serverDate = parseDateAsEpoch(headerValue);
    }
    //獲取 Cache-Contral 頭部的相關信息
    headerValue = headers.get("Cache-Control");
    if (headerValue != null) {
        hasCacheControl = true;
        String[] tokens = headerValue.split(",");
        for (int i = 0; i < tokens.length; i++) {
            String token = tokens[i].trim();
            if (token.equals("no-cache") || token.equals("no-store")) {
            //如果出現(xiàn)了 no-cache,no-store指令问欠,代表服務器不允許緩存響應,返回的 entry 對象為空
                return null;
            } else if (token.startsWith("max-age=")) {
            //如果出現(xiàn)了 max-age指令粒蜈,那么服務器允許緩存該響應顺献,并且給出響應的過期時間
                try {
                    maxAge = Long.parseLong(token.substring(8));
                } catch (Exception e) {
                }
            } else if (token.startsWith("stale-while-revalidate=")) {
            //這段時間處于新鮮時間和過期時間之間,在這段時間內發(fā)起的請求枯怖,都可以利用之前的緩存信息注整,但是需要將請求發(fā)送給服務器做驗證,如果是304 響應度硝,則請求結束肿轨,不然將重新傳遞響應結果。
                try {
                    staleWhileRevalidate = Long.parseLong(token.substring(23));
                } catch (Exception e) {
                }
            } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
            //代表不區(qū)分新鮮時間與過期時間蕊程,到了max-age指定的時間之后椒袍,請求都將發(fā)送給服務器用來驗證緩存的有效性。
                mustRevalidate = true;
            }
        }
    }

    headerValue = headers.get("Expires");
    if (headerValue != null) {
        serverExpires = parseDateAsEpoch(headerValue);
    }

    headerValue = headers.get("Last-Modified");
    if (headerValue != null) {
        lastModified = parseDateAsEpoch(headerValue);
    }

    serverEtag = headers.get("ETag");

    // 在 Cache-Control 和 Expires 頭部都存在的情況下藻茂,以Cache-Control為準
    if (hasCacheControl) {
        softExpire = now + maxAge * 1000;
        finalExpire = mustRevalidate
                    ? softExpire
                    : softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
    // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }

    Cache.Entry entry = new Cache.Entry();
    entry.data = response.data;
    entry.etag = serverEtag;
    entry.softTtl = softExpire;
    entry.ttl = finalExpire;
    entry.serverDate = serverDate;
    entry.lastModified = lastModified;
    entry.responseHeaders = headers;

    return entry;
}

這是一個靜態(tài)工具方法驹暑,用于提取響應頭部的信息,來構建一個 Cache.Entry 類型的緩存對象捌治,針對該方法的分析都已經寫在注釋當中「诠常現(xiàn)在我們已經了解了將響應轉化為緩存的部分,下面我們來看看肖油,Volley 是如何使用緩存的,在上面我們介紹 CacheDispatcher 工作流程的時候已經大致看過了處理緩存請求的過程兼吓,下面我再針對緩存的部分具體分析一下,下面是 CacheDispatcher run() 方法的部分代碼:

Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
    mNetworkQueue.put(request);
    continue;
}
if (entry.isExpired()) {
    request.addMarker("cache-hit-expired");
    request.setCacheEntry(entry);
    mNetworkQueue.put(request);
    continue;
}
Response<?> response = request.parseNetworkResponse(
            new NetworkResponse(entry.data, entry.responseHeaders));
if (!entry.refreshNeeded()) {
    mDelivery.postResponse(request, response);
} else {
    request.setCacheEntry(entry);
    response.intermediate = true;
    mDelivery.postResponse(request, response, new Runnable() {
        @Override
        public void run() {
            try {
            mNetworkQueue.put(request);
            } catch (InterruptedException e) {
            }
        }
    });
}

先從緩存中取出緩存實體森枪,然后通過 isExpired() 判斷該緩存有無過期视搏,內部是通過 return this.ttl < System.currentTimeMillis(); 的形式來進行比較,ttl 的含義县袱,在??的代碼中我們已經介紹過了浑娜,之后通過 return this.softTtl < System.currentTimeMillis();的方式來判斷實體是否需要刷新,softTtl的值我們同樣已經介紹過了式散,如果不需要刷新筋遭,那么我們就可以直接使用緩存,不然的話就需要向服務器驗證緩存的有效性暴拄。那么如何通知服務器來進行驗證呢漓滔,接下來我們看看執(zhí)行請求的時候,在 BasicNetwork # performRequest() 中調用 HttpStack 執(zhí)行請求執(zhí)行乖篷,會調用 addCacheHeaders(headers, request.getCacheEntry()); 方法响驴,用來附加請求頭部信息,我們看看該方法內部的實現(xiàn):

private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
// If there's no cache entry, we're done.
    if (entry == null) {
    return;
    }

    if (entry.etag != null) {
        headers.put("If-None-Match", entry.etag);
    }
    if (entry.lastModified > 0) {
        Date refTime = new Date(entry.lastModified);
        headers.put("If-Modified-Since",DateUtils.formatDate(refTime));
    }
}

結合我們上面對請求頭的介紹撕蔼,大家很容易明白這段代碼的意思豁鲤,如果資源沒有發(fā)生改變秽誊,就會返回 304 響應碼,告訴我們可以使用之前的緩存琳骡,對緩存的分析就到這里席揽。

Byte[] 緩存

再次看一下下面這段代碼:

// Some responses such as 204s do not have content.  We must check.
if (httpResponse.getEntity() != null) {
    responseContents = entityToBytes(httpResponse.getEntity());
} else {
    // Add 0 byte response as a way of honestly representing a
    // no-content request.
    responseContents = new byte[0];
}

這段代碼位于 BasicNetwork 的 performRequest() 方法中乏屯,用于將返回的 HttpEntity 對象轉化為 byte[] 對象,這個字節(jié)數(shù)組最后將用于被轉化為 T 類型的對象,也就是請求期望的對象童本,我們之所以沒有直接返回 HttpEntity 對象惕它,而把它解析成 byte[] 就是為之后 Request 的子類進行解析提供便利曾棕。
接下來油昂,我們看一下用于轉換數(shù)據(jù)的方法:

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();
    }
}

上面的代碼中有兩種類型對象我們需要注意,一個是 PoolingByteArrayOutputStream 對象 bytes毕荐,另一個是 ByteArrayPool 對象 mPool束析。我們先說一下沒有這兩個對象之前的轉化方式,首先我們從 HttpEntity 打開是一個輸入流憎亚,然后構建一個緩沖字節(jié)數(shù)組 buffer员寇,不斷的將輸入流的數(shù)據(jù)寫入 buffer 中,在通過 ByteArrayOutputStream 不斷的將 buffer 中的數(shù)據(jù)輸入到 ByteArrayOutputStream 中的 buf 字節(jié)數(shù)組中第美,如果 buf 的大小不夠蝶锋,將會 new 出新的 byte[] 對象,賦值給 buf

在這個過程中什往,byte[] 對象被不斷的創(chuàng)建和銷毀扳缕,內存不斷的分配和回收,如果處理不斷可能會造成內存泄漏别威,消耗了系統(tǒng)資源躯舔,Volley 通過 ByteArrayPoolPoolingByteArrayOutputStream 來解決這個問題,先看一下 ByteArrayPool 的代碼:

public class ByteArrayPool {
    private final List<byte[]> mBuffersByLastUse = new LinkedList<byte[]>();//按照使用順序排列的Buffer緩存
    private final List<byte[]> mBuffersBySize = new ArrayList<byte[]>(64);//按照 byte[]大小排列的Buffer緩存

    /** 當前緩存池中緩存的總大小 */
    private int mCurrentSize = 0;

    /**
     * 緩存池上限,達到這個限制之后省古,最近最長時間未使用的 byte[]       * 將被丟棄
     */
    private final int mSizeLimit;

    /** 通過 buffer 的大小進行比較 */
    protected static final Comparator<byte[]> BUF_COMPARATOR = new Comparator<byte[]>() {
        @Override
        public int compare(byte[] lhs, byte[] rhs) {
            return lhs.length - rhs.length;
        }
    };
    public ByteArrayPool(int sizeLimit) {
        mSizeLimit = sizeLimit;
    }

    // 從緩存池中獲取所需的 byte[]粥庄,如果沒有大小合適的 byte[],將新new一個 byte[] 對象
    public synchronized byte[] getBuf(int len) {
        for (int i = 0; i < mBuffersBySize.size(); i++) {
            byte[] buf = mBuffersBySize.get(i);
            if (buf.length >= len) {
                mCurrentSize -= buf.length;
                mBuffersBySize.remove(i);
                mBuffersByLastUse.remove(buf);
                return buf;
            }
        }
        return new byte[len];
    }

    // 將使用后的 byte[] 返還給 緩存池
    public synchronized void returnBuf(byte[] buf) {
        if (buf == null || buf.length > mSizeLimit) {
            return;
        }
        mBuffersByLastUse.add(buf);
        int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
        if (pos < 0) {
            pos = -pos - 1;
        }
        mBuffersBySize.add(pos, buf);
        mCurrentSize += buf.length;
        trim();
    }

    // 對緩存池數(shù)據(jù)進行修剪
    private synchronized void trim() {
        while (mCurrentSize > mSizeLimit) {
            byte[] buf = mBuffersByLastUse.remove(0);
            mBuffersBySize.remove(buf);
            mCurrentSize -= buf.length;
        }
    }

}

簡單的來說就是,ByteArrayPool 通過在內存中維護了兩組 byte[] 對象豺妓,來減少重復創(chuàng)建 byte[] 的次數(shù)惜互。當我們需要使用 byte[] 的時候,通過 Pool 來獲取一個 byte[],當使用完畢的時候琳拭,再將該字節(jié)數(shù)組返回給 Pool载佳。

在來看看 PoolingByteArrayOutputStream 的代碼,他是 ByteArrayOutputStream 的子類,內部使用了 ByteArrayPool 來代替 new Byte[]操作臀栈,提高性能:

public class PoolingByteArrayOutputStream extends ByteArrayOutputStream {
    /**
     * 默認的 buf 大小
     */
    private static final int DEFAULT_SIZE = 256;

    private final ByteArrayPool mPool;

    /**
     * 如果寫入的數(shù)據(jù)超出 buf 的大小,將會擴展 buf 的大小挠乳,之前通過 new Byte[] 來分配更大的空間权薯,現(xiàn)在通過 ByteArrayPool 提供姑躲,避免創(chuàng)建對象
     */
    public PoolingByteArrayOutputStream(ByteArrayPool pool) {
        this(pool, DEFAULT_SIZE);
    }
    public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) {
        mPool = pool;
        buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE));
    }
    //關閉輸出流,將使用的 buf 歸還到緩沖池中
    @Override
    public void close() throws IOException {
        mPool.returnBuf(buf);
        buf = null;
        super.close();
    }

    // GC 的時候調用盟蚣,我們不能保證高方法的觸發(fā)時機黍析,所以最好手動調用 close 方法
    @Override
    public void finalize() {
        mPool.returnBuf(buf);
    }

    /**
     * 擴展 Buf 的大小
     */
    private void expand(int i) {
        // 判斷 buffer 能否處理更多的byte,不能的話將要擴展 buffer 的大小
        if (count + i <= buf.length) {
            return;
        }
        byte[] newbuf = mPool.getBuf((count + i) * 2);
        System.arraycopy(buf, 0, newbuf, 0, count);
        mPool.returnBuf(buf);
        buf = newbuf;
    }

    @Override
    public synchronized void write(byte[] buffer, int offset, int len) {
        expand(len);
        super.write(buffer, offset, len);
    }

    @Override
    public synchronized void write(int oneByte) {
        expand(1);
        super.write(oneByte);
    }
}

內部的操作很簡單屎开,在每次寫入的時候都會檢查 buffer 大小是否合適阐枣,是否需要擴展,在輸出流結束的時候奄抽,我們需要手動顯示調用 close 方法蔼两,來歸還從 ByteArrayPool 中擴展的 byte[]

請求重試

RetryPolicy

RetryPolicy 接口,代表著請求重試的行為:

public interface RetryPolicy {
    //當前超時的時間
    int getCurrentTimeout();
    //當前重試的次數(shù)
    int getCurrentRetryCount();

    /**
     * 準備重試
     * 當拋出 VolleyError 即意味著停止重試
     */
    void retry(VolleyError error) throws VolleyError;
}

在我們初始化 Request 的時候逞度,會給 Request 設置一個默認的重試策略 DefaultRetryPolicy 下面我們來看看它的代碼:

DefaultRetryPolicy

public class DefaultRetryPolicy implements RetryPolicy {
    /** 當前超時毫秒數(shù). */
    private int mCurrentTimeoutMs;

    /** 當前重試次數(shù). */
    private int mCurrentRetryCount;

    /** 最大重試次數(shù). */
    private final int mMaxNumRetries;

    /** 超時乘積因子额划,用來累計計算超時時間. */
    private final float mBackoffMultiplier;

    /** 默認超時時間 */
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** 默認重試次數(shù) */
    public static final int DEFAULT_MAX_RETRIES = 1;

    /** 默認超時乘積因子 */
    public static final float DEFAULT_BACKOFF_MULT = 1f;

    public DefaultRetryPolicy() {
        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
    }

    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
        mCurrentTimeoutMs = initialTimeoutMs;
        mMaxNumRetries = maxNumRetries;
        mBackoffMultiplier = backoffMultiplier;
    }

    /**
     * 返回當前超時時間
     */
    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    /**
     * 返回當前重試次數(shù).
     */
    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    /**
     * 返回超時乘積因子.
     */
    public float getBackoffMultiplier() {
        return mBackoffMultiplier;
    }

    /**
     * 為下一次重試計算重試時間
     * @param error 上一次請求的錯誤.
     */
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        //累計下一次重試的時間
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
        //拋出參數(shù)中傳入的錯誤,就代表停止重試
    }

    /**
     * 判斷是否允許下一次重試
     */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}

默認的重試策略也挺簡單的档泽,每一次累計超時的時間俊戳,然后判斷是否到達重試的上限,如果達到上限馆匿,就拋出入?yún)⒌?VolleyError 代表停止重試抑胎,那么為什么拋出傳入的參數(shù),就可以停止重試了呢渐北,我們繼續(xù)看看 BasicNetwork 中的 performRequest() 方法:

public NetworkResponse performRequest(Request<?> request) throws VolleyError {
    while (true) {
        try{
        
              //........省略
              
        } catch (SocketTimeoutException e) {
            attemptRetryOnException("socket", request, new TimeoutError());
        } catch (ConnectTimeoutException e) {
            attemptRetryOnException("connection", request, new TimeoutError());
        } catch (MalformedURLException e) {
            throw new RuntimeException("Bad URL " + request.getUrl(), e);
        } catch (IOException e) {
            int statusCode;
            if (httpResponse != null) {
                statusCode = httpResponse.getStatusLine().getStatusCode();
            } else {
                throw new NoConnectionError(e);
            }
            VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
            NetworkResponse networkResponse;
            if (responseContents != null) {
                networkResponse = new NetworkResponse(statusCode, responseContents,responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
                if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
                    statusCode == HttpStatus.SC_FORBIDDEN) {
                    attemptRetryOnException("auth",
                    request, new AuthFailureError(networkResponse));
                } else if (statusCode >= 400 && statusCode <= 499) {
                    // Don't retry other client errors.
                    throw new ClientError(networkResponse);
                } else if (statusCode >= 500 && statusCode <= 599) {
                    if (request.shouldRetryServerErrors()) {
                        attemptRetryOnException("server",
                        request, new ServerError(networkResponse));
                    } else {
                        throw new ServerError(networkResponse);
                    }
                } else {
                    // 3xx? No reason to retry.
                    throw new ServerError(networkResponse);
                }
            } else {
                attemptRetryOnException("network", request, new NetworkError());
            }
        }
    }
}

這部分的代碼我們在上面分析 BasicNetwork 的代碼的時候已經介紹過了阿逃,當時省略了 catch 塊中的代碼,catch 塊中的代碼就是用來實現(xiàn)請求重試的腔稀。

當捕獲到 SocketTimeoutExceptionConnectTimeoutException 異常的時候調用 attemptRetryOnException() 方法來進行重試盆昙,該方法中會調用 retryPolicy.retry(exception);方法,該方法我們已經分析過了焊虏,是用來計算請求超時時間淡喜,以及是否達到重試上限,如果可以重試诵闭,那么該方法執(zhí)行完炼团,會繼續(xù)下一次循環(huán),再次發(fā)起請求疏尿;當達到重試上線瘟芝,無法進行重試的時候我們會拋出 VolleyError 的實例,在 BasicNetwork#performRequest() 中我們沒有捕獲 VolleyError 異常褥琐,因次會跳出循環(huán)锌俱,停止重試,該方法執(zhí)行結束敌呈,在外部 NetworkDispatcher 的 run() 方法中捕獲了該異常贸宏,將異常結果傳遞到主線程中供回調函數(shù)處理造寝。

  • ConnectTimeoutException 表示請求超時
  • SocketTimeoutException 表示響應超時

在代碼中同樣對 AuthFailureError 以及服務器異常提供了重試操作。

一些總結

  • Volley 可以幫助我們完成請求的自動調度處理吭练,我們只需要將 Request 加入 RequestQueue 就可以了
  • 提供了多個并發(fā)線程幫助處理請求诫龙,但是不適合大文件下載,因為在響應解析的過程中鲫咽,會將所有響應的數(shù)據(jù)保存在內存中
  • 提供了緩存(一定程度上符合 HTTP 語義)
  • 支持請求的優(yōu)先級签赃,可以方便的取消請求(根據(jù) Tag 或者自定義的過濾規(guī)則)
  • 提供了請求重試機制,可以自定義重試機制(簡單分尸,方便)
  • Volley 面向接口編程锦聊,采用組合(少用繼承)的形式提供功能,可以自定義 Network寓落,HttpStack括丁,Cache 等實現(xiàn),擴展性很高
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末伶选,一起剝皮案震驚了整個濱河市史飞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌仰税,老刑警劉巖构资,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異陨簇,居然都是意外死亡吐绵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門河绽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來己单,“玉大人,你說我怎么就攤上這事耙饰∥屏” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵苟跪,是天一觀的道長廷痘。 經常有香客問我,道長件已,這世上最難降的妖魔是什么笋额? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮篷扩,結果婚禮上兄猩,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好厦滤,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布援岩。 她就那樣靜靜地躺著,像睡著了一般掏导。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上羽峰,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天趟咆,我揣著相機與錄音,去河邊找鬼梅屉。 笑死值纱,一個胖子當著我的面吹牛,可吹牛的內容都是我干的坯汤。 我是一名探鬼主播虐唠,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼惰聂!你這毒婦竟也來了疆偿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤搓幌,失蹤者是張志新(化名)和其女友劉穎杆故,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溉愁,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡处铛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了拐揭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撤蟆。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖堂污,靈堂內的尸體忽然破棺而出家肯,到底是詐尸還是另有隱情,我是刑警寧澤敷鸦,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布息楔,位于F島的核電站,受9級特大地震影響扒披,放射性物質發(fā)生泄漏值依。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一碟案、第九天 我趴在偏房一處隱蔽的房頂上張望愿险。 院中可真熱鬧,春花似錦、人聲如沸辆亏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扮叨。三九已至缤弦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間彻磁,已是汗流浹背碍沐。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留衷蜓,地道東北人累提。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像磁浇,于是被迫代替她去往敵國和親斋陪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容