重溫Volley源碼(二):重試策略

目錄

一、核心類

二忠荞、重試策略

參考資料

本文是建立在對Volley框架的工作流程有一定基礎了解的情況下繼續(xù)深入的,這里順便貼上我寫的上篇文章《重溫Volley源碼(一):工作流程》 ,歡迎閱讀指正夫嗓。

一桐玻、核心類

RetryPolicy:Volley定義的請求重試策略接口


public interface RetryPolicy {

    /**當前請求的超時時間
     * Returns the current timeout (used for logging).
     */
    public int getCurrentTimeout();

    /**當前請求重試的次數(shù)
     * Returns the current retry count (used for logging).
     */
    public int getCurrentRetryCount();

    /**在請求異常時調用此方法
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     * @throws VolleyError In the event that the retry could not be performed (for example if we
     * ran out of attempts), the passed in error is thrown.
     */
    public void retry(VolleyError error) throws VolleyError;
}

DefaultRetryPolicy:RetryPolicy的實現(xiàn)子類


public class DefaultRetryPolicy implements RetryPolicy {
    /** The current timeout in milliseconds. 當前超時時間*/
    private int mCurrentTimeoutMs; 

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

    /** The maximum number of attempts. 最大重試次數(shù)*/
    private final int mMaxNumRetries;

    /** The backoff multiplier for the policy. 超時時間的乘積因子*/
    private final float mBackoffMultiplier;

    /** The default socket timeout in milliseconds 默認超時時間*/
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** The default number of retries 默認的重試次數(shù)*/
    public static final int DEFAULT_MAX_RETRIES = 0;

    /** The default backoff multiplier 默認超時時間的乘積因子*/
    /**
    *   以默認超時時間為2.5s為例
    *   DEFAULT_BACKOFF_MULT = 1f, 則每次HttpUrlConnection設置的超時時間都是2.5s*1f*mCurrentRetryCount
    *   DEFAULT_BACKOFF_MULT = 2f, 則第二次超時時間為:2.5s+2.5s*2=7.5s,第三次超時時間為:7.5s+7.5s*2=22.5s
    */
    public static final float DEFAULT_BACKOFF_MULT = 1f;


    /**
     * Constructs a new retry policy using the default timeouts.
     */
    public DefaultRetryPolicy() {
        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
    }

    /**
     * Constructs a new retry policy.
     * @param initialTimeoutMs The initial timeout for the policy.
     * @param maxNumRetries The maximum number of retries.
     * @param backoffMultiplier Backoff multiplier for the policy.
     */
    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
        mCurrentTimeoutMs = initialTimeoutMs;
        mMaxNumRetries = maxNumRetries;
        mBackoffMultiplier = backoffMultiplier;
    }

    /**
     * Returns the current timeout.
     */
    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    /**
     * Returns the current retry count.
     */
    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    /**
     * Returns the backoff multiplier for the policy.
     */
    public float getBackoffMultiplier() {
        return mBackoffMultiplier;
    }

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     */
    @Override
    public void retry(VolleyError error) throws VolleyError {
        //當前重試次數(shù)++
        mCurrentRetryCount++;
        //當前超時時間計算
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        //判斷是否還有剩余次數(shù)篙挽,如果沒有則拋出VolleyError異常
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    /**
     * 判斷當前Request的重試次數(shù)是否超過最大重試次數(shù)
     * Returns true if this policy has attempts remaining, false otherwise.
     */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}

二、重試策略

在深入源碼閱讀你會發(fā)現(xiàn)镊靴,retry方法會拋出VolleyError的異常铣卡,但該方法內部并不是重新發(fā)起了網(wǎng)絡請求,而是變更重試策略的屬性偏竟,如超時時間和重試次數(shù)煮落,當超過重試策略設定的限定就會拋異常,這個可以在DefaultRetryPolicy里得到驗證踊谋。那么它究竟是如何做到重試的呢蝉仇,我們可以跟蹤源碼 retry 方法被調用到的地方,來到了BasicNetwork的attemptRetryOnException方法:


