All RxJava - 為Retrofit添加重試

在我們的日常開(kāi)發(fā)中離不開(kāi)I/O操作棒卷,尤其是網(wǎng)絡(luò)請(qǐng)求顾孽,但并不是所有的請(qǐng)求都是可信賴的,因此我們必須為APP添加請(qǐng)求重試功能比规。

對(duì)于一個(gè)網(wǎng)絡(luò)請(qǐng)求重試而言若厚,我認(rèn)為它至少應(yīng)該做到以下兩點(diǎn):

  • 可配置次數(shù)的重試
    因?yàn)椴⒉皇撬械木W(wǎng)絡(luò)請(qǐng)求都需要頻繁地重試蜒什,比如說(shuō)一個(gè)重要的表單提交测秸,它應(yīng)該盡可能多失敗重連,相反地灾常,埋點(diǎn)上報(bào)等統(tǒng)計(jì)功能霎冯,它可能最多只需要重試一次就足夠了。因此針對(duì)不同的場(chǎng)景钞瀑,我們需要不同的重試次數(shù)肃晚。

  • 退避策略
    我們應(yīng)該為請(qǐng)求重試加入一個(gè)合理的退避算法仔戈,而不是一旦遭遇了失敗就立即無(wú)腦般的再次發(fā)起請(qǐng)求,這樣做沒(méi)有一點(diǎn)好處拧廊,不但降低了用戶體驗(yàn)监徘,甚至還在浪費(fèi)網(wǎng)絡(luò)資源。一個(gè)合理的重試策略應(yīng)該是:遇到網(wǎng)絡(luò)異常時(shí)應(yīng)該等待一段時(shí)間后再重試吧碾,若遇到的異常次數(shù)越多凰盔,等待(退避)的時(shí)間就應(yīng)該越長(zhǎng)。

我一直使用SquareretrofitReactiveXRxJava倦春,接下來(lái)我就來(lái)分享一下我是如何使用這兩個(gè)庫(kù)來(lái)實(shí)現(xiàn)一個(gè)可配置次數(shù)的退避重試策略的户敬。

Repeat? Retry!

RxJava中有兩個(gè)操作符能夠觸發(fā)重訂閱,分別是:

從上面的彈珠圖中睁本,我們可以了解到尿庐,這兩個(gè)操作符的區(qū)別僅僅是針對(duì)不同的“終止事件”來(lái)會(huì)觸發(fā)重訂閱:.repeat()接收到onCompleted后觸發(fā)重訂閱;而.retry()則是接收到OnError后觸發(fā)重訂閱呢堰。

需要注意的是抄瑟,千萬(wàn)不要使用這兩個(gè)操作符無(wú)限地重訂閱源Observable,一定要在恰當(dāng)?shù)臅r(shí)候通過(guò)取消訂閱的方式來(lái)停止它們枉疼,避免陷入無(wú)限循環(huán)皮假,從而導(dǎo)致系統(tǒng)崩潰。除此之外還可以使用它們的重載函數(shù).repeat(n).retry(n)骂维,來(lái)設(shè)置一個(gè)合適的重訂閱次數(shù)n惹资。

ps : 寫這篇博客的時(shí)候我參照了RxJava-1.2.10的源碼,.repeat().retry()的內(nèi)部實(shí)現(xiàn)幾乎是一模一樣的航闺,一點(diǎn)細(xì)微不同是:除了取消訂閱能夠同時(shí)終止它倆的重訂閱之外褪测,.repeat()還能被OnError終止,相對(duì)的.retry()能被onCompleted終止。

回到本篇文章的主題上汰扭,我們需要的是在遭遇I/O異常時(shí)稠肘,發(fā)起重試,而不是請(qǐng)求成功時(shí)萝毛,很明顯的.retry()勝出项阴!

Retry?RetryWhen!

首先笆包,我們需要認(rèn)清的事實(shí)是:所有的網(wǎng)絡(luò)異常都屬于I/O異常环揽。

我們的重點(diǎn)是,只有遭遇了IOException時(shí)才重試網(wǎng)絡(luò)請(qǐng)求庵佣,也就是說(shuō)那些IllegalStateException歉胶,NullPointerException或者當(dāng)你使用gson來(lái)解析json時(shí)還可能出現(xiàn)的JsonParseException等非I/O異常均不在重試的范圍內(nèi)。

因此.retry()以及它的重載函數(shù)已經(jīng)不能滿足我們的需求了巴粪,好在RxJava為我們提供了另一個(gè)非常有用的操作符.retryWhen()通今,我們可以通過(guò)判斷異常類型,來(lái)決定是否發(fā)起重試(重訂閱)肛根。

.retryWhen()的函數(shù)簽名如下:

public final Observable<T> retryWhen(Func1<? super Observable<? extends java.lang.Throwable>,? extends Observable<?>> notificationHandler)

