重温Volley源码(二):重试策略

目录

一、核心类

二、重试策略

参考资料

本文是建立在对Volley框架的工作流程有一定基础了解的情况下继续深入的,这里顺便贴上我写的上篇文章《重温Volley源码(一):工作流程》 ,欢迎阅读指正。

一、核心类

RetryPolicy:Volley定义的请求重试策略接口


public interface RetryPolicy {

    /**当前请求的超时时间
     * Returns the current timeout (used for logging).
     */
    public int getCurrentTimeout();

    /**当前请求重试的次数
     * 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的实现子类


public class DefaultRetryPolicy implements RetryPolicy {
    /** The current timeout in milliseconds. 当前超时时间*/
    private int mCurrentTimeoutMs; 

    /** The current retry count. 当前重试次数*/
    private int mCurrentRetryCount;

    /** The maximum number of attempts. 最大重试次数*/
    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 默认的重试次数*/
    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 {
        //当前重试次数++
        mCurrentRetryCount++;
        //当前超时时间计算
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        //判断是否还有剩余次数,如果没有则抛出VolleyError异常
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    /**
     * 判断当前Request的重试次数是否超过最大重试次数
     * Returns true if this policy has attempts remaining, false otherwise.
     */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}

二、重试策略

在深入源码阅读你会发现,retry方法会抛出VolleyError的异常,但该方法内部并不是重新发起了网络请求,而是变更重试策略的属性,如超时时间和重试次数,当超过重试策略设定的限定就会抛异常,这个可以在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); //内部实现重试次数、超时时间的变更,如果重试次数超过最大限定次数,该方法抛出异常
        } catch (VolleyError e) {
            //当超过最大重试次数,捕获到异常,更改该请求的标记
            request.addMarker(
                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
            //当仍然可以进行重试的时候,不会执行到catch语句,但是当执行到catch语句的时候,表示已经不能进行重试了,就抛出异常  中断while(true)循环
            throw e;
        }
        //给请求添加标记,请求了多少次  
        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    }

}

继续跟踪attemptRetryOnException方法被调用的地方,来到了BasicNetwork的performRequest方法:


public class BasicNetwork implements Network {

......

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

            try {
                ......

                //如果发生了超时、认证失败等错误,进行重试操作,直到成功。若attemptRetryOnException再抛出异常则结束
                //当catch后没有执行上面的return,而当前又是一个while(true)循环,可以保证下面的请求重试的执行,是利用循环进行请求重试,请求重试策略只是记录重试的次数、超时时间等内容
                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来执行请求,在该方法内部又是一个while(true)循环

在这个方法里attemptRetryOnException总共被调用了四次:

  • 发生SocketTimeoutException时(Socket通信超时,即从服务端读取数据时超时)
  • 发生ConnectTimeoutException时(请求超时,即连接HTTP服务端超时或者等待HttpConnectionManager返回可用连接超时)
  • 发生IOException,相应的状态码401/403(授权未通过)时
  • 发生IOException,相应的状态码301/302(URL发生转移)时

现在我们归纳一下,首先假设我们设置了请求重试的策略(Volley默认最大请求重试次数为0,即不重试),其次BasicNetwork的performRequest方法的外面其实是个while循环,假如在网络请求过程中发生了异常,如超时、认证未通过等上述四个被调用的异常,catch这个异常的代码会看看是否可以重试,如果可以重试,就会把这个异常吞噬掉,然后进入下一次循环,否则超过重试次数,attemptRetryOnException内部再抛出异常,此时交由上一层代码去处理,并退出while循环。

可能还有个疑问,上面说的交由上一层代码去处理是到底是怎么处理并退出while循环的?这里因为BasicNetwork的performRequest方法并没有捕获VolleyError异常,因此没有被try&catch住的异常会继续往外抛出,这里我们回过头来看看NetworkDispatcher的run方法里头:

public class NetworkDispatcher extends Thread {

    ......

    @Override
    public void run() {

        ......

        while (true) {

            ......

            try {

                ......


                // Perform the network request. 真正执行网络请求的地方,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无法继续重试后抛出的VolleyError异常,会被NetworkDispatcher捕获,然后利用Delivery去回调用户设置的ErrorListener。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容