public class BasicNetwork implements Network {

......

    /**
    *  嘗試對一個請求進行重試策略殖蚕,
    */
    private static void attemptRetryOnException(String logPrefix, Request<?> request,
            VolleyError exception) throws VolleyError {
        RetryPolicy retryPolicy = request.getRetryPolicy(); //獲取該請求的重試策略
        int oldTimeout = request.getTimeoutMs(); //獲取該請求的超時時間

        try {
            retryPolicy.retry(exception); //內部實現(xiàn)重試次數(shù)轿衔、超時時間的變更,如果重試次數(shù)超過最大限定次數(shù)睦疫,該方法拋出異常
        } catch (VolleyError e) {
            //當超過最大重試次數(shù)害驹,捕獲到異常,更改該請求的標記
            request.addMarker(
                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
            //當仍然可以進行重試的時候蛤育,不會執(zhí)行到catch語句宛官,但是當執(zhí)行到catch語句的時候,表示已經(jīng)不能進行重試了瓦糕,就拋出異常  中斷while(true)循環(huán)
            throw e;
        }
        //給請求添加標記底洗,請求了多少次  
        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    }

}

繼續(xù)跟蹤attemptRetryOnException方法被調用的地方,來到了BasicNetwork的performRequest方法:


public class BasicNetwork implements Network {

......

    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        long requestStart = SystemClock.elapsedRealtime();
        while (true) {
            ......

            try {
                ......

                //如果發(fā)生了超時咕娄、認證失敗等錯誤枷恕,進行重試操作,直到成功谭胚。若attemptRetryOnException再拋出異常則結束
                //當catch后沒有執(zhí)行上面的return徐块,而當前又是一個while(true)循環(huán)未玻,可以保證下面的請求重試的執(zhí)行,是利用循環(huán)進行請求重試胡控,請求重試策略只是記錄重試的次數(shù)扳剿、超時時間等內容
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                        SystemClock.elapsedRealtime() - requestStart);
            } catch (SocketTimeoutException e) {
                //1.嘗試進行請求重試
                attemptRetryOnException("socket", request, new TimeoutError());
            } catch (ConnectTimeoutException e) {
                //2.嘗試進行請求重試
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (MalformedURLException e) {
                throw new RuntimeException("Bad URL " + request.getUrl(), e);
            } catch (IOException e) {
                int statusCode = 0;
                NetworkResponse networkResponse = null;
                if (httpResponse != null) {
                    statusCode = httpResponse.getStatusLine().getStatusCode();
                } else {
                    throw new NoConnectionError(e);
                }
                if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
                        statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
                } else {
                    VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
                }
                if (responseContents != null) {
                    networkResponse = new NetworkResponse(statusCode, responseContents,
                            responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
                    if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
                            statusCode == HttpStatus.SC_FORBIDDEN) {
                        //3.嘗試進行請求重試
                        attemptRetryOnException("auth",
                                request, new AuthFailureError(networkResponse));
                    } else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
                                statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                        //4.嘗試進行請求重試
                        attemptRetryOnException("redirect",
                                request, new RedirectError(networkResponse));
                    } else {
                        // TODO: Only throw ServerError for 5xx status codes.
                        throw new ServerError(networkResponse);
                    }
                } else {
                    throw new NetworkError(e);
                }
            }
        }
    }

}

performRequest這個方法名是否很眼熟?是的昼激,它曾被我在上一篇文章中簡單提到過庇绽,還記得在NetworkDispatcher的run方法中,mNetWork對象(即BasicNetwork)會調用performRequest來執(zhí)行請求橙困,在該方法內部又是一個while(true)循環(huán)

在這個方法里attemptRetryOnException總共被調用了四次:

  • 發(fā)生SocketTimeoutException時(Socket通信超時瞧掺,即從服務端讀取數(shù)據(jù)時超時)
  • 發(fā)生ConnectTimeoutException時(請求超時,即連接HTTP服務端超時或者等待HttpConnectionManager返回可用連接超時)
  • 發(fā)生IOException凡傅,相應的狀態(tài)碼401/403(授權未通過)時
  • 發(fā)生IOException辟狈,相應的狀態(tài)碼301/302(URL發(fā)生轉移)時