其中notificationHandler是我們需要實(shí)現(xiàn)的函數(shù)辫塌,它有兩個(gè)概念必須弄清:

  • 參數(shù)Observable<Throwable>,其中的泛型意指上游操作符拋出的異常派哲,我們可以通過(guò)這個(gè)條件來(lái)判斷異常的類型臼氨。

  • 返回值Observable<?>,通配符(泛型)表示我們可以返回任意類型的Observable芭届,它的作用是:一旦這個(gè)Observable通過(guò)onNext()發(fā)送事件储矩,則重訂閱(重試)發(fā)生一次,如果這個(gè)Observable調(diào)用了onComplete或者onError那么將跳過(guò)重訂閱褂乍,最終這些終止事件將會(huì)向下傳遞持隧,從此這個(gè)操作符的重訂閱功能也就失效了。

RX-CODE!

下面這段代碼是我使用的notificationHandler的實(shí)現(xiàn)類RetryWhenHandler逃片,它基本滿足了我的重試要求舆蝴。

final class RetryWhenHandler implements Func1<Observable<? extends Throwable>, Observable<Long>> {

  private static final int INITIAL = 1;
  private int maxConnectCount = 1;

  RetryWhenHandler(int retryCount) {
    this.maxConnectCount += retryCount;
  }

  @Override public Observable<Long> call(Observable<? extends Throwable> errorObservable) {
    return errorObservable.zipWith(Observable.range(INITIAL, maxConnectCount),
        new Func2<Throwable, Integer, ThrowableWrapper>() {
          @Override public ThrowableWrapper call(Throwable throwable, Integer i) {

            //①
            if (throwable instanceof IOException) return new ThrowableWrapper(throwable, i);

            return new ThrowableWrapper(throwable, maxConnectCount);
          }
        }).concatMap(new Func1<ThrowableWrapper, Observable<Long>>() {
      @Override public Observable<Long> call(ThrowableWrapper throwableWrapper) {

        final int retryCount = throwableWrapper.getRetryCount();

        //②
        if (maxConnectCount == retryCount) {
          return Observable.error(throwableWrapper.getSourceThrowable());
        }

        //③
        return Observable.timer((long) Math.pow(2, retryCount), TimeUnit.SECONDS,
            Schedulers.immediate());
      }
    });
  }

  private static final class ThrowableWrapper {

    private Throwable sourceThrowable;
    private Integer retryCount;

    ThrowableWrapper(Throwable sourceThrowable, Integer retryCount) {
      this.sourceThrowable = sourceThrowable;
      this.retryCount = retryCount;
    }

    Throwable getSourceThrowable() {
      return sourceThrowable;
    }

    Integer getRetryCount() {
      return retryCount;
    }
  }
}

有三點(diǎn)地方需要注意:

① 只在IOException的情況下記錄本次請(qǐng)求在最大請(qǐng)求次數(shù)中的位置,否則視為最后一次請(qǐng)求题诵,避免多余的請(qǐng)求重試洁仗。

②如果最后一次網(wǎng)絡(luò)請(qǐng)求依然遭遇了異常,則將此異常繼續(xù)向下傳遞性锭,以便在最后的onError()函數(shù)中處理赠潦。

③使用.timer()操作符實(shí)現(xiàn)一個(gè)簡(jiǎn)單的二進(jìn)制指數(shù)退避算法,需要注意的是.timer()操作符默認(rèn)執(zhí)行在Schedulers.computation()草冈,我們并不希望它切換到別的線程去執(zhí)行重試邏輯她奥,因此使用了它的重載函數(shù)瓮增,并指定在當(dāng)前線程立即執(zhí)行。

@Retry

由于retrofit的請(qǐng)求參數(shù)是基于函數(shù)描述的哩俭,因此我們創(chuàng)建一個(gè)注解Retry用來(lái)描述重試次數(shù)绷跑。代碼如下:

@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface Retry {
  //retry times when an IOException is encountered
  int count() default 0;
}

值得一提的是,我們只希望這個(gè)注解能夠被聲明在方法上凡资,而且必須是RuntimeVisibleAnnotations砸捏,否則我們無(wú)法在運(yùn)行時(shí)拿到。

假設(shè)你已經(jīng)閱讀過(guò)了retrofit的源碼隙赁,至少知道如何使用CallAdapter.Factory來(lái)定義一個(gè)CallAdapter垦藏。如果對(duì)它不了解,則只需要記住伞访,在CallAdapter.Factory中我們必須實(shí)現(xiàn)的抽象方法掂骏,其中第二個(gè)參數(shù)annotations包含了我們定義在方法上的所有RUNTIME注解。:

 public abstract @Nullable CallAdapter<?, ?> get(Type returnType, Annotation[] annotations,
 Retrofit retrofit);

接下來(lái)厚掷,稍微改造一下RxJavaCallAdapter的構(gòu)造函數(shù)弟灼,添加一個(gè)重試變量,并在Observable調(diào)用鏈中添加我們之前已經(jīng)寫好的RetryWhenHandler:

