在我們的日常開(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)。
我一直使用Square的retrofit和ReactiveX的RxJava倦春,接下來(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.