現(xiàn)在我們歸納一下,首先假設我們設置了請求重試的策略(Volley默認最大請求重試次數(shù)為0夏跷,即不重試)哼转,其次BasicNetwork的performRequest方法的外面其實是個while循環(huán),假如在網(wǎng)絡請求過程中發(fā)生了異常槽华,如超時壹蔓、認證未通過等上述四個被調用的異常,catch這個異常的代碼會看看是否可以重試猫态,如果可以重試佣蓉,就會把這個異常吞噬掉,然后進入下一次循環(huán)亲雪,否則超過重試次數(shù)勇凭,attemptRetryOnException內部再拋出異常,此時交由上一層代碼去處理匆光,并退出while循環(huán)。

可能還有個疑問酿联,上面說的交由上一層代碼去處理是到底是怎么處理并退出while循環(huán)的终息?這里因為BasicNetwork的performRequest方法并沒有捕獲VolleyError異常,因此沒有被try&catch住的異常會繼續(xù)往外拋出贞让,這里我們回過頭來看看NetworkDispatcher的run方法里頭:

public class NetworkDispatcher extends Thread {

    ......

    @Override
    public void run() {

        ......

        while (true) {

            ......

            try {

                ......


                // Perform the network request. 真正執(zhí)行網(wǎng)絡請求的地方周崭,BasicNetwork超時拋出的VolleyError最終會拋出到這里
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                
                ......

                mDelivery.postResponse(request, response);
            } catch (VolleyError volleyError) {
                // 捕獲VolleyError異常,通過主線程Handler回調用戶設置的ErrorListener中的onErrorResponse回調方法
                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);
            }
        }
    }
}

Request無法繼續(xù)重試后拋出的VolleyError異常,會被NetworkDispatcher捕獲喳张,然后利用Delivery去回調用戶設置的ErrorListener续镇。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市销部,隨后出現(xiàn)的幾起案子摸航,更是在濱河造成了極大的恐慌制跟,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酱虎,死亡現(xiàn)場離奇詭異雨膨,居然都是意外死亡,警方通過查閱死者的電腦和手機读串,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門聊记,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恢暖,你說我怎么就攤上這事排监。” “怎么了杰捂?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵舆床,是天一觀的道長。 經(jīng)常有香客問我琼娘,道長峭弟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任脱拼,我火速辦了婚禮瞒瘸,結果婚禮上,老公的妹妹穿的比我還像新娘熄浓。我一直安慰自己情臭,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布赌蔑。 她就那樣靜靜地躺著俯在,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娃惯。 梳的紋絲不亂的頭發(fā)上跷乐,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音趾浅,去河邊找鬼愕提。 笑死,一個胖子當著我的面吹牛皿哨,可吹牛的內容都是我干的浅侨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼证膨,長吁一口氣:“原來是場噩夢啊……” “哼如输!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤不见,失蹤者是張志新(化名)和其女友劉穎澳化,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脖祈,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡肆捕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了盖高。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慎陵。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖喻奥,靈堂內的尸體忽然破棺而出席纽,到底是詐尸還是另有隱情,我是刑警寧澤撞蚕,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布润梯,位于F島的核電站,受9級特大地震影響甥厦,放射性物質發(fā)生泄漏纺铭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一刀疙、第九天 我趴在偏房一處隱蔽的房頂上張望舶赔。 院中可真熱鬧,春花似錦谦秧、人聲如沸竟纳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锥累。三九已至,卻和暖如春集歇,著一層夾襖步出監(jiān)牢的瞬間桶略,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工诲宇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留际歼,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓焕窝,卻偏偏與公主長得像蹬挺,于是被迫代替她去往敵國和親维贺。 傳聞我的和親對象是個殘疾皇子它掂,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容