RxJava的用武之地
Rxjava這個(gè)庫(kù)和其他常見(jiàn)庫(kù)不太一樣,一般的庫(kù)例如Glide,ButterKnife都是為了解決實(shí)際問(wèn)題出現(xiàn)的,一定程度上是剛需。Glide庫(kù)如果不用他,那么應(yīng)用自己就要處理圖片下載、壓縮、內(nèi)存管理、多級(jí)緩存等等復(fù)雜的邏輯。這類(lèi)問(wèn)題復(fù)雜而常見(jiàn),而像Glide這類(lèi)的輪子,Api的設(shè)計(jì)都比較友好,一個(gè)簡(jiǎn)單的api調(diào)用就能完成一個(gè)原本很復(fù)雜的功能,簡(jiǎn)直不要太爽。
Glide.with(context)
.load(url)//圖片加載
.crossFade()//動(dòng)畫(huà)設(shè)置
.placeholder(R.drawable.place_image)//占位圖
.error(R.drawable.error_image)//失敗占位圖
.override(width,height)//圖片裁剪
.thumbnail(thumbnailRequest)//配置縮略圖
.diskCacheStrategy(DiskCacheStrategy.SOURCE)//緩存策略
.into(imageView);
而Rxjava,你剛開(kāi)始看起來(lái),都不知道他是干什么的。“異步處理”?不是一般都使用觀察者模式嗎?AsyncTask,Handler也可以,要rxjava干嘛?如果你有興趣研究過(guò)一點(diǎn)rxjava,會(huì)發(fā)現(xiàn)網(wǎng)上的教程都會(huì)說(shuō):"zip map flatmap debounce等操作符把異步回調(diào)變得‘簡(jiǎn)潔’‘優(yōu)雅’",然后對(duì)比一下原來(lái)的代碼和使用rxjava后的代碼,最后感嘆一下rxjava設(shè)計(jì)的鬼才和功能的強(qiáng)大。我自己在初次接觸rxjava時(shí)也感覺(jué),這些rxjava的優(yōu)點(diǎn)描述比較空洞,這項(xiàng)技術(shù)的意義大于實(shí)用。
實(shí)際情況是這樣么?在具體開(kāi)發(fā)中,異步調(diào)用給我們的最大困擾是:異步回調(diào)的時(shí)間并不可控。當(dāng)有多個(gè)異步回調(diào)時(shí),這些調(diào)用相互聯(lián)系和依賴(lài),搞清楚每個(gè)回調(diào)何時(shí)返回是個(gè)重要的問(wèn)題。在每個(gè)關(guān)鍵時(shí)間節(jié)點(diǎn)對(duì)‘分散的callback’做正確的事,有過(guò)類(lèi)似編程經(jīng)驗(yàn)的人都知道,是非常痛苦的事,如果還想代碼容易看懂,簡(jiǎn)直是瘋了。
rxjava號(hào)稱(chēng)異步調(diào)用的終極解決方案,能否解決以上困擾?隨著學(xué)習(xí)和應(yīng)用的深入,體會(huì)會(huì)更明顯。以下會(huì)用一個(gè)稍復(fù)雜的例子,實(shí)操一個(gè)復(fù)雜異步場(chǎng)景,看看rxjava處理的怎么樣。
典型復(fù)雜異步場(chǎng)景 -- Token的前置校驗(yàn)
經(jīng)常遇到這種需求,接口的請(qǐng)求依賴(lài)token信息。一個(gè)請(qǐng)求需要先請(qǐng)求token(token如果存在緩存則使用緩存),依賴(lài)這個(gè)token才能進(jìn)行正常網(wǎng)絡(luò)請(qǐng)求。這個(gè)token有一定的時(shí)效性,在時(shí)效性內(nèi)可以使用緩存,過(guò)期后需要重新請(qǐng)求token并重新發(fā)起一次請(qǐng)求。這個(gè)流程可以歸納如下圖:
光看這些需求,是不是覺(jué)得已經(jīng)夠你喝一壺了,別忙,還有些潛在的邏輯這個(gè)圖沒(méi)有表現(xiàn)出來(lái):
1 高并發(fā)網(wǎng)絡(luò)請(qǐng)求時(shí),如果token正在請(qǐng)求,需要對(duì)請(qǐng)求阻塞(token請(qǐng)求過(guò)程中,不再接受新的token請(qǐng)求)
2 阻塞的同時(shí),要把這些請(qǐng)求記錄下來(lái),token請(qǐng)求成功后,再‘依次’發(fā)送這些阻塞的請(qǐng)求。
3 token失效情況下,網(wǎng)絡(luò)請(qǐng)求限制重試次數(shù)。(防止遞歸調(diào)用)
4 token請(qǐng)求本身,重試策略需單獨(dú)配置。
不使用rxjava,我們?nèi)绾螌?shí)現(xiàn)上述需求:
1、網(wǎng)絡(luò)請(qǐng)求前,對(duì)token是否有緩存判斷,如果沒(méi)有先請(qǐng)求token,并把這個(gè)請(qǐng)求阻塞且緩存
2、token請(qǐng)求過(guò)程中,如果有新的token請(qǐng)求進(jìn)來(lái),加入阻塞隊(duì)列
3、token請(qǐng)求后,通知阻塞的隊(duì)列(廣播等方式),依次進(jìn)行阻塞的請(qǐng)求
4、對(duì)兩種次數(shù)限制,分別做邏輯判斷
以上就是傳統(tǒng)實(shí)現(xiàn)方法,就不貼代碼了,這樣實(shí)現(xiàn)有以下特點(diǎn):
1、要時(shí)刻維護(hù)一個(gè)阻塞隊(duì)列 (注意其添加和清空的時(shí)機(jī))
2、token請(qǐng)求結(jié)束后,有一個(gè)回調(diào)機(jī)制通知阻塞隊(duì)列,(這個(gè)回調(diào)需要注冊(cè)和反注冊(cè))
3、兩處的次數(shù)限制,次數(shù)維護(hù)的變量,不好維護(hù)(一般動(dòng)態(tài)秘鑰為了便于使用會(huì)做成單例,單例內(nèi)的變量類(lèi)似static,維護(hù)較復(fù)雜)
4、請(qǐng)求重試的邏輯不好實(shí)現(xiàn),
我們可以看到這里涉及到很多靜態(tài)變量的維護(hù),廣播等異步回調(diào)的處理,這種情況一多,編程者會(huì)變得很被動(dòng)。而且token的異步請(qǐng)求和真正的網(wǎng)絡(luò)異步請(qǐng)求雜糅在一起,增大了問(wèn)題的復(fù)雜性。
我們來(lái)看下rxjava如何處理:
一些代碼網(wǎng)絡(luò)請(qǐng)求部分與前一篇博客《基于RxJava Retrofit的網(wǎng)絡(luò)框架》相關(guān)。
先看看完整的請(qǐng)求過(guò)程
public static <R> Observable send(final MapiHttpRequest request, final MapiTypeReference<R> t){
return Observable.defer(new Callable<ObservableSource<String>>() {
@Override
public ObservableSource<String> call() throws Exception {
//傳入token緩存
return Observable.just(Store.sToken);
}
}).flatMap(new Function<String, ObservableSource<R>>() {
@Override
public ObservableSource<R> apply(String key) throws Exception {
if(TextUtils.isEmpty(key) && !request.skipCheckKeyValid()){
//token沒(méi)有緩存,需要請(qǐng)求Token
return Observable.<R>error(new KeyNotValidThrowable());
} else {
//Token存在緩存,直接請(qǐng)求
return sendRequestInternal(request,t);
}
}
})
//進(jìn)入失敗重試流程
.retryWhen(new Function<Observable<? extends Throwable>, ObservableSource<String>>() {
private int retryCount = 0;
@Override
public ObservableSource<String> apply(Observable<? extends Throwable> throwableObservable) throws Exception {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<String>>() {
@Override
public ObservableSource<String> apply(Throwable throwable) throws Exception {
if (throwable instanceof KeyNotValidThrowable){
//同一Request,有過(guò)一次KeyNotValidThrowable,則不再重試
if (retryCount > 0){
return Observable.error(throwable);
} else {
//token緩存不在,進(jìn)入TokenLoader請(qǐng)求token
retryCount++;
return TokenLoader.getInstance().getNetTokenLocked();
}
} else if (throwable instanceof ApiException){
//token過(guò)期的情況,重新獲取token,并重試
ApiException apiException = (ApiException)throwable;
if (apiException.getCode() == MapiResultCode.SECRETKEY_EXPIRED.value()){
if (retryCount > 0){
return Observable.error(throwable);
} else {
//token緩存失效,進(jìn)入TokenLoader請(qǐng)求token
retryCount++;
return DynamicKeyLoader.getInstance().getNetTokenLocked();
}
}
}
//其他類(lèi)型錯(cuò)誤,直接拋出,不再重試
return Observable.error(throwable);
}
});
}
});
}
也許你第一次看也挺暈,別怕,你順著注釋捋捋邏輯,是不是感覺(jué)代碼的實(shí)現(xiàn)好像畫(huà)了一個(gè)時(shí)序圖。
除了注釋以外,幾點(diǎn)說(shuō)明:
1、defer操作符的作用是在retry時(shí),會(huì)重新創(chuàng)建新的Observable,否則會(huì)使用上次的Observable,不會(huì)重新獲取Store.sToken
2、retryWhen操作符,與sendRequestInternal內(nèi)部統(tǒng)一配置的retryWhen并不沖突,相當(dāng)于二次retry
3、retryWhen中如果拋出error ,則不再重試;
4、重試請(qǐng)求,通過(guò)返回getNetTokenLocked這個(gè)subject實(shí)現(xiàn)。(下面詳述)
階段總結(jié):
整體的流程被壓縮到了一個(gè)函數(shù)中,rxjava本身的retrywhen和subject機(jī)制,已經(jīng)替我們完成了這么幾點(diǎn):
1、自動(dòng)重試的注冊(cè)和反注冊(cè),subject被回調(diào)完直接失效,再次請(qǐng)求要重新注冊(cè)。
2、高并發(fā)request,維護(hù)隊(duì)列,通過(guò)mTokenObservable的回調(diào)自動(dòng)解決了這個(gè)問(wèn)題
3、retry次數(shù)的維護(hù),由于每次request的retry都是重新創(chuàng)建的內(nèi)部類(lèi),所以變量的維護(hù)變的簡(jiǎn)單。
4、重試的邏輯被retry操作符自動(dòng)實(shí)現(xiàn)了,只要重寫(xiě)retry的返回值就可以控制重試的策略。
TokenLoader:Token的獲取過(guò)程
public class TokenLoader {
public static final String TAG = TokenLoader.class.getSimpleName();
private AtomicBoolean mRefreshing = new AtomicBoolean(false);
private PublishSubject<String> mPublishSubject;
private Observable<String> mTokenObservable;
private TokenLoader() {
final TokenRequest request = new TokenRequest(CarOperateApplication.getInstance());
mTokenObservable = Observable
.defer(new Callable<ObservableSource<TokenRequest>>() {
@Override
public ObservableSource<TokenRequest> call() throws Exception {
return Observable.just(request);
}
})
.flatMap(new Function<TokenRequest, ObservableSource<MapiHttpResponse<Boolean>>>() {
@Override
public ObservableSource<MapiHttpResponse<Boolean>> apply(RefreshKeyRequest refreshKeyRequest) throws Exception {
//Token請(qǐng)求接口
return ApiHelper.sendDynamicKey(refreshKeyRequest,new MapiTypeReference<MapiHttpResponse<Boolean>>(){});
}
})
.retryWhen(new Function<Observable<Throwable>, ObservableSource<TokenRequest>>() {
private int retryCount = 0;
@Override
public ObservableSource<TokenRequest> apply(Observable<Throwable> throwableObservable) throws Exception {
return throwableObservable.flatMap(new Function<Throwable, ObservableSource<TokenRequest>>() {
@Override
public ObservableSource<RefreshKeyRequest> apply(Throwable throwable) throws Exception {
retryCount++;
if (retryCount == 3){
//失敗次數(shù)達(dá)到閾值,更改請(qǐng)求策略
request.setFlag(0);
return Observable.just(request);
} else if (retryCount > 3){
//失敗次數(shù)超過(guò)閾值,拋出失敗,放棄請(qǐng)求
mRefreshing.set(false);
return Observable.error(throwable);
} else {
//再次請(qǐng)求token
return Observable.just(request);
}
}
});
}
})
// .delay(6000, TimeUnit.MILLISECONDS) //模擬token請(qǐng)求延遲
.map(new Function<MapiHttpResponse<Boolean>,String>() {
@Override
public String apply(MapiHttpResponse<Boolean> response) throws Exception {
//成功,保存token緩存
if (response.getContent().booleanValue() == true){
setCacheToken(response.getToken());
} else if (response.getContent().booleanValue() == false){
setCacheToken(UcarK.getSign());
}
//請(qǐng)求完成標(biāo)識(shí)
mRefreshing.set(false);
return getCacheToken();
}
});
}
public static TokenLoader getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
private static final TokenLoader INSTANCE = new TokenLoader();
}
public String getCacheToken() {
return Store.sToken;
}
public void setCacheToken(String key){
Store.sToken = key;
}
/**
*
* @return
*/
public Observable<String> getNetTokenLocked() {
if (mRefreshing.compareAndSet(false, true)) {
Log.d(TAG, "沒(méi)有請(qǐng)求,發(fā)起一次新的Token請(qǐng)求");
startTokenRequest();
} else {
Log.d(TAG, "已經(jīng)有請(qǐng)求,直接返回等待");
}
return mPublishSubject;
}
private void startTokenRequest() {
mPublishSubject = PublishSubject.create();
mTokenObservable.subscribe(mPublishSubject);
}
}
還是讀注釋?zhuān)俗⑨屢酝猓瑤c(diǎn)說(shuō)明:
1、mRefreshing的作用是在token請(qǐng)求過(guò)程中,不再允許新的token請(qǐng)求,
變量采用原子類(lèi),而非boolean;這樣在多線程環(huán)境下,原子類(lèi)的方法是線程安全的。
compareAndSet(boolean expect, boolean update)這個(gè)方法兩個(gè)作用
1)比較expect和mRefresh是否一致
2)將mRefreshing置為update
2、startTokenRequest()方法開(kāi)啟token請(qǐng)求,注意Observable在subscribe時(shí)才正式開(kāi)始
3、這里使用了PublishSubject較為關(guān)鍵,在rxjava中Subject既是observable,又是observer,在TokenLoader中,mPublishSubject是mTokenObservable的觀察者,token請(qǐng)求的會(huì)由mPublishSubject響應(yīng),同時(shí)mPublishSubject也作為Observable返回給TokenLoader的調(diào)用者作為retryWhen的返回值返回。(所以這里PublishSubject的泛型與send()方法中Observable的泛型應(yīng)該是一致的)
4、對(duì)于mRefreshing是true的情況,直接返回mPublishSubject,這樣每個(gè)阻塞的請(qǐng)求retryWhen都會(huì)等待mPublishSubject的返回值,回調(diào)通知的順序與加入阻塞的順序是隊(duì)列關(guān)系(先請(qǐng)求的接口,先回調(diào)),滿足我們的需求。
最后:
感覺(jué)怎么樣,是豁然開(kāi)朗還是越陷越深,不管那樣都沒(méi)有關(guān)系,你需要的是了解還存在另一種處理異步任務(wù)的方法。在你下一次遇到同樣讓你頭疼的問(wèn)題時(shí),你可以把這篇文章拿起來(lái)再看看,也許你的頭疼會(huì)好一點(diǎn)了。。。