final class RxJavaCallAdapter<R> implements CallAdapter<R, Object> {
  private final Type responseType;
  private final @Nullable Scheduler scheduler;
  private final int retryCount;
  private final boolean isAsync;
  private final boolean isResult;
  private final boolean isBody;
  private final boolean isSingle;
  private final boolean isCompletable;

  RxJavaCallAdapter(Type responseType, @Nullable Scheduler scheduler, int retryCount,
      boolean isAsync, boolean isResult, boolean isBody, boolean isSingle, boolean isCompletable) {
    this.responseType = responseType;
    this.scheduler = scheduler;
    this.retryCount = retryCount
    this.isAsync = isAsync;
    this.isResult = isResult;
    this.isBody = isBody;
    this.isSingle = isSingle;
    this.isCompletable = isCompletable;
  }

  @Override public Type responseType() {
    return responseType;
  }

  @Override public Object adapt(Call<R> call) {
    OnSubscribe<Response<R>> callFunc = isAsync
        ? new CallEnqueueOnSubscribe<>(call)
        : new CallExecuteOnSubscribe<>(call);

    OnSubscribe<?> func;
    if (isResult) {
      func = new ResultOnSubscribe<>(callFunc);
    } else if (isBody) {
      func = new BodyOnSubscribe<>(callFunc);
    } else {
      func = callFunc;
    }
    Observable<?> observable = Observable.create(func).retryWhen(new RetryWhenHandler(retryCount));

    if (scheduler != null) {
      observable = observable.subscribeOn(scheduler);
    }

    if (isSingle) {
      return observable.toSingle();
    }
    if (isCompletable) {
      return observable.toCompletable();
    }
    return observable;
  }
}

解析@Retry注解的操作需要放在RxJavaCallAdapterFactory#Line104中:

int count;
for (Annotation annotation : annotations) {
  if (!Retry.class.isAssignableFrom(annotation.getClass())) continue;
  count = Retry.class.cast(annotation).count();
  if (count<0) throw new IllegalArgumentException(
      "The count in the \'@Retry\' is less than zero");
}

總結(jié)

至此冒黑,我們基本完成了通過(guò)RxJava為retrofit添加重試的功能袜爪,它利用retrofit本身的“基于方法描述的特性”,因此足夠靈活薛闪,而且擴(kuò)展性也很高 : )

當(dāng)然,不局限于此俺陋,如果你使用了okhttp豁延,還可以通過(guò)自定義Interceptor的方式,為你的網(wǎng)絡(luò)請(qǐng)求添加失敗重試功能腊状。

這篇文章只是提供一個(gè)簡(jiǎn)單的思路诱咏,對(duì)于健壯應(yīng)用程序,我們?nèi)匀恍枰粩嗟膰L試與探索缴挖,如果你有更好的經(jīng)驗(yàn)袋狞,歡迎分享,如果你喜歡這篇文章映屋,請(qǐng)點(diǎn)個(gè)贊苟鸯。

文中所有代碼,都可以從github上獲取Forked from retrofit棚点,希望這篇文章能夠?qū)δ闼袔椭绱Αappy coding, enjoy it.

參考

【譯】對(duì)RxJava中.repeatWhen()和.retryWhen()操作符的思考 - 小鄧子

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瘫析,隨后出現(xiàn)的幾起案子砌梆,更是在濱河造成了極大的恐慌默责,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咸包,死亡現(xiàn)場(chǎng)離奇詭異桃序,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)烂瘫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門媒熊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人忱反,你說(shuō)我怎么就攤上這事泛释。” “怎么了温算?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵怜校,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我注竿,道長(zhǎng)茄茁,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任巩割,我火速辦了婚禮裙顽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宣谈。我一直安慰自己愈犹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布闻丑。 她就那樣靜靜地躺著漩怎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗦嗡。 梳的紋絲不亂的頭發(fā)上勋锤,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音侥祭,去河邊找鬼叁执。 笑死,一個(gè)胖子當(dāng)著我的面吹牛矮冬,可吹牛的內(nèi)容都是我干的谈宛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼胎署,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼入挣!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起硝拧,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤径筏,失蹤者是張志新(化名)和其女友劉穎葛假,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體滋恬,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡聊训,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恢氯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片带斑。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖勋拟,靈堂內(nèi)的尸體忽然破棺而出勋磕,到底是詐尸還是另有隱情,我是刑警寧澤敢靡,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布挂滓,位于F島的核電站,受9級(jí)特大地震影響啸胧,放射性物質(zhì)發(fā)生泄漏赶站。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一纺念、第九天 我趴在偏房一處隱蔽的房頂上張望贝椿。 院中可真熱鬧,春花似錦陷谱、人聲如沸烙博。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)渣窜。三九已至,卻和暖如春焙格,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背夷都。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工眷唉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人囤官。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓冬阳,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親党饮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肝陪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容